Skip to content

Commit 118bac4

Browse files
feat: add support for guardrails decorators [AL-288] (#736)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent aa4c517 commit 118bac4

23 files changed

Lines changed: 2199 additions & 276 deletions

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.9.19"
3+
version = "0.9.20"
44
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
77
dependencies = [
88
"uipath>=2.10.29, <2.11.0",
99
"uipath-core>=0.5.2, <0.6.0",
10-
"uipath-platform>=0.1.18, <0.2.0",
10+
"uipath-platform>=0.1.20, <0.2.0",
1111
"uipath-runtime>=0.10.0, <0.11.0",
1212
"langgraph>=1.0.0, <2.0.0",
1313
"langchain-core>=1.2.11, <2.0.0",
@@ -98,6 +98,7 @@ line-ending = "auto"
9898
plugins = ["pydantic.mypy"]
9999
exclude = ["samples/.*", "testcases/.*"]
100100

101+
namespace_packages = true
101102
follow_imports = "silent"
102103
warn_redundant_casts = true
103104
warn_unused_ignores = true
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
# Joke Agent (Decorator-based Guardrails)
2+
3+
A simple LangGraph agent that generates family-friendly jokes based on a given topic using UiPath's LLM. This sample demonstrates the unified `@guardrail` decorator applied to LLM factories, tools, agent factories, graph nodes, and plain Python functions — without a middleware stack.
4+
5+
## Requirements
6+
7+
- Python 3.11+
8+
9+
## Installation
10+
11+
```bash
12+
uv venv -p 3.11 .venv
13+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
14+
uv sync
15+
```
16+
17+
## Usage
18+
19+
Run the joke agent:
20+
21+
```bash
22+
uv run uipath run agent '{"topic": "banana"}'
23+
```
24+
25+
### Input Format
26+
27+
```json
28+
{
29+
"topic": "banana"
30+
}
31+
```
32+
33+
### Output Format
34+
35+
```json
36+
{
37+
"joke": "Why did the banana go to the doctor? Because it wasn't peeling well!"
38+
}
39+
```
40+
41+
## Guardrails Overview
42+
43+
This sample uses a single unified `@guardrail` decorator with three components:
44+
45+
- **`validator`** — what to check (`CustomValidator`, `PIIValidator`, `PromptInjectionValidator`)
46+
- **`action`** — what to do on violation (`BlockAction`, `LogAction`, or a custom `GuardrailAction`)
47+
- **`stage`** — when to check (`GuardrailExecutionStage.PRE` or `POST`)
48+
49+
| Decorator | Target | Validator | Action |
50+
|---|---|---|---|
51+
| `@guardrail(validator=PromptInjectionValidator(...))` | `create_llm` factory | Prompt Injection | `BlockAction` — blocks on detection |
52+
| `@guardrail(validator=PIIValidator(...EMAIL...))` | `create_llm` factory | PII (Email) | `LogAction(WARNING)` — logs and continues |
53+
| `@guardrail(validator=CustomValidator(...))` | `analyze_joke_syntax` tool | Custom (word check) | `CustomFilterAction` — replaces "donkey" with "[censored]" |
54+
| `@guardrail(validator=CustomValidator(...))` | `analyze_joke_syntax` tool | Custom (length check) | `BlockAction` — blocks jokes > 1000 chars |
55+
| `@guardrail(validator=CustomValidator(...))` | `analyze_joke_syntax` tool | Custom (always true) | `CustomFilterAction` — always-on output transform (POST) |
56+
| `@guardrail(validator=PIIValidator(...EMAIL, PHONE...))` | `analyze_joke_syntax` tool | PII (Email, Phone) | `LogAction(WARNING)` — logs email/phone |
57+
| `@guardrail(validator=PIIValidator(...PERSON...))` | `create_joke_agent` factory | PII (Person) | `BlockAction` — blocks person names |
58+
| `@guardrail(validator=PIIValidator(...PERSON...))` | `joke_node` graph node | PII (Person) | `BlockAction` — blocks person names in node input |
59+
| `@guardrail(validator=CustomValidator(...))` | `format_joke_for_display` function | Custom (word check) | `CustomFilterAction` — replaces "donkey" in display output |
60+
61+
## The `@guardrail` Decorator
62+
63+
### LLM-level guardrails
64+
65+
Stacked decorators on a factory function. The outermost decorator runs first:
66+
67+
```python
68+
@guardrail(
69+
validator=PromptInjectionValidator(threshold=0.5),
70+
action=BlockAction(),
71+
name="LLM Prompt Injection Detection",
72+
stage=GuardrailExecutionStage.PRE,
73+
)
74+
@guardrail(
75+
validator=pii_email,
76+
action=LogAction(severity_level=LoggingSeverityLevel.WARNING),
77+
name="LLM PII Detection",
78+
stage=GuardrailExecutionStage.PRE,
79+
)
80+
def create_llm():
81+
return UiPathChat(model="gpt-4o-2024-08-06", temperature=0.7)
82+
83+
llm = create_llm()
84+
```
85+
86+
### Tool-level guardrails
87+
88+
`CustomValidator` applies local rule functions — no UiPath API call. The validator receives the tool input dict and returns `True` to signal a violation. `PIIValidator` evaluates via the UiPath guardrails API.
89+
90+
```python
91+
@guardrail(
92+
validator=CustomValidator(lambda args: "donkey" in args.get("joke", "").lower()),
93+
action=CustomFilterAction(word_to_filter="donkey", replacement="[censored]"),
94+
stage=GuardrailExecutionStage.PRE,
95+
name="Joke Content Word Filter",
96+
)
97+
@guardrail(
98+
validator=CustomValidator(lambda args: len(args.get("joke", "")) > 1000),
99+
action=BlockAction(title="Joke is too long", detail="The generated joke is too long"),
100+
stage=GuardrailExecutionStage.PRE,
101+
name="Joke Content Length Limiter",
102+
)
103+
@guardrail(
104+
validator=CustomValidator(lambda args: True),
105+
action=CustomFilterAction(word_to_filter="words", replacement="words++"),
106+
stage=GuardrailExecutionStage.POST,
107+
name="Joke Content Always Filter",
108+
)
109+
@guardrail(
110+
validator=pii_email_phone,
111+
action=LogAction(
112+
severity_level=LoggingSeverityLevel.WARNING,
113+
message="Email or phone number detected",
114+
),
115+
name="Tool PII Detection",
116+
stage=GuardrailExecutionStage.PRE,
117+
)
118+
@tool
119+
def analyze_joke_syntax(joke: str) -> str:
120+
...
121+
```
122+
123+
### Agent-level guardrail
124+
125+
```python
126+
@guardrail(
127+
validator=PIIValidator(
128+
entities=[PIIDetectionEntity(PIIDetectionEntityType.PERSON, threshold=0.5)],
129+
),
130+
action=BlockAction(
131+
title="Person name detection",
132+
detail="Person name detected and is not allowed",
133+
),
134+
name="Agent PII Detection",
135+
stage=GuardrailExecutionStage.PRE,
136+
)
137+
def create_joke_agent():
138+
return create_agent(model=llm, tools=[analyze_joke_syntax], ...)
139+
140+
agent = create_joke_agent()
141+
```
142+
143+
### Graph node guardrail
144+
145+
```python
146+
@guardrail(
147+
validator=PIIValidator(
148+
entities=[PIIDetectionEntity(PIIDetectionEntityType.PERSON, threshold=0.5)],
149+
),
150+
action=BlockAction(
151+
title="Person name detection in topic",
152+
detail="Person name detected in the node input and is not allowed",
153+
),
154+
name="Node Input PII Detection",
155+
stage=GuardrailExecutionStage.PRE,
156+
)
157+
async def joke_node(state: Input) -> Output:
158+
...
159+
```
160+
161+
### Plain Python function guardrail with `GuardrailExclude`
162+
163+
`@guardrail` also works on plain Python functions. Use `Annotated[..., GuardrailExclude()]` to exclude specific parameters from guardrail evaluation:
164+
165+
```python
166+
@guardrail(
167+
validator=CustomValidator(lambda args: "donkey" in args.get("topic", "").lower()),
168+
action=CustomFilterAction(word_to_filter="donkey", replacement="[topic redacted]"),
169+
stage=GuardrailExecutionStage.PRE,
170+
name="Topic Word Filter",
171+
)
172+
def format_joke_for_display(
173+
topic: str,
174+
joke: str,
175+
config: Annotated[dict[str, Any], GuardrailExclude()],
176+
) -> str:
177+
...
178+
```
179+
180+
The PRE guardrail receives `{"topic": ..., "joke": ...}` — the `config` parameter is excluded.
181+
182+
### Custom action
183+
184+
`CustomFilterAction` (defined locally in `graph.py`) demonstrates how to implement a custom `GuardrailAction`. When a violation is detected it replaces the offending word in the tool input dict or string, logs the change, then returns the modified data so execution continues with the sanitised input:
185+
186+
```python
187+
@dataclass
188+
class CustomFilterAction(GuardrailAction):
189+
word_to_filter: str
190+
replacement: str = "***"
191+
192+
def handle_validation_result(self, result, data, guardrail_name):
193+
# filter word from dict/str and return modified data
194+
...
195+
```
196+
197+
## Validator Types
198+
199+
| Validator | What it does | API call? |
200+
|---|---|---|
201+
| `CustomValidator(fn)` | Calls `fn(args_dict)` — returns `True` for violation | No (local) |
202+
| `PIIValidator(entities=[...])` | Detects PII entities (email, phone, person, etc.) | Yes (UiPath API) |
203+
| `PromptInjectionValidator(threshold=...)` | Detects prompt injection attempts | Yes (UiPath API) |
204+
205+
## Reusable Validators
206+
207+
Validators can be declared once and reused across multiple `@guardrail` decorators:
208+
209+
```python
210+
pii_email = PIIValidator(
211+
entities=[PIIDetectionEntity(PIIDetectionEntityType.EMAIL, threshold=0.5)],
212+
)
213+
214+
pii_email_phone = PIIValidator(
215+
entities=[
216+
PIIDetectionEntity(PIIDetectionEntityType.EMAIL, threshold=0.5),
217+
PIIDetectionEntity(PIIDetectionEntityType.PHONE_NUMBER, threshold=0.5),
218+
],
219+
)
220+
```
221+
222+
## Verification
223+
224+
To manually verify each guardrail fires, run from this directory:
225+
226+
```bash
227+
uv run uipath run agent '{"topic": "donkey"}'
228+
```
229+
230+
**Scenario 1 — word filter (PRE):** the LLM includes "donkey" in the joke passed to `analyze_joke_syntax`. `CustomFilterAction` replaces it with `[censored]` before the tool executes. Look for `[FILTER][Joke Content Word Filter]` in stdout.
231+
232+
**Scenario 2 — length limiter (PRE):** if the generated joke exceeds 1000 characters, `BlockAction` raises `AgentRuntimeError(TERMINATION_GUARDRAIL_VIOLATION)` before the tool is called.
233+
234+
**Scenario 3 — PII at tool and agent scope:** supply a topic containing an email address:
235+
236+
```bash
237+
uv run uipath run agent '{"topic": "donkey, test@example.com"}'
238+
```
239+
240+
Both the agent-scope and LLM-scope PII guardrails log a `WARNING` when the email is detected. The tool-scope PII guardrail logs when the email reaches the tool input.
241+
242+
## Differences from the Middleware Approach (`joke-agent`)
243+
244+
| Aspect | Middleware (`joke-agent`) | Decorator (`joke-agent-decorator`) |
245+
|---|---|---|
246+
| Configuration | Middleware class instances passed to `create_agent(middleware=[...])` | `@guardrail(validator=..., action=..., stage=...)` stacked on the target |
247+
| Scope | Explicit `scopes=[...]` list | Inferred automatically from the decorated object |
248+
| Validator + Action | Bundled inside middleware class | Separate, composable objects |
249+
| Tool guardrails | `UiPathDeterministicGuardrailMiddleware(tools=[...])` | `@guardrail` directly on the `@tool` |
250+
| Plain functions | Not supported | `@guardrail` works on any callable, with `GuardrailExclude` for parameter exclusion |
251+
| Custom loops | Not supported (requires `create_agent`) | Works in any custom LangChain/LangGraph graph |
252+
| API calls | Via middleware stack | Direct `uipath.guardrails.evaluate_guardrail()` for built-in validators; local for `CustomValidator` |
253+
254+
## Example Topics
255+
256+
- `"banana"` — normal run, all guardrails pass
257+
- `"donkey"` — triggers the word filter on `analyze_joke_syntax`
258+
- `"donkey, test@example.com"` — triggers word filter + PII guardrails at all scopes
259+
- `"computer"`, `"coffee"`, `"pizza"`, `"weather"`

0 commit comments

Comments
 (0)