|
| 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