diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 33b8ea7..eafc5bd 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -6,31 +6,66 @@ on: pull_request: branches: [ main ] +permissions: + contents: read + pull-requests: write + actions: read + jobs: - integration-tests: + discover-testcases: runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write + outputs: + testcases: ${{ steps.discover.outputs.testcases }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Discover testcases + id: discover + run: | + # Find all testcase folders (excluding common folders like README, etc.) + testcase_dirs=$(find testcases -maxdepth 1 -type d -name "*-*" | sed 's|testcases/||' | sort) + echo "Found testcase directories:" + echo "$testcase_dirs" + + # Convert to JSON array for matrix + testcases_json=$(echo "$testcase_dirs" | jq -R -s -c 'split("\n")[:-1]') + echo "testcases=$testcases_json" >> $GITHUB_OUTPUT + + integration-tests: + needs: [discover-testcases] + runs-on: ubuntu-latest + container: + image: ghcr.io/astral-sh/uv:python3.12-bookworm + env: + UIPATH_JOB_KEY: "3a03d5cb-fa21-4021-894d-a8e2eda0afe0" strategy: + fail-fast: false matrix: - include: - - build-dir: quickstart-agent - - build-dir: simple-hitl-agent - + testcase: ${{ fromJson(needs.discover-testcases.outputs.testcases) }} + environment: [alpha, staging] # temporary disable [cloud] + + name: "${{ matrix.testcase }} / ${{ matrix.environment }}" + steps: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Install Dependencies + run: uv sync - - name: Build Docker image (${{ matrix.build-dir }}) + - name: Run testcase + env: + CLIENT_ID: ${{ matrix.environment == 'alpha' && secrets.ALPHA_TEST_CLIENT_ID || matrix.environment == 'staging' && secrets.STAGING_TEST_CLIENT_ID || matrix.environment == 'cloud' && secrets.CLOUD_TEST_CLIENT_ID }} + CLIENT_SECRET: ${{ matrix.environment == 'alpha' && secrets.ALPHA_TEST_CLIENT_SECRET || matrix.environment == 'staging' && secrets.STAGING_TEST_CLIENT_SECRET || matrix.environment == 'cloud' && secrets.CLOUD_TEST_CLIENT_SECRET }} + BASE_URL: ${{ matrix.environment == 'alpha' && secrets.ALPHA_BASE_URL || matrix.environment == 'staging' && secrets.STAGING_BASE_URL || matrix.environment == 'cloud' && secrets.CLOUD_BASE_URL }} + working-directory: testcases/${{ matrix.testcase }} run: | - docker build -f testcases/${{ matrix.build-dir }}/Dockerfile \ - -t ${{ matrix.build-dir }}:test \ - --build-arg CLIENT_ID="${{ secrets.ALPHA_TEST_CLIENT_ID }}" \ - --build-arg CLIENT_SECRET="${{ secrets.ALPHA_TEST_CLIENT_SECRET }}" \ - --build-arg BASE_URL="${{ secrets.ALPHA_BASE_URL }}" \ - . \ No newline at end of file + echo "Running testcase: ${{ matrix.testcase }}" + echo "Environment: ${{ matrix.environment }}" + echo "Working directory: $(pwd)" + + # Execute the testcase run script directly + bash run.sh + bash ../common/validate_output.sh diff --git a/samples/quickstart-agent/AGENTS.md b/samples/quickstart-agent/AGENTS.md new file mode 100644 index 0000000..4982342 --- /dev/null +++ b/samples/quickstart-agent/AGENTS.md @@ -0,0 +1,724 @@ +# AGENTS.md – Unified Guide for UiPath Workflows & Agentic Solutions + +This document provides a comprehensive guide to building, testing, and deploying Python automations and intelligent agents on the UiPath platform using the `uipath-python` and `uipath-langchain-python` SDK, just like the agent present in this folder. + +--- + +## 0) Local Environment Setup (with `uv`) + +This project assumes you’re using [`uv`](https://github.com/astral-sh/uv) for fast Python installs, virtualenvs, and command execution. + +### 0.1 Install Python & create a virtualenv + +```bash +# Install a modern Python (adjust the version if you need) +uv python install 3.12 + +# Create a local virtual environment (uses the latest installed Python by default) +uv venv +``` + +> **Tip:** You don’t need to “activate” the venv if you use `uv run ...`, but if you prefer activation: +> +> - macOS/Linux: `source .venv/bin/activate` +> - Windows PowerShell: `.venv\Scripts\Activate.ps1` + +### 0.2 Install dependencies + +```bash +uv pip install -e . +``` + +### 0.3 Run the UiPath CLI via `uv` + +Using `uv run` means you don’t have to activate the venv: + +```bash +# Log in and write credentials to .env +uv run uipath auth + +# Initialize (scans entrypoints and updates uipath.json) +uv run uipath init + +# Interactive dev loop (recommended) +uv run uipath dev + +# Non-interactive run of classic entrypoint +uv run uipath run main.py '{"message": "Hello from uv"}' + +# If you exposed a compiled graph entrypoint called "agent" +# (name exposed in langgraph.json) +uv run uipath run agent '{"topic": "Quarterly sales"}' +``` + +## 1) Core Developer Workflow (CLI) + +The **unified CLI** supports both classic automations and LangGraph agents. + +### 1.1 Authenticate + +```bash +uipath auth +``` + +- Opens a browser login and writes credentials to `.env`. +- Required before local runs or publishing. + +### 1.2 Initialize + +```bash +uipath init + +``` + +- Scans the classic entrypoint (`main.py`) and creates/updates `uipath.json` with **input/output schema** and **resource bindings**. +- Re‑run when you change function signatures, add Assets/Queues/Buckets, or new graph entrypoints. + +### 1.3 Local Run & Debug + +```bash +# Interactive development mode (recommended) +uipath dev + +# Non‑interactive quick runs +uipath run main.py '{"message": "Hello from the CLI"}' +# For a compiled graph +uipath run agent '{"topic": "Quarterly sales"}' +``` + +- `dev` shows live logs, traces, and chat history. + +### 1.4 Package, Publish, Deploy + +```bash +uipath pack +uipath publish +uipath deploy +``` + +- `deploy` is a wrapper that packs and publishes to your Orchestrator feed. +- Use per‑environment pipelines (Dev → Test → Prod folders/tenants). + +### 1.5 Other Useful Commands + +```bash +uipath invoke # Execute a process remotely (when configured) +uipath eval # Run evaluation scenarios for agents +uipath --help # Discover flags and subcommands +``` + +--- + +## 2) Environment, Credentials & Configuration + +Both SDKs read their configuration from **environment variables** (directly, or via `.env` loaded by `python-dotenv`). + +### 2.1 Minimal local `.env` + +```bash +UIPATH_URL="https://cloud.uipath.com/ORG/TENANT" +UIPATH_ACCESS_TOKEN="your-token" + +# Common defaults +UIPATH_FOLDER_PATH="Shared" +``` + +> **Best practice:** Commit `.env.example` (documenting required vars) but never commit `.env`. + +### 2.3 Loading configuration in code + +```python +from dotenv import load_dotenv +from uipath import UiPath +from uipath.models.errors import BaseUrlMissingError, SecretMissingError + +load_dotenv() +try: + sdk = UiPath() +except (BaseUrlMissingError, SecretMissingError) as e: + raise SystemExit(f"Config error: {e}. Run 'uipath auth' or set env vars.") +``` + +--- + +## 3) Classic Automation Track (Python SDK, `uipath`) + +### 3.1 Entrypoint shape (Pydantic IO strongly recommended) + +```python +# src/main.py +from pydantic import BaseModel +from typing import Optional +from dotenv import load_dotenv +from uipath import UiPath + +load_dotenv() + +class AutomationInput(BaseModel): + customer_id: str + message: str + +class AutomationOutput(BaseModel): + status: str + confirmation_code: Optional[str] = None + +def main(input: AutomationInput) -> AutomationOutput: + sdk = UiPath() + cfg = sdk.assets.retrieve(name="GlobalConfig") + print(f"Using API URL from asset: {cfg.value}") + return AutomationOutput(status="Success", confirmation_code="ABC-123") +``` + +### 3.2 Core recipes + +#### Execute a child process + +```python +import time +from dotenv import load_dotenv +from uipath import UiPath + +load_dotenv() +sdk = UiPath() +job = sdk.processes.invoke( + name="Process_To_Run_In_Finance", + input_arguments={"customer_id": 12345}, + folder_path="Finance", +) +while job.state in ("Pending", "Running"): + time.sleep(5) + job = sdk.jobs.retrieve(key=job.key) +print("State:", job.state, "Output:", job.output_arguments) +``` + +#### Assets – configuration & credentials + +```python +from uipath import UiPath + +sdk = UiPath() +plain = sdk.assets.retrieve(name="My_App_Config") +print("Endpoint:", plain.value) + +try: + cred = sdk.assets.retrieve_credential(name="My_App_Credential") + print("Got credential username:", cred.username) +except ValueError as e: + print("Credential unavailable in non‑robot context:", e) +``` + +#### Queues – transactional work + +```python +from uipath import UiPath + +sdk = UiPath() +QUEUE = "InvoiceProcessing" +sdk.queues.create_item( + name=QUEUE, + specific_content={"invoice_id": "INV-9876", "amount": 450.75, "vendor": "Supplier Inc."}, + priority="High", +) +trx = sdk.queues.create_transaction_item(name=QUEUE) +if trx: + try: + # ... process trx.specific_content ... + sdk.queues.complete_transaction_item(trx.id, {"status": "Successful", "message": "OK"}) + except Exception as e: + sdk.queues.complete_transaction_item(trx.id, {"status": "Failed", "is_successful": False, "processing_exception": str(e)}) +``` + +#### Buckets – file management + +```python +from uipath import UiPath + +sdk = UiPath() +with open("report.pdf", "w") as f: + f.write("sample report") + +sdk.buckets.upload(name="MonthlyReports", source_path="report.pdf", blob_file_path="2024/July/report.pdf") +sdk.buckets.download(name="InputFiles", blob_file_path="data/customers.xlsx", destination_path="local_customers.xlsx") +``` + +#### Context Grounding – RAG + +```python +from uipath import UiPath + +async def main(input: dict): + sdk = UiPath() + q = input.get("query") + hits = sdk.context_grounding.search(name="Internal_Wiki", query=q, number_of_results=3) + context = "\n".join([h.content for h in hits]) + enriched = f"Context:\n{context}\n\nAnswer: {q}" + resp = await sdk.llm.chat_completions(model="gpt-4o-mini-2024-07-18", messages=[{"role": "user", "content": enriched}]) + return {"answer": resp.choices[0].message.content} +``` + +#### Event triggers – Integration Service + +```python +from pydantic import BaseModel +from uipath import UiPath +from uipath.models import EventArguments + +class Output(BaseModel): + status: str + summary: str + +def main(input: EventArguments) -> Output: + sdk = UiPath() + payload = sdk.connections.retrieve_event_payload(input) + if "event" in payload and "text" in payload["event"]: + txt = payload["event"]["text"] + user = payload["event"].get("user", "Unknown") + summ = sdk.llm.chat(prompt=f"Summarize from {user}: {txt}", model="gpt-4") + return Output(status="Processed", summary=getattr(summ, "content", str(summ))) + return Output(status="Skipped", summary="Not a Slack message event") +``` + +#### Passing files between jobs – attachments + +```python +from uipath import UiPath +from uipath.models import InvokeProcess + +def main(input_args: dict): + sdk = UiPath() + csv = "id,name\n1,Alice\n2,Bob" + att_key = sdk.jobs.create_attachment(name="processed.csv", content=csv) + return InvokeProcess(name="LoadDataToSystem", input_arguments={"dataFileKey": str(att_key)}) +``` + +--- + +## 4) Agentic Track (LangGraph/LangChain SDK, `uipath-langchain`) + +### 4.1 Quick start – chat model + +```python +from uipath_langchain.chat import UiPathChat +from langchain_core.messages import HumanMessage + +chat = UiPathChat(model="gpt-4o-2024-08-06", max_retries=3) +print(chat.invoke([HumanMessage(content="Hello")]).content) +``` + +### 4.2 Simple graph example + +`graph = builder.compile()` is enough for the agent to run with `uipath run agent '{"topic": "Quarterly sales"}` + +```python +from langchain_core.messages import HumanMessage, SystemMessage +from langgraph.graph import START, StateGraph, END +from uipath_langchain.chat import UiPathChat +from pydantic import BaseModel +import os + +llm = UiPathChat(model="gpt-4o-mini-2024-07-18") + +class GraphState(BaseModel): + topic: str + +class GraphOutput(BaseModel): + report: str + +async def generate_report(state: GraphState) -> GraphOutput: + system_prompt = "You are a report generator. Please provide a brief report based on the given topic." + output = await llm.ainvoke([SystemMessage(system_prompt), HumanMessage(state.topic)]) + return GraphOutput(report=output.content) + +builder = StateGraph(GraphState, output=GraphOutput) + +builder.add_node("generate_report", generate_report) + +builder.add_edge(START, "generate_report") +builder.add_edge("generate_report", END) + +graph = builder.compile() +``` + +### 4.3 ReAct‑style agent with tools + +```python +from langgraph.graph import StateGraph, START, END +from langgraph.prebuilt import create_react_agent +from langchain_tavily import TavilySearch +from uipath_langchain.chat import UiPathChat +from pydantic import BaseModel + +class GraphState(BaseModel): + company_name: str + +tavily = TavilySearch(max_results=5) +llm = UiPathChat(model="gpt-4o-2024-08-06") +agent = create_react_agent(llm, tools=[tavily], prompt="You are a research assistant.") + +builder = StateGraph(GraphState) +builder.add_node("research", agent) +builder.add_edge(START, "research") +builder.add_edge("research", END) + +graph = builder.compile() +``` + +### 4.4 RAG – Context Grounding vector store & retriever + +```python +from uipath_langchain.vectorstores import ContextGroundingVectorStore +from uipath_langchain.chat import UiPathAzureChatOpenAI +from langchain_core.prompts import ChatPromptTemplate + +vs = ContextGroundingVectorStore(index_name="my_knowledge_base") +retriever = vs.as_retriever(search_kwargs={"k": 3}) + +prompt = ChatPromptTemplate.from_template("""Answer from context: +{context} +Question: {question} +""") +llm = UiPathAzureChatOpenAI(model="gpt-4o-2024-08-06") + +docs = retriever.invoke("Vacation policy?") +print(llm.invoke(prompt.format(context=docs, question="Vacation policy? ")).content) +``` + +### 4.5 Embeddings & structured outputs + +```python +from uipath_langchain.embeddings import UiPathAzureOpenAIEmbeddings +from uipath_langchain.chat import UiPathChat +from pydantic import BaseModel, Field + +emb = UiPathAzureOpenAIEmbeddings(model="text-embedding-3-large") +vec = emb.embed_query("remote work policy") + +class EmailRule(BaseModel): + rule_name: str = Field(description="Name of the rule") + conditions: dict = Field(description="Rule conditions") + target_folder: str = Field(description="Target folder") + +schema_chat = UiPathChat(model="gpt-4o-2024-08-06").with_structured_output(EmailRule) +rule = schema_chat.invoke("Create a rule to move emails from noreply@company.com to Archive") +``` + +### 4.6 Observability – Async tracer + +```python +from uipath_langchain.tracers import AsyncUiPathTracer +from uipath_langchain.chat import UiPathChat + +tracer = AsyncUiPathTracer(action_name="my_action", action_id="unique_id") +chat = UiPathChat(model="gpt-4o-2024-08-06", callbacks=[tracer]) +``` + +--- + +## 5) LLM Gateway – Two ways to call models + +### 5.1 OpenAI‑compatible path + +```python +from uipath import UiPath +sdk = UiPath() +resp = sdk.llm_openai.chat.completions.create( + model="gpt-4", + messages=[{"role": "user", "content": "Summarize this invoice"}], +) +print(resp.choices[0].message.content) +``` + +### 5.2 Normalized UiPath LLM path + +```python +from uipath import UiPath +sdk = UiPath() +answer = sdk.llm.chat(prompt="Analyze customer feedback", model="gpt-4") +print(answer.content if hasattr(answer, "content") else answer) +``` + +--- + +## 6) Testing, Evaluation & Quality Gates + +- **Unit tests**: Pure functions in `src/` and graph nodes in `graphs/`. +- **E2E tests**: Use `uipath run` against local mocks or a Dev folder tenant. +- **Evaluations**: For agent behaviors, leverage `uipath eval` scenarios to benchmark prompt/graph changes. +- **Static checks**: `ruff`, `pyright`/`mypy` with type‑strict public APIs. + +Minimal sanity check: + +```python +import importlib.metadata, sys +print("uipath:", importlib.metadata.version("uipath")) +print("python:", sys.version) +``` + +## 8) Security, Secrets & Governance + +- **Never** commit secrets. Use Secret Manager / GitHub Actions secrets. +- Scope tokens to least privilege; rotate regularly. +- For **credential assets**, prefer Action Center/Robot context retrieval over plain text. +- Enable **telemetry** and **tracing** for auditability. + +--- + +## 9) Operational Patterns & Pitfalls + +- **Folder context**: Prefer `folder_path`/`folder_key` explicitly in critical calls. +- **Idempotency**: For queues and bucket uploads, include natural keys and conflict handling. +- **Backpressure**: Poll jobs with exponential backoff; avoid tight loops. +- **Timeouts**: Raise `UIPATH_TIMEOUT_SECONDS` for large payloads. +- **Version pins**: For LangGraph, stay within `>=0.5,<0.7` range. + +--- + +## 10) Merged API Surface + +### 10.1 `uipath` (Python SDK) + +#### Processes + +- **`sdk.processes.invoke(name, input_arguments=None, folder_key=None, folder_path=None) -> Job`** + Start a process by **Release name**. Returns a `Job` with attributes like `.key`, `.state`, `.start_time`, `.end_time`, `.output_arguments` (string or JSON), and `.faulted_reason` when applicable. + _Common errors_: `httpx.HTTPStatusError 404 /Releases` (bad name or wrong folder), `403 Forbidden` (insufficient RBAC). + _Example:_ + + ```python + job = sdk.processes.invoke("ACME_Invoice_Load", {"batch_id": "B-42"}, folder_path="Finance") + while job.state in ("Pending", "Running"): + await asyncio.sleep(3) + job = sdk.jobs.retrieve(job.key, folder_path="Finance") + if job.state == "Successful": + print("Output:", job.output_arguments) + else: + print("Failed:", job.faulted_reason) + ``` + +- **`sdk.processes.invoke_async(...) -> Job`** – Fire‑and‑forget; same return as `invoke` but do not block. + +#### Jobs + +- **`sdk.jobs.retrieve(job_key, folder_key=None, folder_path=None) -> Job`** – Refresh job state and metadata. +- **`sdk.jobs.resume(inbox_id, job_id, folder_key=None, folder_path=None, payload=None) -> None`** – Resume a suspended job (HITL continuation). +- **`sdk.jobs.extract_output(job) -> Optional[str]`** – Convenience helper to get the output string. +- **Attachments API** + - `sdk.jobs.list_attachments(job_key, folder_key=None, folder_path=None) -> list[str]` + - `sdk.jobs.create_attachment(name, content=None, source_path=None, job_key=None, category=None, folder_key=None, folder_path=None) -> uuid.UUID` + - `sdk.jobs.link_attachment(attachment_key, job_key, category=None, folder_key=None, folder_path=None) -> None` + _Example:_ + ```python + key = sdk.jobs.create_attachment("summary.csv", content="id,val\n1,9") + sdk.jobs.link_attachment(key, job.key, category="Output") + for att in sdk.jobs.list_attachments(job.key): + print("Attachment:", att) + ``` + +#### Assets + +- **`sdk.assets.retrieve(name, folder_key=None, folder_path=None) -> Asset | UserAsset`** – Read scalar/JSON assets; access `.value`. +- **`sdk.assets.retrieve_credential(name, folder_key=None, folder_path=None)`** – Robot‑only; provides `.username` and `.password`. +- **`sdk.assets.update(robot_asset, folder_key=None, folder_path=None) -> Response`** – Update value (admin required). + _Tips_: Keep secrets in **Credential** assets. For per‑environment config, store by folder and pass `folder_path` explicitly. + +#### Queues + +- **`sdk.queues.create_item(name, specific_content: dict, priority='Normal', reference=None, due_date=None, ... ) -> Response`** – Enqueue work. Consider setting a **`reference`** to ensure idempotency. +- **`sdk.queues.create_transaction_item(name, no_robot: bool = False) -> TransactionItem | None`** – Claim a transaction for processing. Returned item has `.id`, `.specific_content`, `.reference`. +- **`sdk.queues.update_progress_of_transaction_item(transaction_key, progress: str) -> Response`** – Heartbeat/progress note. +- **`sdk.queues.complete_transaction_item(transaction_key, result: dict) -> Response`** – Mark result and, on failure, include `processing_exception`. +- **`sdk.queues.list_items(status=None, reference=None, top=100, ... ) -> Response`** – Filter queue contents. + _Example:_ + ```python + trx = sdk.queues.create_transaction_item("InvoiceProcessing") + if trx: + try: + # process trx.specific_content... + sdk.queues.complete_transaction_item(trx.id, {"status": "Successful", "message": "OK"}) + except Exception as e: + sdk.queues.complete_transaction_item(trx.id, { + "status": "Failed", "is_successful": False, "processing_exception": str(e) + }) + ``` + +#### Buckets (Storage) + +- **`sdk.buckets.upload(name, blob_file_path, source_path=None, content=None, content_type=None, folder_key=None, folder_path=None) -> None`** – Upload from disk or in‑memory `content`. +- **`sdk.buckets.download(name, blob_file_path, destination_path, folder_key=None, folder_path=None) -> None`** – Save a blob to local path. +- **`sdk.buckets.retrieve(name, key=None, folder_key=None, folder_path=None) -> Bucket`** – Inspect bucket metadata. + _Tip_: Use MIME `content_type` for correct downstream handling (e.g., `application/pdf`, `text/csv`). + +#### Actions (Action Center) + +- **`sdk.actions.create(title=None, data=None, app_name=None, app_key=None, app_folder_path=None, app_folder_key=None, app_version=None, assignee=None) -> Action`** +- **`sdk.actions.retrieve(action_key, app_folder_path=None, app_folder_key=None) -> Action`** + _Async variants available (`create_async`)._ + _Pattern_: Create → return `WaitAction` from your `main()` → human completes → automation resumes via `jobs.resume` with payload. + +#### Context Grounding (RAG) + +- **`sdk.context_grounding.search(name, query, number_of_results=5, folder_key=None, folder_path=None) -> list[ContextGroundingQueryResponse]`** – Retrieve top‑k chunks (`.content`, `.source`). +- **`sdk.context_grounding.add_to_index(name, blob_file_path=None, content_type=None, content=None, source_path=None, ingest_data=True, folder_key=None, folder_path=None) -> None`** – Add docs. +- **`sdk.context_grounding.retrieve(name, folder_key=None, folder_path=None) -> ContextGroundingIndex`** – Inspect index. +- **`sdk.context_grounding.ingest_data(index, folder_key=None, folder_path=None) -> None`**, **`delete_index(index, ...)`** – Bulk ops. + _Example:_ + ```python + hits = sdk.context_grounding.search("Internal_Wiki", "vacation policy", 3) + ctx = "\n".join(h.content for h in hits) + answer = await sdk.llm.chat_completions(model="gpt-4o-mini-2024-07-18", + messages=[{"role":"user","content": f"Use this context:\n{ctx}\n\nQ: What is our policy?"}]) + ``` + +#### Connections (Integration Service) + +- **`sdk.connections.retrieve(key) -> Connection`** – Connection metadata. +- **`sdk.connections.retrieve_token(key) -> ConnectionToken`** – OAuth token passthrough. +- **`sdk.connections.retrieve_event_payload(event_args) -> dict`** – Get full trigger payload for event‑driven agents. + +#### Attachments (generic) + +- **`sdk.attachments.upload(name, content=None, source_path=None, folder_key=None, folder_path=None) -> uuid.UUID`** +- **`sdk.attachments.download(key, destination_path, folder_key=None, folder_path=None) -> str`** +- **`sdk.attachments.delete(key, folder_key=None, folder_path=None) -> None`** + +#### Folders + +- **`sdk.folders.retrieve_key(folder_path) -> str | None`** – Resolve a path to folder key for scoping. + +#### LLM Gateway + +- **Normalized path**: + - `sdk.llm.chat_completions(model, messages, max_tokens=None, temperature=None, tools=None, tool_choice=None, ...) -> ChatCompletion` +- **OpenAI‑compatible path**: + - `sdk.llm_openai.chat.completions.create(model, messages, max_tokens=None, temperature=None, ...) -> ChatCompletion` + - `sdk.llm_openai.embeddings.create(input, embedding_model, openai_api_version=None) -> Embeddings` + _Tip_: Prefer **normalized** for UiPath‑first features; use **OpenAI‑compatible** to reuse LC/third‑party clients unchanged. + +#### Low‑level HTTP + +- **`sdk.api_client.request(method, url, scoped=True, infer_content_type=True, **kwargs) -> Response`\*\* +- **`sdk.api_client.request_async(...) -> Response`** + _Use cases_: custom endpoints, preview APIs, or troubleshooting raw requests. + +--- + +### 10.2 `uipath-langchain` (LangGraph SDK) + +#### Chat Models + +- **`uipath_langchain.chat.UiPathChat`** (normalized) & **`UiPathAzureChatOpenAI`** (Azure passthrough) + **Init (common):** `model='gpt-4o-2024-08-06'`, `temperature`, `max_tokens`, `top_p`, `n`, `streaming=False`, `max_retries=2`, `request_timeout=None`, `callbacks=None`, `verbose=False` + **Messages:** `langchain_core.messages` (`SystemMessage`, `HumanMessage`, `AIMessage`) or plain string. + **Methods:** + + - `invoke(messages | str) -> AIMessage` (sync) + - `ainvoke(messages | str) -> AIMessage` (async) + - `astream(messages)` → async generator of chunks (for streaming UIs) + - `with_structured_output(pydantic_model)` → parsed/validated output object + _Examples:_ + + ```python + chat = UiPathChat(model="gpt-4o-2024-08-06") + print(chat.invoke("Say hi").content) + + class Answer(BaseModel): + text: str + score: float + + tool_chat = chat.with_structured_output(Answer) + parsed = tool_chat.invoke("Return a JSON with text and score") + ``` + +#### Embeddings + +- **`uipath_langchain.embeddings.UiPathAzureOpenAIEmbeddings`** + - `embed_documents(list[str]) -> list[list[float]]` + - `embed_query(str) -> list[float]` + _Params:_ `model='text-embedding-3-large'`, `dimensions=None`, `chunk_size=1000`, `max_retries=2`, `request_timeout=None`. + +#### Vector Store (Context Grounding) + +- **`uipath_langchain.vectorstores.ContextGroundingVectorStore(index_name, folder_path=None, uipath_sdk=None)`** + - `similarity_search(query, k=4) -> list[Document]` + - `similarity_search_with_score(query, k=4) -> list[tuple[Document, float]]` + - `similarity_search_with_relevance_scores(query, k=4, score_threshold=None) -> list[tuple[Document, float]]` + - `.as_retriever(search_kwargs={'k': 3}) -> BaseRetriever` + _Document fields_: `.page_content`, `.metadata` (source, uri, created_at). + +#### Retriever + +- **`uipath_langchain.retrievers.ContextGroundingRetriever(index_name, folder_path=None, folder_key=None, uipath_sdk=None, number_of_results=10)`** + - `invoke(query) -> list[Document]` (sync/async) + _Tip_: Use retriever in LC chains/graphs for clean separation of concerns. + +#### Tracing / Observability + +- **`uipath_langchain.tracers.AsyncUiPathTracer(action_name=None, action_id=None, context=None)`** + Add to `callbacks=[tracer]` on any LC runnable to capture spans/metadata into UiPath. + +#### Agent Building with LangGraph + +- **`langgraph.prebuilt.create_react_agent(llm, tools, prompt=None, **kwargs) -> Runnable`\*\* – Get a practical ReAct agent quickly. +- **`langgraph.graph.StateGraph`** + - `add_node(name, fn)` – Node is callable (sync/async) receiving/returning your `State` model. + - `add_edge(src, dst)` – Connect nodes (`START` and `END` available). + - `compile() -> Graph` – Freeze DAG for execution. + _Pattern:_ use nodes for **tool‑use**, **HITL interrupt**, **routing**, and **post‑processing**. + +--- + +## 11) Troubleshooting + +**Classic** + +- `SecretMissingError` / `BaseUrlMissingError` → run `uipath auth`, verify env. +- 404 for Releases/Assets → check object names and folder context. +- 403 Forbidden → token scopes; re‑authenticate or create proper OAuth app. +- Timeouts → network/proxy; verify `UIPATH_URL`. + +**LangGraph** + +- `ModuleNotFoundError: uipath_langchain` → `pip install uipath-langchain`. +- 401 Unauthorized → check `UIPATH_ACCESS_TOKEN` or OAuth pair. +- Version mismatch → ensure `langgraph>=0.5,<0.7`. + +--- + +## 12) Glossary + +- **Action Center (HITL)**: Human‑in‑the‑loop task approvals. +- **Assets**: Centralized config/secret storage. +- **Buckets**: Cloud file storage. +- **Context Grounding**: UiPath semantic indexing for RAG. +- **LLM Gateway**: UiPath’s model broker (normalized and OpenAI‑compatible). +- **Queues**: Transactional work management. + +--- + +## 13) Checklists + +**Before first run** + +- [ ] `uipath auth` +- [ ] Populate `.env` and copy to teammates as `.env.example` template +- [ ] `uipath init` after defining `main()` and/or graphs + +**Pre‑publish** + +- [ ] Unit + E2E passing +- [ ] `uipath pack` clean +- [ ] Secrets in Assets / Connections, not in code + +**Production readiness** + +- [ ] Folder scoping & RBAC verified +- [ ] Tracing/Telemetry enabled +- [ ] Runbooks for failures, retries, backoff + +--- + +## 14) Links + +- Project README: `./README.md` +- UiPath Python SDK docs & samples: https://uipath.github.io/uipath-python/ +- UiPath LangGraph SDK docs & samples: https://uipath.github.io/uipath-python/langchain/quick_start/ diff --git a/testcases/common/validate_output.sh b/testcases/common/validate_output.sh new file mode 100644 index 0000000..71ff190 --- /dev/null +++ b/testcases/common/validate_output.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Common utility to print UiPath output file +# Usage: source /app/testcases/common/validate_output.sh + +debug_print_uipath_output() { + echo "Printing output file..." + if [ -f "__uipath/output.json" ]; then + echo "=== OUTPUT FILE CONTENT ===" + cat __uipath/output.json + echo "=== END OUTPUT FILE CONTENT ===" + else + echo "ERROR: __uipath/output.json not found!" + echo "Checking directory contents:" + ls -la + if [ -d "__uipath" ]; then + echo "Contents of __uipath directory:" + ls -la __uipath/ + else + echo "__uipath directory does not exist!" + fi + fi +} + +validate_output() { + echo "Printing output file for validation..." + debug_print_uipath_output + + echo "Validating output..." + python src/assert.py || { echo "Validation failed!"; exit 1; } + + echo "Testcase completed successfully." +} + +validate_output diff --git a/testcases/quickstart-agent/Dockerfile b/testcases/quickstart-agent/Dockerfile deleted file mode 100644 index 75bc50d..0000000 --- a/testcases/quickstart-agent/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -FROM ghcr.io/astral-sh/uv:python3.12-bookworm - -WORKDIR /app - -COPY . . - -WORKDIR /app/testcases/quickstart-agent - -RUN uv sync - -ARG CLIENT_ID -ARG CLIENT_SECRET -ARG BASE_URL - -RUN if [ -z "$CLIENT_ID" ]; then echo "CLIENT_ID build arg is required" && exit 1; fi -RUN if [ -z "$CLIENT_SECRET" ]; then echo "CLIENT_SECRET build arg is required" && exit 1; fi -RUN if [ -z "$BASE_URL" ]; then echo "BASE_URL build arg is required" && exit 1; fi - -# Set environment variables for runtime -ENV CLIENT_ID=$CLIENT_ID -ENV CLIENT_SECRET=$CLIENT_SECRET -ENV BASE_URL=$BASE_URL -ENV TAVILY_API_KEY=${TAVILY_API_KEY:-""} -ENV UIPATH_TENANT_ID=${UIPATH_TENANT_ID:-""} -ENV UIPATH_JOB_KEY=8c6a342e-036e-492c-a3d2-99e66f6554ce - -# Authenticate with UiPath during build -RUN uv run uipath auth --client-id="$CLIENT_ID" --client-secret="$CLIENT_SECRET" --base-url="$BASE_URL" - -RUN uv run uipath pack - -RUN AGENT_INPUT=$(cat input.json) && uv run uipath run agent "$AGENT_INPUT" - -# Run the Python assert script to validate output -RUN python src/assert.py \ No newline at end of file diff --git a/testcases/quickstart-agent/run.sh b/testcases/quickstart-agent/run.sh new file mode 100644 index 0000000..b6a90d7 --- /dev/null +++ b/testcases/quickstart-agent/run.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +echo "Syncing dependencies..." +uv sync + +echo "Authenticating with UiPath..." +uv run uipath auth --client-id="$CLIENT_ID" --client-secret="$CLIENT_SECRET" --base-url="$BASE_URL" + +echo "Initializing the project..." +uv run uipath init + +echo "Packing agent..." +uv run uipath pack + +echo "Running agent..." +echo "Input from input.json file" +uv run uipath run agent --file input.json + diff --git a/testcases/quickstart-agent/uv.lock b/testcases/quickstart-agent/uv.lock index 1783eb6..10518c7 100644 --- a/testcases/quickstart-agent/uv.lock +++ b/testcases/quickstart-agent/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.12'", @@ -897,6 +897,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" }, ] +[[package]] +name = "linkify-it-py" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" }, +] + [[package]] name = "llama-cloud" version = "0.1.32" @@ -1225,6 +1237,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] +plugins = [ + { name = "mdit-py-plugins" }, +] + [[package]] name = "markupsafe" version = "3.0.2" @@ -1295,6 +1315,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878, upload-time = "2025-02-03T15:32:22.295Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -2361,6 +2393,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/94/05d0310bfa92c26aa50a9d2dea2c6448a1febfdfcf98fb340a99d48a3078/pypdf-5.8.0-py3-none-any.whl", hash = "sha256:bfe861285cd2f79cceecefde2d46901e4ee992a9f4b42c56548c4a6e9236a0d1", size = 309718, upload-time = "2025-07-13T12:51:33.159Z" }, ] +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2650,6 +2691,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] +[[package]] +name = "textual" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify", "plugins"] }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/30/38b615f7d4b16f6fdd73e4dcd8913e2d880bbb655e68a076e3d91181a7ee/textual-6.2.1.tar.gz", hash = "sha256:4699d8dfae43503b9c417bd2a6fb0da1c89e323fe91c4baa012f9298acaa83e1", size = 1570645, upload-time = "2025-10-01T16:11:24.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/93/02c7adec57a594af28388d85da9972703a4af94ae1399542555cd9581952/textual-6.2.1-py3-none-any.whl", hash = "sha256:3c7190633cd4d8bfe6049ae66808b98da91ded2edb85cef54e82bf77b03d2a54", size = 710702, upload-time = "2025-10-01T16:11:22.161Z" }, +] + [[package]] name = "tiktoken" version = "0.9.0" @@ -2789,31 +2846,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, ] +[[package]] +name = "uc-micro-py" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" }, +] + [[package]] name = "uipath" -version = "2.0.82" +version = "2.1.72" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-monitor-opentelemetry" }, { name = "click" }, { name = "httpx" }, + { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, { name = "pathlib" }, { name = "pydantic" }, + { name = "pyperclip" }, { name = "python-dotenv" }, { name = "rich" }, { name = "tenacity" }, + { name = "textual" }, { name = "tomli" }, { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/82/e6bb5ceabc46b4d2e82b8f228148260229e5bd8c4e7216b1e94219757f97/uipath-2.0.82.tar.gz", hash = "sha256:9d5acf2240bd9679089b8e6859fe86207ca2a81a0e9dc7c98b6068c637a43250", size = 1991272, upload-time = "2025-07-17T13:31:53.205Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/9e/21d3221949f37f6d50effe245bff56f1a46c316e4abb50f45f23048e194a/uipath-2.1.72.tar.gz", hash = "sha256:b3a6be552e7e7d386bf1254ccadf35cb7f93128c942ab4a35b2c78c562b7862b", size = 2044661, upload-time = "2025-10-03T15:36:12.361Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/89/ba3a23aa29a8d93386bbd85cec45bcea1a596eedb630cc676eca10994804/uipath-2.0.82-py3-none-any.whl", hash = "sha256:99404519b5ea9c0f607188c7b4abcb85fcf3c6559cbdca72330c2c2e1c1cf8e5", size = 136346, upload-time = "2025-07-17T13:31:51.291Z" }, + { url = "https://files.pythonhosted.org/packages/25/8f/4b18d44f21cba03e7f10100b7c734a2445142aa8d6e6a5b37e7704b478c5/uipath-2.1.72-py3-none-any.whl", hash = "sha256:a9d390dda11456d3dd87906b0c6bcbb89c870cc977fbd2d8ac0330997e012da6", size = 252558, upload-time = "2025-10-03T15:36:10.633Z" }, ] [[package]] name = "uipath-llamaindex" -version = "0.0.30" +version = "0.0.36" source = { editable = "../../" } dependencies = [ { name = "llama-index" }, @@ -2829,7 +2898,7 @@ requires-dist = [ { name = "llama-index-embeddings-azure-openai", specifier = ">=0.3.8" }, { name = "llama-index-llms-azure-openai", specifier = ">=0.3.2" }, { name = "openinference-instrumentation-llama-index", specifier = ">=4.3.0" }, - { name = "uipath", specifier = ">=2.0.79,<2.1.0" }, + { name = "uipath", specifier = ">=2.1.54,<2.2.0" }, ] [package.metadata.requires-dev] diff --git a/testcases/simple-hitl-agent/Dockerfile b/testcases/simple-hitl-agent/Dockerfile deleted file mode 100644 index 76974b4..0000000 --- a/testcases/simple-hitl-agent/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -FROM ghcr.io/astral-sh/uv:python3.12-bookworm - -WORKDIR /app - -COPY . . - -WORKDIR /app/testcases/simple-hitl-agent - -RUN uv sync - -ARG CLIENT_ID -ARG CLIENT_SECRET -ARG BASE_URL - -RUN if [ -z "$CLIENT_ID" ]; then echo "CLIENT_ID build arg is required" && exit 1; fi -RUN if [ -z "$CLIENT_SECRET" ]; then echo "CLIENT_SECRET build arg is required" && exit 1; fi -RUN if [ -z "$BASE_URL" ]; then echo "BASE_URL build arg is required" && exit 1; fi - -# Set environment variables for runtime -ENV CLIENT_ID=$CLIENT_ID -ENV CLIENT_SECRET=$CLIENT_SECRET -ENV BASE_URL=$BASE_URL -ENV TAVILY_API_KEY=${TAVILY_API_KEY:-""} -ENV UIPATH_TENANT_ID=${UIPATH_TENANT_ID:-""} -ENV UIPATH_JOB_KEY=8c6a342e-036e-492c-a3d2-99e66f6554ce - -# Authenticate with UiPath during build -RUN uv run uipath auth --client-id="$CLIENT_ID" --client-secret="$CLIENT_SECRET" --base-url="$BASE_URL" - -RUN uv run uipath pack - -# Run the agent with input from input.json -RUN AGENT_INPUT=$(cat input.json) && uv run uipath run agent "$AGENT_INPUT" -RUN HUMAN_RESPONSE=$(cat human_response.json) && uv run uipath run agent "$HUMAN_RESPONSE" --resume - -# Run the Python assert script to validate output -RUN python src/assert.py \ No newline at end of file diff --git a/testcases/simple-hitl-agent/run.sh b/testcases/simple-hitl-agent/run.sh new file mode 100644 index 0000000..e5d480d --- /dev/null +++ b/testcases/simple-hitl-agent/run.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +echo "Syncing dependencies..." +uv sync + +echo "Authenticating with UiPath..." +uv run uipath auth --client-id="$CLIENT_ID" --client-secret="$CLIENT_SECRET" --base-url="$BASE_URL" + +echo "Initializing the project..." +uv run uipath init + +echo "Packing agent..." +uv run uipath pack + +echo "Running agent..." +echo "Input from input.json file" +uv run uipath run agent --file input.json + +echo "Resuming agent run with human response..." +uv run uipath run agent --file human_response.json --resume +