feat: add EscalateAction HITL middleware action for guardrails [AL-289]#888
feat: add EscalateAction HITL middleware action for guardrails [AL-289]#888apetraru-uipath wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new human-in-the-loop escalation action for the guardrails middleware path, and updates middleware exception handling so LangGraph control-flow signals (e.g., interrupt(...)) can suspend/resume runs instead of being swallowed by broad exception handlers.
Changes:
- Introduce
EscalateAction(middlewareGuardrailAction) that escalates guardrail violations viainterrupt(CreateEscalation(...))and applies reviewer outcomes (ApprovesubstitutesReviewedInputs,Rejectblocks). - Update built-in guardrail middleware base to re-raise
GraphBubbleUpso HITL interrupts propagate correctly. - Update the
joke-agentsample to demonstrate PII escalation and document configuration/env vars.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/uipath_langchain/guardrails/middlewares/_base.py |
Ensures LangGraph control-flow exceptions (GraphBubbleUp) bubble up instead of being logged/swallowed. |
src/uipath_langchain/guardrails/escalate_action.py |
Adds middleware-compatible EscalateAction to create HITL escalation tasks and handle review outcomes. |
src/uipath_langchain/guardrails/actions.py |
Re-exports EscalateAction alongside existing LogAction/BlockAction. |
src/uipath_langchain/guardrails/__init__.py |
Exposes EscalateAction at the package level. |
samples/joke-agent/README.md |
Documents HITL guardrail escalation usage, behavior, and configuration variables. |
samples/joke-agent/graph.py |
Updates sample guardrails to use EscalateAction and adds env-configurable app parameters. |
| if outcome.lower() == "approve": | ||
| reviewed = response.get("ReviewedInputs") | ||
| if not reviewed: | ||
| return None | ||
| return _coerce_reviewed(reviewed, data_is_dict) |
There was a problem hiding this comment.
This falsy check is intentional and mirrors the factory-path EscalateAction (which also treats an empty reviewed value as 'no change'), so approving without editing the field never wipes the input. Clarified in 6c98e56 via the docstring, an inline comment, and the README so the wording matches the behavior (absent/empty ReviewedInputs → keep original).
aa2dc08 to
6c98e56
Compare
| stage=GuardrailExecutionStage.PRE, | ||
| action=EscalateAction( | ||
| app_name=ESCALATION_APP_NAME, | ||
| app_folder_path=ESCALATION_APP_FOLDER, |
There was a problem hiding this comment.
Here we also support recipientType, but I don't want to add emails to this sample
| to apply guardrail to. Must contain at least one tool. | ||
| Can be a mix of strings (tool names) or BaseTool objects. | ||
| If TOOL scope is not specified, this parameter is ignored. | ||
| stage: Optional execution stage controlling when the guardrail runs. |
There was a problem hiding this comment.
I've extended the functionality to allow pre and post for middleware PII
|
Potential issue: |
|
Re: the potential issue on |
f89bbed to
6e1fc3e
Compare
6e1fc3e to
8408e64
Compare
…[AL-289] Publish the guardrail runtime context (scope / stage / component) around each action call in the LangChain adapter, mirroring the middleware path, so a decorator @guardrail(action=EscalateAction(...)) can derive Component / ExecutionStage at runtime instead of requiring them hardcoded. The action call sites already let GraphInterrupt propagate (they catch only GuardrailBlockException), so interrupt-based escalation suspends correctly with no exception-handling change. - _langchain_adapter.py: add _run_action() helper that publishes GuardrailActionContext for tool/llm/agent pre+post, reusing EscalateAction + _action_context from the middleware escalation (PR #888). - joke-agent-decorator: add an AGENT-scope EMAIL-PII EscalateAction example on create_joke_agent, env-configurable app name/folder, plus README scenarios. - tests: adapter context-publishing unit tests; decorator escalation integration test (suspend -> approve -> reject) alongside the middleware one. Verified live against Guardrail.Escalation.Action.App.2: suspend (Component=Agent, ExecutionStage=PreExecution), approve completes, reject terminates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…middlewares [AL-289] Add EscalateAction, a GuardrailAction that routes a guardrail violation to a human reviewer via the documented HITL primitive interrupt(CreateEscalation(...)): the run suspends with a task in a UiPath Action App, and on resume Approve (optionally editing the reviewed input/output) continues while Reject terminates. - Runtime context: a GuardrailActionContext ContextVar (scope / stage / component / description / original input) is published around each action call so EscalateAction derives the app payload's Component, ExecutionStage, GuardrailDescription and the Inputs/Outputs split automatically — no hardcoding. - Stage-aware payload: PRE fills Inputs; POST fills Outputs and carries the original input in Inputs so the reviewer sees both. Legacy ToolInputs/ToolOutputs are still sent for older apps (comments/docs use Inputs/Outputs). Typed recipient supported. - Shared, stage-gated _build_message_hooks across all message middlewares (PII, harmful content, IP, prompt injection, user prompt attacks) so stage=PRE/POST registers the right before_*/after_* hook once (escalate once per run) and the wiring can't drift. - Re-raise GraphBubbleUp in the middleware so interrupt() isn't swallowed. - Tests (action-context publishing, hook-wiring stage gating, escalate-action suspend/approve/reject, output-stage escalation) and docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8408e64 to
d4145ec
Compare
| # tenant deployment). Defaults point at the "Guardrail Escalation Action App (2)" | ||
| # published as the process "Guardrail.Escalation.Action.App.2" in the "Shared" | ||
| # folder (its deployed name/folder in the tenant — verified via `uip or processes list`). | ||
| ESCALATION_APP_NAME = os.getenv( |
There was a problem hiding this comment.
add bindings in bindings.json for those instead of using env vars. that is the correct design
…utputs applies [AL-289] The after_agent (AGENT-scope POST) hook validated the whole joined conversation, so the flagged Outputs included the input echo and an escalation's ReviewedOutputs edit couldn't be mapped back to a single message — it was silently dropped. Validate only the agent's final AI message (like the LLM-scope after_model hook): Outputs is the agent output, the original input is carried as Inputs, and an approve-with-edit is applied on resume. Add TestMiddlewareEscalation coverage for AGENT-POST and LLM-POST escalations (payload Component/ExecutionStage/Inputs/Outputs + ReviewedOutputs on approve). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rop env vars [AL-289] Address the review comment on graph.py: configure the Guardrail Escalation Action App as a declarative bindings.json "app" resource (mirroring ticket-classification) instead of GUARDRAIL_ESCALATION_APP_* env vars. graph.py passes the literal app name/folder to EscalateAction; the binding is the deploy-time contract that Studio/deploy resolves and can override (locally the literals are used). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
…[AL-289] Publish the guardrail runtime context (scope / stage / component) around each action call in the LangChain adapter, mirroring the middleware path, so a decorator @guardrail(action=EscalateAction(...)) can derive Component / ExecutionStage at runtime instead of requiring them hardcoded. The action call sites already let GraphInterrupt propagate (they catch only GuardrailBlockException), so interrupt-based escalation suspends correctly with no exception-handling change. - _langchain_adapter.py: add _run_action() helper that publishes GuardrailActionContext for tool/llm/agent pre+post, reusing EscalateAction + _action_context from the middleware escalation (PR #888). - joke-agent-decorator: add an AGENT-scope EMAIL-PII EscalateAction example on create_joke_agent, env-configurable app name/folder, plus README scenarios. - tests: adapter context-publishing unit tests; decorator escalation integration test (suspend -> approve -> reject) alongside the middleware one. Verified live against Guardrail.Escalation.Action.App.2: suspend (Component=Agent, ExecutionStage=PreExecution), approve completes, reject terminates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…[AL-289] Publish the guardrail runtime context (scope / stage / component) around each action call in the LangChain adapter, mirroring the middleware path, so a decorator @guardrail(action=EscalateAction(...)) can derive Component / ExecutionStage at runtime instead of requiring them hardcoded. The action call sites already let GraphInterrupt propagate (they catch only GuardrailBlockException), so interrupt-based escalation suspends correctly with no exception-handling change. - _langchain_adapter.py: add _run_action() helper that publishes GuardrailActionContext for tool/llm/agent pre+post, reusing EscalateAction + _action_context from the middleware escalation (PR #888). - joke-agent-decorator: add an AGENT-scope EMAIL-PII EscalateAction example on create_joke_agent, env-configurable app name/folder, plus README scenarios. - tests: adapter context-publishing unit tests; decorator escalation integration test (suspend -> approve -> reject) alongside the middleware one. Verified live against Guardrail.Escalation.Action.App.2: suspend (Component=Agent, ExecutionStage=PreExecution), approve completes, reject terminates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>



What changed?
Adds human-in-the-loop guardrail escalation to the LangChain middleware path, which previously only supported
LogAction/BlockAction.EscalateAction(GuardrailAction)(uipath_langchain.guardrails): on a guardrail violation it escalates the flagged content to a UiPath Action App (e.g. the Guardrail Escalation Action App) via the documented HITL primitiveinterrupt(CreateEscalation(...)). It maps guardrail context onto the action app's input schema (GuardrailName,GuardrailDescription,GuardrailResult,Inputs, plus legacyTool*aliases — mirroring the factory-pathEscalateAction) and applies the reviewer's decision back:Approvereturns the edited content (ReviewedInputs) for the middleware to substitute,RejectraisesGuardrailBlockException.middlewares/_base.py): the built-in guardrail middlewares invoked the action inside a broadexcept Exception:, which caughtinterrupt()'sGraphInterrupt(anExceptionsubclass) and merely logged it. We now re-raiseGraphBubbleUpbefore that handler in_check_messagesand both_run_tool_guardrail(PRE/POST) paths, so an escalation action can suspend/resume the run durably. Purely additive — no existing behavior changes.joke-agentPII guardrail now escalates viaaction=EscalateAction(...)(replacing the prior log-only action), with README docs and env-configurable app name/folder.How has this been tested?
pytest tests/guardrails tests/agent/guardrails→ 332 passed (the shared_base.pychange is non-regressive).ruff check,ruff format --check,mypy→ clean on all changed files.EscalateAction→interrupt(CreateEscalation(...))propagates → theCreateEscalationtask is created in the deployed action app and the run suspends successfully. A non-PII topic completes normally (no escalation).Are there any breaking changes?