Skip to content

feat: add EscalateAction HITL middleware action for guardrails [AL-289]#888

Open
apetraru-uipath wants to merge 3 commits into
mainfrom
feat/langchain_guardrails_escalations
Open

feat: add EscalateAction HITL middleware action for guardrails [AL-289]#888
apetraru-uipath wants to merge 3 commits into
mainfrom
feat/langchain_guardrails_escalations

Conversation

@apetraru-uipath

Copy link
Copy Markdown
Contributor

What changed?

Adds human-in-the-loop guardrail escalation to the LangChain middleware path, which previously only supported LogAction/BlockAction.

  • New 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 primitive interrupt(CreateEscalation(...)). It maps guardrail context onto the action app's input schema (GuardrailName, GuardrailDescription, GuardrailResult, Inputs, plus legacy Tool* aliases — mirroring the factory-path EscalateAction) and applies the reviewer's decision back: Approve returns the edited content (ReviewedInputs) for the middleware to substitute, Reject raises GuardrailBlockException.
  • Middleware no longer swallows interrupts (middlewares/_base.py): the built-in guardrail middlewares invoked the action inside a broad except Exception:, which caught interrupt()'s GraphInterrupt (an Exception subclass) and merely logged it. We now re-raise GraphBubbleUp before that handler in _check_messages and both _run_tool_guardrail (PRE/POST) paths, so an escalation action can suspend/resume the run durably. Purely additive — no existing behavior changes.
  • Sample: the joke-agent PII guardrail now escalates via action=EscalateAction(...) (replacing the prior log-only action), with README docs and env-configurable app name/folder.

Note: the package version bump (pyproject.toml0.11.13) and uv.lock / sample dependency pin land in a follow-up commit on this branch (currently pointing at local editable dev paths).

How has this been tested?

  • pytest tests/guardrails tests/agent/guardrails332 passed (the shared _base.py change is non-regressive).
  • ruff check, ruff format --check, mypy → clean on all changed files.
  • Live end-to-end run: a PII-containing topic triggers EscalateActioninterrupt(CreateEscalation(...)) propagates → the CreateEscalation task 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?

  • None

Copilot AI review requested due to automatic review settings June 4, 2026 13:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (middleware GuardrailAction) that escalates guardrail violations via interrupt(CreateEscalation(...)) and applies reviewer outcomes (Approve substitutes ReviewedInputs, Reject blocks).
  • Update built-in guardrail middleware base to re-raise GraphBubbleUp so HITL interrupts propagate correctly.
  • Update the joke-agent sample 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.

Comment thread src/uipath_langchain/guardrails/escalate_action.py
Comment on lines +117 to +121
if outcome.lower() == "approve":
reviewed = response.get("ReviewedInputs")
if not reviewed:
return None
return _coerce_reviewed(reviewed, data_is_dict)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread samples/joke-agent/README.md
@apetraru-uipath apetraru-uipath force-pushed the feat/langchain_guardrails_escalations branch from aa2dc08 to 6c98e56 Compare June 4, 2026 16:32
Comment thread samples/joke-agent/graph.py Outdated
stage=GuardrailExecutionStage.PRE,
action=EscalateAction(
app_name=ESCALATION_APP_NAME,
app_folder_path=ESCALATION_APP_FOLDER,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've extended the functionality to allow pre and post for middleware PII

Comment thread docs/guardrails.md Outdated
Comment thread samples/joke-agent/README.md Outdated
Comment thread src/uipath_langchain/guardrails/escalate_action.py Outdated
Comment thread src/uipath_langchain/guardrails/escalate_action.py Outdated
Comment thread src/uipath_langchain/guardrails/escalate_action.py Outdated
Comment thread src/uipath_langchain/guardrails/escalate_action.py Outdated
Comment thread src/uipath_langchain/guardrails/escalate_action.py
Comment thread docs/guardrails.md Outdated
Comment thread docs/guardrails.md Outdated
Comment thread samples/joke-agent/README.md Outdated
Comment thread src/uipath_langchain/guardrails/middlewares/_base.py Outdated
Comment thread src/uipath_langchain/guardrails/middlewares/harmful_content.py Outdated
@valentinabojan

Copy link
Copy Markdown
Contributor

Potential issue: UiPathPromptInjectionMiddleware and UiPathUserPromptAttacksMiddleware still invoke _check_messages(list(messages)) without passing scope=GuardrailScope.LLM and stage=GuardrailExecutionStage.PRE. Since EscalateAction relies on the published action context, escalations from these PRE-only LLM middlewares omit Component / Tool / ExecutionStage from the Action App payload, even though the docs say these fields are derived automatically. Could we pass the same context here as the PII/Harmful/IP middleware paths do?

Comment thread src/uipath_langchain/guardrails/escalate_action.py
@apetraru-uipath

Copy link
Copy Markdown
Contributor Author

Re: the potential issue on UiPathPromptInjectionMiddleware / UiPathUserPromptAttacksMiddleware not passing scope/stage — fixed in f4bd4d5. Both now build their before_model hook via the shared _build_message_hooks(GuardrailScope.LLM, GuardrailExecutionStage.PRE, ...), which calls _check_messages with scope=LLM, stage=PRE. Verified at runtime that the published context is now scope=LLM, stage=PRE, component='LLM call', so escalations from these middlewares include Component/Tool/ExecutionStage. Added TestPromptInjectionHookWiring.

@apetraru-uipath apetraru-uipath force-pushed the feat/langchain_guardrails_escalations branch from f89bbed to 6e1fc3e Compare June 9, 2026 12:53
@apetraru-uipath apetraru-uipath force-pushed the feat/langchain_guardrails_escalations branch from 6e1fc3e to 8408e64 Compare June 9, 2026 13:45
apetraru-uipath added a commit that referenced this pull request Jun 9, 2026
…[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>
@apetraru-uipath apetraru-uipath force-pushed the feat/langchain_guardrails_escalations branch from 8408e64 to d4145ec Compare June 10, 2026 12:20
Comment thread samples/joke-agent/graph.py Outdated
# 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(

@radu-mocanu radu-mocanu Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add bindings in bindings.json for those instead of using env vars. that is the correct design

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

apetraru-uipath and others added 2 commits June 10, 2026 16:20
…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>
@sonarqubecloud

Copy link
Copy Markdown

apetraru-uipath added a commit that referenced this pull request Jun 10, 2026
…[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>
apetraru-uipath added a commit that referenced this pull request Jun 10, 2026
…[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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants