fix automation run inputs, hitl routing, and detail UI polish

This commit is contained in:
CREDO23 2026-05-28 02:48:47 +02:00
parent ed8d56aa16
commit 91962ba879
8 changed files with 258 additions and 86 deletions

View file

@ -22,11 +22,14 @@ from typing import Any
from uuid import UUID
from fastapi import HTTPException
from langchain.tools import ToolRuntime
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from pydantic import ValidationError
from app.agents.new_chat.tools.hitl import request_approval
from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import (
request_approval,
)
from app.automations.schemas.api import AutomationCreate
from app.automations.services.automation import AutomationService
from app.db import User, async_session_maker
@ -56,7 +59,7 @@ def create_create_automation_tool(
uid = UUID(user_id) if isinstance(user_id, str) else user_id
@tool
async def create_automation(intent: str) -> dict[str, Any]:
async def create_automation(intent: str, runtime: ToolRuntime) -> dict[str, Any]:
"""Draft + save an automation from a natural-language intent.
Use this when the user wants SurfSense to do something on its own
@ -137,6 +140,7 @@ def create_create_automation_tool(
tool_name="create_automation",
params=card_params,
context={"search_space_id": search_space_id},
tool_call_id=runtime.tool_call_id,
)
if result.rejected:
@ -200,6 +204,5 @@ def _extract_json(text: str) -> dict[str, Any] | None:
def _format_validation_issues(exc: ValidationError) -> list[str]:
return [
f"{'.'.join(str(p) for p in err['loc'])}: {err['msg']}"
for err in exc.errors()
f"{'.'.join(str(p) for p in err['loc'])}: {err['msg']}" for err in exc.errors()
]

View file

@ -49,6 +49,7 @@ def request_approval(
params: dict[str, Any],
context: dict[str, Any] | None = None,
trusted_tools: list[str] | None = None,
tool_call_id: str | None = None,
) -> HITLResult:
"""Pause the graph for user approval and return the user's decision.
@ -64,6 +65,10 @@ def request_approval(
forwarded verbatim to the FE for richer card chrome.
trusted_tools: Per-session allowlist; when ``tool_name`` is in it the
interrupt is skipped and the tool runs immediately.
tool_call_id: Caller's LangChain tool-call id. Required for tools
running directly on the main agent; subagent-mounted tools omit
it (the ``task`` chokepoint stamps it on re-raise see
:mod:`...checkpointed_subagent_middleware.propagation`).
Returns:
:class:`HITLResult` with ``rejected=True`` if the user declined or
@ -90,6 +95,8 @@ def request_approval(
interrupt_type=action_type,
context=context,
)
if tool_call_id:
payload["tool_call_id"] = tool_call_id
approval = interrupt(payload)
parsed = parse_lc_envelope(approval)

View file

@ -67,8 +67,15 @@ async def dispatch_run(
def _validate_inputs(
definition: AutomationDefinition, inputs: dict[str, Any]
) -> dict[str, Any]:
"""Validate merged inputs against the optional declared schema.
No declared schema pass through (runtime inputs like ``fired_at`` /
``last_fired_at`` and trigger ``static_inputs`` must still reach the
template context). Returning ``{}`` here strips them and makes Jinja
blow up on any ``{{ inputs.* }}`` reference.
"""
if definition.inputs is None or not definition.inputs.schema_:
return {}
return inputs
try:
jsonschema.validate(instance=inputs, schema=definition.inputs.schema_)
except jsonschema.ValidationError as exc: