Skip to content

Commit 451e856

Browse files
feat: generate HITL form schema dynamically via LLM for guardrail escalations
When a guardrail fires and triggers an escalation, the agent's own LLM now generates a HitlSchema from the full conversation context — system prompt, user request, and the offending tool call — using `model.with_structured_output(HitlSchema)`. The generated schema travels inline with the QuickForm task request (via `create_quickform_async`). No pre-deployed Action App is required. Implementation: - `EscalateAction` (agent graph path) gains an optional `model` field. When set, schema is generated dynamically; when absent, falls back to the existing static Action App path for backward compatibility. - `create_agent` injects the model into any `EscalateAction` in the guardrails list before wiring subgraphs — no API change for callers. - Middleware `EscalateAction` is unchanged (no state.messages access). Depends on: UiPath/uipath-python#1737 (HitlSchema types + QuickForm dispatch)
1 parent 6f3a705 commit 451e856

3 files changed

Lines changed: 63 additions & 9 deletions

File tree

src/uipath_langchain/agent/guardrails/actions/escalate_action.py

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
from __future__ import annotations
22

33
import ast
4+
import hashlib
45
import json
56
import re
7+
import uuid
68
from typing import Any, Dict, Literal, cast
79

10+
from langchain_core.language_models import BaseChatModel
811
from langchain_core.messages import (
912
AIMessage,
1013
AnyMessage,
1114
BaseMessage,
15+
SystemMessage,
1216
ToolMessage,
1317
)
1418
from langgraph.types import Command, interrupt
@@ -28,6 +32,7 @@
2832
BaseGuardrail,
2933
GuardrailScope,
3034
)
35+
from uipath.platform.hitl import HitlSchema
3136
from uipath.runtime.errors import UiPathErrorCategory
3237

3338
from ...exceptions import AgentRuntimeError, AgentRuntimeErrorCode
@@ -40,6 +45,23 @@
4045
from .base_action import GuardrailAction, GuardrailActionNodes
4146

4247

48+
_SCHEMA_GEN_PROMPT = SystemMessage(
49+
"A guardrail policy was violated during the agent's tool execution. "
50+
"Based on the conversation above — including the agent's purpose, the user's request, "
51+
"and the tool call that triggered the violation — generate a human review form schema. "
52+
"Input fields should show the reviewer the relevant context (read-only). "
53+
"Output fields should capture the reviewer's decision and any corrections. "
54+
"Keep the schema concise and specific to this escalation."
55+
)
56+
57+
58+
def _schema_key(schema: HitlSchema) -> str:
59+
"""Deterministic UUID from schema content so Orchestrator can upsert rather than duplicate."""
60+
wire = json.dumps(schema.to_wire_format(), sort_keys=True)
61+
digest = hashlib.sha256(wire.encode()).digest()[:16]
62+
return str(uuid.UUID(bytes=digest))
63+
64+
4365
class EscalateAction(GuardrailAction):
4466
"""Node-producing action that inserts a HITL interruption node into the graph.
4567
@@ -54,19 +76,26 @@ def __init__(
5476
app_folder_path: str,
5577
version: int,
5678
recipient: AgentEscalationRecipient,
79+
model: BaseChatModel | None = None,
5780
):
5881
"""Initialize EscalateAction with escalation app configuration.
5982
6083
Args:
61-
app_name: Name of the escalation app.
84+
app_name: Name of the escalation app. Used when *model* is None
85+
(static Action App path).
6286
app_folder_path: Folder path where the escalation app is located.
6387
version: Version of the escalation app.
6488
recipient: Recipient object (StandardRecipient or AssetRecipient).
89+
model: Optional chat model injected by the agent runtime. When set,
90+
the schema for the HITL task is generated dynamically by the LLM
91+
using the full conversation context at escalation time — no
92+
pre-deployed Action App is needed.
6593
"""
6694
self.app_name = app_name
6795
self.app_folder_path = app_folder_path
6896
self.version = version
6997
self.recipient = recipient
98+
self.model = model
7099

71100
@property
72101
def action_type(self) -> str:
@@ -211,15 +240,31 @@ async def _create_task_node(
211240
data["ToolInputs"] = input_content
212241
data["ToolOutputs"] = output_content
213242

214-
# Create the escalation task via API
243+
# Create the escalation task via API.
244+
# Dynamic path: LLM generates a schema from the full conversation
245+
# context so the reviewer form is specific to this escalation.
246+
# Static path (fallback): use the pre-deployed Action App.
215247
client = UiPath()
216-
created_task = await client.tasks.create_async(
217-
title="Agents Guardrail Task",
218-
data=data,
219-
app_name=self.app_name,
220-
app_folder_path=self.app_folder_path,
221-
recipient=task_recipient,
222-
)
248+
if self.model is not None:
249+
generated_schema: HitlSchema = await self.model.with_structured_output(
250+
HitlSchema
251+
).ainvoke(state.messages + [_SCHEMA_GEN_PROMPT])
252+
created_task = await client.tasks.create_quickform_async(
253+
title="Agents Guardrail Task",
254+
task_schema_key=_schema_key(generated_schema),
255+
schema=generated_schema.to_wire_format(),
256+
data=data,
257+
folder_path=self.app_folder_path,
258+
recipient=task_recipient,
259+
)
260+
else:
261+
created_task = await client.tasks.create_async(
262+
title="Agents Guardrail Task",
263+
data=data,
264+
app_name=self.app_name,
265+
app_folder_path=self.app_folder_path,
266+
recipient=task_recipient,
267+
)
223268

224269
# Store task URL in metadata for observability — before interrupt
225270
task_id = created_task.id

src/uipath_langchain/agent/react/agent.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ def create_agent(
120120
):
121121
node.awrapper = cas_deep_rag_citation_wrapper
122122

123+
# Inject the agent's model into any EscalateAction so it can generate
124+
# HITL form schemas dynamically from the conversation context.
125+
if guardrails:
126+
from ..guardrails.actions.escalate_action import EscalateAction as _EscalateAction
127+
for _, action in guardrails:
128+
if isinstance(action, _EscalateAction) and action.model is None:
129+
action.model = model
130+
123131
tool_nodes_with_guardrails = create_tools_guardrails_subgraph(
124132
tool_nodes, guardrails, input_schema=input_schema
125133
)

src/uipath_langchain/guardrails/escalate_action.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
GuardrailAction,
5959
GuardrailBlockException,
6060
)
61+
from uipath.platform.hitl import HitlSchema
6162

6263
from ._action_context import GuardrailActionContext, current_action_context
6364
from .enums import GuardrailExecutionStage

0 commit comments

Comments
 (0)