diff --git a/src/uipath_langchain/agent/guardrails/actions/escalate_action.py b/src/uipath_langchain/agent/guardrails/actions/escalate_action.py index bb4baf77a..667683a22 100644 --- a/src/uipath_langchain/agent/guardrails/actions/escalate_action.py +++ b/src/uipath_langchain/agent/guardrails/actions/escalate_action.py @@ -1,14 +1,18 @@ from __future__ import annotations import ast +import hashlib import json import re +import uuid from typing import Any, Dict, Literal, cast +from langchain_core.language_models import BaseChatModel from langchain_core.messages import ( AIMessage, AnyMessage, BaseMessage, + SystemMessage, ToolMessage, ) from langgraph.types import Command, interrupt @@ -25,6 +29,7 @@ BaseGuardrail, GuardrailScope, ) +from uipath.platform.hitl import HitlSchema from uipath.runtime.errors import UiPathErrorCategory from ...exceptions import AgentRuntimeError, AgentRuntimeErrorCode @@ -36,6 +41,23 @@ from .base_action import GuardrailAction, GuardrailActionNodes +_SCHEMA_GEN_PROMPT = SystemMessage( + "A guardrail policy was violated during the agent's tool execution. " + "Based on the conversation above — including the agent's purpose, the user's request, " + "and the tool call that triggered the violation — generate a human review form schema. " + "Input fields should show the reviewer the relevant context (read-only). " + "Output fields should capture the reviewer's decision and any corrections. " + "Keep the schema concise and specific to this escalation." +) + + +def _schema_key(schema: HitlSchema) -> str: + """Deterministic UUID from schema content so Orchestrator can upsert rather than duplicate.""" + wire = json.dumps(schema.to_wire_format(), sort_keys=True) + digest = hashlib.sha256(wire.encode()).digest()[:16] + return str(uuid.UUID(bytes=digest)) + + class EscalateAction(GuardrailAction): """Node-producing action that inserts a HITL interruption node into the graph. @@ -50,19 +72,26 @@ def __init__( app_folder_path: str, version: int, recipient: AgentEscalationRecipient, + model: BaseChatModel | None = None, ): """Initialize EscalateAction with escalation app configuration. Args: - app_name: Name of the escalation app. + app_name: Name of the escalation app. Used when *model* is None + (static Action App path). app_folder_path: Folder path where the escalation app is located. version: Version of the escalation app. recipient: Recipient object (StandardRecipient or AssetRecipient). + model: Optional chat model injected by the agent runtime. When set, + the schema for the HITL task is generated dynamically by the LLM + using the full conversation context at escalation time — no + pre-deployed Action App is needed. """ self.app_name = app_name self.app_folder_path = app_folder_path self.version = version self.recipient = recipient + self.model = model @property def action_type(self) -> str: @@ -202,15 +231,31 @@ async def _create_task_node( data["ToolInputs"] = input_content data["ToolOutputs"] = output_content - # Create the escalation task via API + # Create the escalation task via API. + # Dynamic path: LLM generates a schema from the full conversation + # context so the reviewer form is specific to this escalation. + # Static path (fallback): use the pre-deployed Action App. client = UiPath() - created_task = await client.tasks.create_async( - title="Agents Guardrail Task", - data=data, - app_name=self.app_name, - app_folder_path=self.app_folder_path, - recipient=task_recipient, - ) + if self.model is not None: + generated_schema: HitlSchema = await self.model.with_structured_output( + HitlSchema + ).ainvoke(state.messages + [_SCHEMA_GEN_PROMPT]) + created_task = await client.tasks.create_quickform_async( + title="Agents Guardrail Task", + task_schema_key=_schema_key(generated_schema), + schema=generated_schema.to_wire_format(), + data=data, + folder_path=self.app_folder_path, + recipient=task_recipient, + ) + else: + created_task = await client.tasks.create_async( + title="Agents Guardrail Task", + data=data, + app_name=self.app_name, + app_folder_path=self.app_folder_path, + recipient=task_recipient, + ) # Store task URL in metadata for observability — before interrupt task_id = created_task.id diff --git a/src/uipath_langchain/agent/react/agent.py b/src/uipath_langchain/agent/react/agent.py index e8e186f0f..01a4dcd21 100644 --- a/src/uipath_langchain/agent/react/agent.py +++ b/src/uipath_langchain/agent/react/agent.py @@ -114,6 +114,14 @@ def create_agent( ): node.awrapper = cas_deep_rag_citation_wrapper + # Inject the agent's model into any EscalateAction so it can generate + # HITL form schemas dynamically from the conversation context. + if guardrails: + from ..guardrails.actions.escalate_action import EscalateAction as _EscalateAction + for _, action in guardrails: + if isinstance(action, _EscalateAction) and action.model is None: + action.model = model + tool_nodes_with_guardrails = create_tools_guardrails_subgraph( tool_nodes, guardrails, input_schema=input_schema ) diff --git a/src/uipath_langchain/guardrails/escalate_action.py b/src/uipath_langchain/guardrails/escalate_action.py index a187fb5de..87f3a8f40 100644 --- a/src/uipath_langchain/guardrails/escalate_action.py +++ b/src/uipath_langchain/guardrails/escalate_action.py @@ -58,6 +58,7 @@ GuardrailAction, GuardrailBlockException, ) +from uipath.platform.hitl import HitlSchema from ._action_context import GuardrailActionContext, current_action_context from .enums import GuardrailExecutionStage diff --git a/uv.lock b/uv.lock index b2f3babda..ac406c565 100644 --- a/uv.lock +++ b/uv.lock @@ -9,7 +9,7 @@ resolution-markers = [ ] [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-06-20T03:35:36.833027Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -4374,16 +4374,16 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.17" +version = "0.5.18" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e3/80/a626eb3136a6765e0af06c9d5080ac0843c2a72f17b7a2170f1f45da40dd/uipath_core-0.5.17.tar.gz", hash = "sha256:13565e1eba9f059a8221494dfb3239257ddf7f265fc7057199ffe03ed066300a", size = 119023, upload-time = "2026-05-28T21:34:10.903Z" } +sdist = { url = "https://files.pythonhosted.org/packages/14/b1/d4e555a1a2ccf298195a5f2968e538b0cea8592b3e03f43fc12b178d6c69/uipath_core-0.5.18.tar.gz", hash = "sha256:63ebe8bdb818ca30a4bc9ab0ea8171315680691429931282939359ce039401ab", size = 131988, upload-time = "2026-06-08T14:04:49.688Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/cf/f4b481970621e2a9aec869302773fa2c7d346aef294a553429626369633f/uipath_core-0.5.17-py3-none-any.whl", hash = "sha256:6e088eec5130bc492ac176ab85d4924d7d4cb07ee290ed7e6a46984e9de8c12b", size = 44957, upload-time = "2026-05-28T21:34:09.534Z" }, + { url = "https://files.pythonhosted.org/packages/57/de/1a820b33f7bff4565d7649772bc54c88480ac7e70f707097f7da37d05157/uipath_core-0.5.18-py3-none-any.whl", hash = "sha256:351d6faeecfc6a0acea93182e01526f39c04a77e09fa0444be5f4fb580463f5a", size = 54572, upload-time = "2026-06-08T14:04:48.22Z" }, ] [[package]] @@ -4556,7 +4556,7 @@ wheels = [ [[package]] name = "uipath-platform" -version = "0.1.61" +version = "0.1.67" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -4566,9 +4566,9 @@ dependencies = [ { name = "truststore" }, { name = "uipath-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/84/36bd3b31c268d4fcb4e0a42107c87d35415bea6abcfceb6708c894d4c049/uipath_platform-0.1.61.tar.gz", hash = "sha256:96023a121ab6ed343085431f9f5dc00b6a814e373984b57d5c136497dc70a317", size = 372148, upload-time = "2026-06-05T21:48:11.932Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/17/92459c745bfeb5375cf93c6b0aedd6817c6d6a57a6fea4a7ee56b1ee1eda/uipath_platform-0.1.67.tar.gz", hash = "sha256:3a133327747cc27c3747e09bb83f5f4383f3928a16514a01910fab5a651623ee", size = 375192, upload-time = "2026-06-16T16:43:19.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/bc/bb2484d9f7e95d98b0d3b21cfea0de3714c55ad4ab1b85bc2151fa4299bb/uipath_platform-0.1.61-py3-none-any.whl", hash = "sha256:2075481878df4456fbb1058761edecb6d14b3f675d6d916eddf6b4ef96fca6a5", size = 248039, upload-time = "2026-06-05T21:48:10.267Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/41d7507349d84f0aa8e125f30b67d635bccff188fe308d71bbe1c93f9ab9/uipath_platform-0.1.67-py3-none-any.whl", hash = "sha256:48c106e47231e1c27ad6c571d0ae76187b97c2c229380a9f747a813d2d8f6293", size = 249167, upload-time = "2026-06-16T16:43:18.329Z" }, ] [[package]]