Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6e11d22
feat: add execution stage for new guardrails (#683)
dianapirvulescu Mar 11, 2026
fec7624
fix: analyze files tool when mime type is missing (#684)
cristian-groza Mar 11, 2026
2947c1e
fix: durable interrupt tools for cas (#677)
JoshParkSJ Mar 11, 2026
fc2a17e
fix: use centralized header merging in httpx clients (#685)
cotovanu-cristian Mar 13, 2026
848f660
fix: coerce stringified dict/list fields in analyze files tool input …
cotovanu-cristian Mar 13, 2026
c85613e
fix: preserve AIMessage id in replace_tool_calls (#688)
valentinabojan Mar 13, 2026
84aa589
fix: derive mime type from referenced file name [JAR-9397] (#689)
JoshParkSJ Mar 13, 2026
29ba5eb
feat: migrate context tool to unified-search; use wrappers in context…
gcuip Mar 13, 2026
ad49d48
chore: avoid imds timeout in bedrock client init (#690)
cosmin-staicu Mar 13, 2026
8fecff6
chore: use botocore.UNSIGNED to prevent IMDS timeout in bedrock (#692)
cosmin-staicu Mar 14, 2026
899a056
chore: pass bedrock_client with UNSIGNED to prevent IMDS timeout from…
cosmin-staicu Mar 14, 2026
6a078d9
feat: add User Prompt Attacks guardrail mapping (#697)
valentinabojan Mar 17, 2026
544977f
feat: Interrupt for low coded CAS agents [JAR-9208] (#641)
JoshParkSJ Mar 17, 2026
f8b1b58
fix: typo in OpenAiResponses name (#698)
ionut-mihalache-uipath Mar 17, 2026
b11448a
fix: escape apostrophes for parsing and rendering [JAR-9386] (#704)
JoshParkSJ Mar 18, 2026
7d80927
chore: init claude.md (#694)
cosmin-staicu Mar 18, 2026
8c82ec2
feat: add integration test for files (#702)
ionut-mihalache-uipath Mar 18, 2026
db97414
chore: assert .uiproj on init-flow testcase (#695)
radu-mocanu Mar 18, 2026
55aec54
feat: add example for process tool http error handling (#707)
andreitava-uip Mar 19, 2026
ab5b440
fix: inject X-UiPath-License-RefId in transport layer [AE-1109] (#709)
RunnanJia Mar 19, 2026
694cad2
chore: bump version to 0.8.29 (#711)
RunnanJia Mar 19, 2026
453b92d
chore: upgrade uipath and uipath-platform versions (#710)
dianagrecu-uipath Mar 20, 2026
e2ead0f
chore: add app binding for ticket classification sample (#717)
radu-mocanu Mar 22, 2026
74c157b
fix: improve 4xx reporting on context and is tools (#714)
andreitava-uip Mar 23, 2026
437fac8
Fix: fix bedrock payload handler for parallel and strict tool use, ad…
cosminacho Mar 23, 2026
f02cacc
chore: bump uipath-platform - 0.1.5 (#720)
gcuip Mar 23, 2026
98460a9
use name & field names instead of display name
milind-jain-uipath Mar 24, 2026
9396ae2
Merge branch 'main' of https://github.com/UiPath/uipath-langchain-pyt…
milind-jain-uipath Mar 24, 2026
e7a7c33
fix: surface actionable error for dangling $ref in tool schemas (#716)
cotovanu-cristian Mar 24, 2026
55a49b2
removed count(*) examples
milind-jain-uipath Mar 24, 2026
77823d2
Merge branch 'main' into feat-df-agent-integrations-fixes
milind-jain-uipath Mar 24, 2026
fff4b2e
removed count(*) examples + prompt change
milind-jain-uipath Mar 24, 2026
9dc5d28
Merge branch 'feat-df-agent-integrations-fixes' of https://github.com…
milind-jain-uipath Mar 24, 2026
46ef483
fix: pass sys ssl certificates to bedrock client (#718)
radu-mocanu Mar 24, 2026
11ec113
chore: bump uipath 2.10.29 (#723)
gcuip Mar 24, 2026
8ff4412
Fix/fix anthropic payload handler (#724)
cosminacho Mar 24, 2026
858de17
Merge branch 'main' into feat-df-agent-integrations-fixes
milind-jain-uipath Mar 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 1 addition & 28 deletions .github/workflows/commitlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,5 @@ jobs:
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 22

- name: Install Git
run: |
if ! command -v git &> /dev/null; then
echo "Git is not installed. Installing..."
sudo apt-get update
sudo apt-get install -y git
else
echo "Git is already installed."
fi

- name: Install commitlint
run: |
npm install conventional-changelog-conventionalcommits
npm install commitlint@latest
npm install @commitlint/config-conventional

- name: Configure
run: |
echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js

- name: Validate PR commits with commitlint
run: |
git fetch origin pull/${{ github.event.pull_request.number }}/head:pr_branch
npx commitlint --from ${{ github.event.pull_request.base.sha }} --to pr_branch --verbose
uses: wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed # v6
82 changes: 82 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

`uipath-langchain` is a Python SDK that extends UiPath's Python SDK with LangChain/LangGraph integration. It implements the UiPath Runtime Protocol to deploy LangGraph agents to UiPath Cloud Platform. Requires Python 3.11+.

## Common Commands

```bash
# Install dependencies (uses uv)
uv sync --all-extras

# Run all tests
uv run pytest

# Run a single test file
uv run pytest tests/path/to/test_file.py

# Run a single test
uv run pytest tests/path/to/test_file.py::test_name

# Lint
just lint # ruff check + httpx client lint
just format # ruff format check + fix

# Build
uv build
```

## Architecture

### Package: `src/uipath_langchain/`

- **`runtime/`** — `UiPathLangGraphRuntime` executes LangGraph graphs within the UiPath framework. Async execution with streaming, breakpoints, and message mapping. Registered as an entry point via `uipath_langchain.runtime:register_runtime_factory`.

- **`agent/`** — Agent implementation with sub-packages:
- `react/` — ReAct agent pattern (agent, LLM node, router, tool node, guardrails)
- `tools/` — Structured tools: context, escalation, extraction, integration, process, MCP adapters, durable interrupts. All inherit from `BaseUiPathStructuredTool`.
- `guardrails/` — Input/output validation within agent execution
- `multimodal/` — Multimodal invoke support
- `exceptions/` — Structured error types (`AgentRuntimeError`, `AgentStartupError`) and helpers for agent error handling
- `wrappers/` — Agent decorators and wrappers

- **`chat/`** — LLM provider interfaces for OpenAI, Azure OpenAI, AWS Bedrock, Google Vertex AI. Uses **lazy imports** via `__getattr__` in `__init__.py` to keep CLI startup fast. Includes `hitl.py` with the `requires_approval` decorator for human-in-the-loop workflows. Factory pattern via `chat_model_factory.py`.

- **`retrievers/`** and **`vectorstores/`** — Context grounding retrieval and vector storage.

- **`guardrails/`** — Top-level guardrails with actions, enums, models, and middleware.

- **`_cli/`** — CLI commands (`uipath init`, `uipath new`) with project templates.

- **`_tracing/`** — OpenTelemetry instrumentation.

- **`_utils/`** — Shared utilities: HTTP request mixin, settings, sleep policy, environment helpers.

- **`middlewares.py`** — Entry point registered as `uipath_langchain.middlewares:register_middleware`.

### Entry Points (pyproject.toml)

The package registers two entry points consumed by the `uipath` CLI:
- `uipath.middleware` → `uipath_langchain.middlewares:register_middleware`
- `uipath.runtime_factory` → `uipath_langchain.runtime:register_runtime_factory`

## Key Conventions

- **httpx clients**: Always use `**get_httpx_client_kwargs()` when constructing `httpx.Client()` or `httpx.AsyncClient()`. A custom AST linter (`scripts/lint_httpx_client.py`) enforces this — it runs as part of `just lint`.

- **Lazy imports**: The `chat/` module defers heavy imports (langchain_openai, openai SDK) to optimize CLI startup. Use `__getattr__` pattern in `__init__.py` when adding new chat model providers.

- **Naming conventions for SDK methods**: `retrieve` (single by key), `retrieve_by_[field]` (single by other field), `list` (multiple resources).

- **Testing**: pytest only (no unittest). Tests in `./tests/` mirror source structure. Use pytest-asyncio for async tests (mode: auto). A circular import test (`test_no_circular_imports.py`) auto-discovers and validates all modules.

- **Type annotations**: All functions and classes require type annotations. Public APIs require Google-style docstrings.

- **Linting**: Ruff with rules E, F, B, I. Line length 88. mypy with pydantic plugin for type checking.

- **Bedrock/Vertex imports**: `bedrock.py` and `vertex.py` have per-file E402 ignores for conditional imports.

- **Exception handling in `agent/`**: Use the error types and helpers from `agent/exceptions/`. Do not raise raw exceptions or invent new error types. New error codes may be defined.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 3 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[project]
name = "uipath-langchain"
version = "0.8.14"
version = "0.9.6"
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
dependencies = [
"uipath>=2.10.0, <2.11.0",
"uipath>=2.10.29, <2.11.0",
"uipath-core>=0.5.2, <0.6.0",
"uipath-platform>=0.0.18, <0.1.0",
"uipath-platform>=0.1.7, <0.2.0",
"uipath-runtime>=0.9.1, <0.10.0",
"langgraph>=1.0.0, <2.0.0",
"langchain-core>=1.2.11, <2.0.0",
Expand Down Expand Up @@ -69,7 +69,6 @@ dev = [
"numpy>=1.24.0",
"pytest_httpx>=0.35.0",
"rust-just>=1.39.0",
"uipath-langchain",
]

[tool.hatch.build.targets.wheel]
Expand Down Expand Up @@ -119,6 +118,3 @@ name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

[tool.uv.sources]
uipath-langchain = { workspace = true }
20 changes: 20 additions & 0 deletions samples/ticket-classification/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,26 @@ cd uipath-langchain-python/samples/ticket-classification

The Ticket Classification Agent utilizes HITL (Human In The Loop) technology, allowing the system to incorporate feedback directly from supervisory personnel. We'll leverage UiPath [Action Center](https://docs.uipath.com/action-center/automation-suite/2023.4/user-guide/introduction) for this functionality.

There are two ways to configure the escalation app:

#### Option A: Use App Bindings (recommended — no code changes required)

After publishing the agent package, you can override the escalation app directly from the **Package Requirements** tab in UiPath, without modifying any code. This is made possible by the `bindings.json` file included in this sample.

![app-binding-package-requirements](../../docs/sample_images/ticket-classification/app-binding-package-requirements.png)

In the **Package Requirements** tab, select the app you want to use as the escalation app. The app you bind **must respect the following contract** expected by the agent code:

- **Inputs:**
- `AgentOutput` (string) — the classification summary sent to the reviewer
- `AgentName` (string) — the name of the agent creating the task
- **Output:**
- `Answer` (boolean) — `true` to approve, `false` to reject

> This approach allows you to use any compatible UiPath App as the escalation interface, swapping it out without redeploying or editing code.

#### Option B: Deploy the pre-built solution app

Follow these steps to deploy the pre-built application using [UiPath Solutions Management](https://docs.uipath.com/solutions-management/automation-cloud/latest/user-guide/solutions-management-overview):

1. **Upload Solution Package**
Expand Down
26 changes: 26 additions & 0 deletions samples/ticket-classification/bindings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"version": "2.0",
"resources": [
{
"resource": "app",
"key": "escalation_agent_app.app_folder_path",
"value": {
"name": {
"defaultValue": "escalation_agent_app",
"isExpression": false,
"displayName": "App Name"
},
"folderPath": {
"defaultValue": "app_folder_path",
"isExpression": false,
"displayName": "App Folder Path"
}
},
"metadata": {
"ActivityName": "create_async",
"BindingsVersion": "2.2",
"DisplayLabel": "app_name"
}
}
]
}
2 changes: 1 addition & 1 deletion samples/ticket-classification/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ async def wait_for_human(state: GraphState) -> Command:
"AgentName": "ticket-classification "},
app_version=1,
assignee=state.get("assignee", None),
app_folder_path="FOLDER_PATH_PLACEHOLDER",
app_folder_path="app_folder_path",
))

return Command(
Expand Down
2 changes: 1 addition & 1 deletion src/uipath_langchain/_cli/cli_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def generate_pyproject(target_directory, project_name):
description = "{project_name}"
authors = [{{ name = "John Doe", email = "john.doe@myemail.com" }}]
dependencies = [
"uipath-langchain>=0.2.0",
"uipath-langchain>=0.8.0, <0.9.0",
]
requires-python = ">=3.11"
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Durable interrupt package for side-effect-safe interrupt/resume in LangGraph."""

from .decorator import _durable_state, durable_interrupt
from .decorator import (
_durable_state,
durable_interrupt,
)
from .skip_interrupt import SkipInterruptValue

__all__ = [
Expand Down
2 changes: 2 additions & 0 deletions src/uipath_langchain/agent/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
AgentStartupError,
AgentStartupErrorCode,
)
from .helpers import raise_for_enriched

__all__ = [
"AgentStartupError",
"AgentRuntimeError",
"AgentStartupErrorCode",
"AgentRuntimeErrorCode",
"raise_for_enriched",
]
66 changes: 66 additions & 0 deletions src/uipath_langchain/agent/exceptions/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Helpers for raising structured errors from HTTP exceptions."""

from collections import defaultdict

from uipath.platform.errors import EnrichedException
from uipath.runtime.errors import UiPathErrorCategory

from .exceptions import AgentRuntimeError, AgentRuntimeErrorCode


def raise_for_enriched(
e: EnrichedException,
known_errors: dict[tuple[int, str | None], tuple[str, UiPathErrorCategory]],
*,
title: str,
**context: str,
) -> None:
"""Raise AgentRuntimeError if the exception matches a known error pattern.

Matches on ``(status_code, error_code)`` pairs. Use ``None`` as error_code
to match any error with that status code. More specific matches (with
error_code) are tried first.

Each value is a ``(template, category)`` pair. Message templates can use
``{keyword}`` placeholders filled from *context*, plus ``{message}`` for
the server's own error message.

Does nothing if no match is found — caller should re-raise the original.

Example::

try:
await client.processes.invoke_async(name=name, folder_path=folder)
except EnrichedException as e:
raise_for_enriched(
e,
{
(404, "1002"): ("Process not found.", UiPathErrorCategory.DEPLOYMENT),
(409, None): ("Conflict: {message}", UiPathErrorCategory.DEPLOYMENT),
},
title=f"Failed to execute tool '{tool_name}'",
)
raise
"""
info = e.error_info
error_code = info.error_code if info else None
server_message = (info.message if info else None) or ""
context["message"] = server_message

# Try specific match first, then wildcard
entry = known_errors.get((e.status_code, error_code))
if entry is None:
entry = known_errors.get((e.status_code, None))
if entry is None:
return

template, category = entry
detail = template.format_map(defaultdict(lambda: "<unknown>", context))
raise AgentRuntimeError(
code=AgentRuntimeErrorCode.HTTP_ERROR,
title=title,
detail=detail,
category=category,
status=e.status_code,
should_wrap=False,
) from e
1 change: 1 addition & 0 deletions src/uipath_langchain/agent/messages/message_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ def replace_tool_calls(message: AIMessage, tool_calls: list[ToolCall]) -> AIMess
content_blocks=content_blocks,
tool_calls=tool_calls,
response_metadata=response_metadata,
id=message.id,
)
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
_VALIDATOR_ALLOWED_STAGES = {
"prompt_injection": {ExecutionStage.PRE_EXECUTION},
"pii_detection": {ExecutionStage.PRE_EXECUTION, ExecutionStage.POST_EXECUTION},
"harmful_content": {ExecutionStage.PRE_EXECUTION, ExecutionStage.POST_EXECUTION},
"intellectual_property": {ExecutionStage.POST_EXECUTION},
"user_prompt_attacks": {ExecutionStage.PRE_EXECUTION},
}


Expand Down
58 changes: 58 additions & 0 deletions src/uipath_langchain/agent/react/json_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import ast
import json
import sys
from typing import Any, ForwardRef, Union, get_args, get_origin

Expand Down Expand Up @@ -212,3 +214,59 @@ def _json_key(field_name: str, field_info: Any) -> str:

def _is_pydantic_model(annotation: Any) -> bool:
return isinstance(annotation, type) and issubclass(annotation, BaseModel)


def _coerce_field(key: str, value: Any, schema: type[BaseModel] | None) -> Any:
"""Coerce a single field value, skipping str-typed fields when schema is available."""
if schema is None:
return coerce_json_strings(value)

field_info = schema.model_fields.get(key)
if field_info is None:
return coerce_json_strings(value)

annotation = _unwrap_optional(field_info.annotation)

if annotation is str:
return value

if _is_pydantic_model(annotation):
return coerce_json_strings(value, annotation)

if get_origin(annotation) is list:
item_args = get_args(annotation)
item_schema = None
if item_args and _is_pydantic_model(item_args[0]):
item_schema = item_args[0]
if isinstance(value, list):
return [coerce_json_strings(item, item_schema) for item in value]

return coerce_json_strings(value)


def coerce_json_strings(data: Any, schema: type[BaseModel] | None = None) -> Any:
"""Parse stringified dicts/lists back into Python objects.

LLMs sometimes serialize nested objects as strings instead of dicts,
either as JSON (double quotes) or Python repr (single quotes).
When a schema is provided, str-typed fields are left untouched.
"""
if isinstance(data, dict):
return {k: _coerce_field(k, v, schema) for k, v in data.items()}
if isinstance(data, list):
return [coerce_json_strings(item) for item in data]
if isinstance(data, str):
try:
parsed = json.loads(data)
if isinstance(parsed, (dict, list)):
return parsed
except (json.JSONDecodeError, TypeError):
pass
# LLMs sometimes emit Python repr (single quotes) instead of JSON
try:
parsed = ast.literal_eval(data)
if isinstance(parsed, (dict, list)):
return parsed
except (ValueError, SyntaxError):
pass
return data
Loading
Loading