24 KiB
#region AgentChat.Spec [C:3] [TYPE ADR] [SEMANTICS spec,requirements,agent-chat,gradio,langgraph]
@BRIEF Feature specification — Gradio submit() + LangGraph agent with native LangChain tools, static interrupt_before HITL, PostgresSaver checkpoints, hybrid keyword+embedding tool router. No custom WebSocket, no REST confirmations, no @assistant_tool for new agent code.
@RATIONALE LangGraph chosen over LangChain AgentExecutor: static interrupt_before + checkpointed resume are native HITL features, PostgresSaver is LangGraph-native checkpointer, create_react_agent from langgraph.prebuilt provides standard tool-calling. Gradio submit() for streaming, nginx/Vite reverse proxy for browser access.
@REJECTED Replacing Svelte frontend with Gradio UI — violates ADR-0006.
@REJECTED Custom SSE/WebSocket — @gradio/client submit() is the native transport.
@REJECTED Custom bidirectional WebSocket — @gradio/client is request/job-oriented, not raw WS.
@REJECTED REST confirmation endpoints — LangGraph interrupt() eliminates them.
@REJECTED @assistant_tool for new tools — native LangChain @tool is SSOT. Old registry → @DEPRECATED for FR-020.
Feature Branch: 033-gradio-agent-chat
Created: 2026-06-08 | Status: Implemented fact snapshot | Last revised: 2026-07-03 (operator UX recovery, hidden runtime context, environment sync)
Current Implementation Facts (2026-07-03)
/agentis served by SvelteKitAgentChat.svelte+AgentChatModel.svelte.ts; the browser connects through@gradio/clientto absolute${window.location.origin}/api/agent/gradio./agentsynchronizes the selected Superset environment fromenvironmentContextStoreand falls back tolocalStorage.selected_env_idafterinitializeEnvironmentContext()so Gradio receivesenv_idbefore every send.- The backend injects hidden
[RUNTIME CONTEXT]into the LLM message: current ISO datetime, environment id, relative-time rules, and bounded dashboard prefetch. The persisted/user-visible message stays clean. DEV_MODE=true ./run.shstarts FastAPI, frontend, and Gradio with explicitBACKEND_URL,GRADIO_URL,GRADIO_SERVER_PORT; Gradio no longer silently falls back to a random port unlessGRADIO_ALLOW_PORT_FALLBACK=true.backend/src/agent/tools.pyis the agent tool SSOT. It exposes 24 native LangChain@toolfunctions:show_capabilities,search_dashboards,get_health_summary,list_environments,get_task_status,list_llm_providers,get_llm_status,create_branch,commit_changes,deploy_dashboard,execute_migration,run_backup,run_llm_validation,run_llm_documentation,list_maintenance_events,start_maintenance,end_maintenance,superset_execute_sql,superset_explore_database,superset_audit_permissions,superset_create_dashboard,superset_copy_dashboard,superset_create_dataset,superset_format_sql.- Deprecated
backend/src/api/routes/assistant/_tool_registry.pyis legacy only and MUST NOT be used by the agent runtime. - Hybrid tool router (
get_tools_for_query()): keyword primary (word-boundary-aware, <1ms) with embedding fallback (cosine similarity viaparaphrase-multilingual-MiniLM-L12-v2, 5-20ms) when keyword matching yields <3 tools. Eliminates substring collision bugs (tool⊂tools, env⊂environment, доступ⊂доступные). Tool subset is 5-10 tools (50-75% token reduction vs full 24-tool catalog). - Negation guard in
fast_confirmation_tool(): regex pre-check for negation patterns (\bне\b,\bno\b,\bdon't\b) bypasses fast-track and routes to LLM. Prevents false-positive confirmations for negated requests like "не показывай схему". - Smart prefetch trigger: dashboard prefetch fires only for search/list intent, excludes creation/diagnostic patterns (
как создать,почему,speed,performance). Capped byAGENT_PREFETCH_DASHBOARD_LIMIT(default25). - HITL guardrails use LangGraph
interrupt_beforefor dangerous tools. Resume creates the agent withinterrupt_before=[]so the confirmed checkpoint does not pause again before the same tool. - The
/agentheader includes an operator status strip: user-facing state, environment context, and process steps (Контекст,Понимание задачи,Инструменты,Подтверждение,Результат). These steps are currently derived from client state and tool metadata; explicit backend progress events remain a future hardening target. - Debug/reference actions are behind a
Диагностикаmenu (debug panel, copy JSON, check LLM) so service data does not compete with primary chat actions. - User-visible message rendering and conversation titles strip hidden context (
[PRE-FETCHED DATA], uploaded-file content blocks). System-only history titles such as✅ list_environmentsnormalize toСистемное действие. - First-activity timeout: if Gradio/LLM emits no token, tool call, confirmation, or thread id within 60s, the stream is converted into an actionable error/recovery state instead of endless "Думаю".
- Recovery card offers
Повторить последний запрос,Проверить LLM,Скопировать debug, andНовый диалог. - Current verification: targeted backend hidden-context test passed; frontend
AgentChatModel.test.ts88 passed; frontend build passed with existing unrelated Svelte warnings.
Architecture Overview
Browser (SvelteKit) Docker
┌──────────────────────┐ nginx ┌──────────────────────┐
│ @gradio/client │──→ /api/agent/gradio─→│ Gradio Agent :7860 │
│ submit("/chat", │ proxy+JWT │ gr.ChatInterface │
│ {message}, │ │ │
│ conversation_id) │ │ ┌─────────────────┐ │
│ for await(event) {} │ │ │ Hybrid Router │ │
│ submission.cancel() │ │ │ keyword primary │ │
│ │ │ │ ↓ <3 tools? │ │
│ REST /api/assistant/*│ │ │ embedding fallbk│ │
└──────────────────────┘ │ └─────────────────┘ │
│ create_agent(model, │
│ tools=subset, │
│ interrupt_before, │
│ checkpointer=PG) │
└──────────┬──────────┘
│ HTTP
┌──────────▼──────────┐
│ FastAPI :8000 │
│ PostgreSQL │
│ /api/assistant (leg.)│
│ /api/auth/svc-token │
└─────────────────────┘
User Stories
Story 1 — Gradio Agent Engine with Streaming Chat (P1) 🎯
Independent Test: Client.connect("/api/agent/gradio"), submit("/chat", {message}, conversation_id), verify tokens stream via for await (event).
Acceptance:
- Given Gradio running, When Svelte calls
submit(), Then tokens stream in real time viaevent.datachunks. - Given streaming in progress, When user calls
submission.cancel(), Then generation stops, partial text preserved.
Story 2 — Autonomous Tool Selection (P1) 🎯
Independent Test: "покажи дашборды с проблемами валидации" → agent calls search + health tools.
Acceptance:
- Given user query, When agent processes it, Then agent selects and invokes
@toolfunctions frombackend/src/agent/tools.py. - Given tool requires confirmation, When agent calls it, Then LangGraph
interrupt_beforepauses execution, UI shows confirmation card. - Given tool fails, When error returned, Then agent explains failure and suggests recovery.
Story 3 — Tool-Call Visibility (P2)
Independent Test: Send multi-tool query, verify each tool appears as inline card with spinner → checkmark/cross transition.
Acceptance:
- Given agent triggers a tool, When
metadata.type="tool_start"received, Then UI shows card with tool name (mono font) and spinner. - Given tool completes, When
metadata.type="tool_end"received, Then spinner becomes green checkmark, result expandable on click. - Given tool fails, When
metadata.type="tool_error"received, Then spinner becomes red cross with error detail expandable on click.
Story 4 — Multi-Turn Context (P2)
Independent Test: Ask "покажи дашборд 42", then "создай для него ветку". Agent resolves "него" → dashboard 42.
Acceptance:
- Given prior messages in conversation, When user uses pronouns/implicit references, Then agent resolves them via PostgreSQL conversation persistence and LangGraph checkpoint context.
- Given conversation exceeds 4000 tokens, When new message sent, Then older messages are summarized/truncated before being passed to LLM.
- Given user starts new conversation, When referencing entities from prior conversation, Then agent does NOT have access to prior context (clean isolation).
Story 5 — Conversation Persistence (P3)
Independent Test: Create 3 conversations, close browser tab, reopen — all 3 visible, resume any.
Acceptance:
- Given past conversations exist, When user opens chat panel, Then conversations listed with auto-titles (truncated first message 60 chars), dates, message counts.
- Given past conversation selected, When user sends new message, Then handler uses
conversation_id/thread_idand persisted messages/checkpoints instead of Gradio-provided history. - Given conversation archived, When user deletes, Then
is_archived=true, removed from active list, visible in archive filter. - Given conversation archived, When associated checkpoints cleared, Then no zombie threads remain in
langgraph-checkpoint-postgrestables.
Story 6 — File Upload & Document Analysis (P2)
Independent Test: Upload PDF report, type "выдели риски". Agent extracts text and responds with structured analysis.
Acceptance:
- Given user attaches PDF, When message sent, Then backend extracts text via pdfplumber, injects as system message context.
- Given user attaches XLSX, When message sent, Then backend parses sheets via openpyxl, presents structured data to agent.
- Given user attaches unsupported format (
.exe,.zip), When upload attempted, Then UI shows validation error with supported format list. - Given file exceeds 10MB, When upload attempted, Then UI rejects with size limit message.
Edge Cases
- Gradio unreachable → reconnect 5×5s
- LLM malformed → retry once, then error
- LLM/Gradio accepts the request but emits no first event → first-activity timeout after 60s and recovery card
- Message too long → truncate + warn
- Tool >30s → async, result when ready
- Multi-tab → REST gate:
GET /api/agent/conversations/{id}/activerejects second tab - Network lost → detect disconnect, offer retry
Functional Requirements
Transport & Core
- FR-001: Gradio
gr.ChatInterface(type="messages", multimodal=True). Consumable via@gradio/clientsubmit("/chat", payload)withadditional_inputs=conversation_id. - FR-002: Svelte frontend uses
@gradio/client submit()exclusively. Nopredict(), no custom WebSocket. - FR-003: Streaming via LangGraph
agent.astream_events()→ yield →event.datachunks. Frontend iteratesfor await (event of submission).
Agent & Tools
- FR-004: LangGraph
create_react_agent(model, tools, checkpointer=PostgresSaver)fromlanggraph.prebuilt.toolsMUST be the intent-scoped subset returned by the hybridget_tools_for_query()— keyword primary (word-boundary-aware, <1ms) with embedding fallback (cosine similarity, 5-20ms) when keyword matching yields <3 tools. Full 24-tool catalog injection is forbidden for models with ≤8k context. - FR-005: Native
@toolfunctions inbackend/src/agent/tools.py. Each calls FastAPI REST. Old@assistant_toolregistry →@DEPRECATEDTombstone and is not used by the agent runtime. - FR-006:
@toolfunctions use PydanticBaseModelforargs_schema. Docstring = description. Single source of metadata. - FR-007: Tools call FastAPI with dual identity: service JWT authenticates the agent, user JWT (forwarded from browser via closure) authorizes the operation. RBAC enforced by FastAPI under user identity.
Confirmation (HumanInTheLoop)
- FR-008: LangGraph static
interrupt_beforefor dangerous tool nodes (deploy_dashboard,execute_migration,commit_changes, and other configured risky operations). Handled entirely by LangGraph — zero REST confirmation endpoints. - FR-009: Resume mechanism: graph pauses at
interrupt_beforenode → primarysubmit()stream yieldsmetadata.type="confirm_required"withthread_idand terminates. User confirms → secondsubmit("/chat", msg, additional_inputs=[conversation_id, "confirm"])→ handler loads checkpoint by thread_id and recreates the graph withinterrupt_before=[]for the resumed run so the same tool is not interrupted again. If checkpoint not found: yieldmetadata.type="error" code="CHECKPOINT_EXPIRED".
Persistence
- FR-010: Conversation history in PostgreSQL (
agent_conversations,agent_messages). REST:POST /api/assistant/conversations,GET .../conversations,GET .../history,DELETE .../{id}. - FR-011: Soft-delete:
is_archived=true. Renamed to "Archive" in UX. - FR-012: LangGraph checkpointer uses PostgreSQL (same instance as FastAPI) via
langgraph-checkpoint-postgres. Survives container restarts. Thread ID = conversation_id. - FR-013: Svelte passes
conversation_idviaadditional_inputs. The Gradio handler receiveshistoryfrom Gradio's built-inChatInterface(mandatory parameter) but MUST ignore it; persisted history is maintained throughagent_conversations/agent_messages, and LangGraph continuity usesthread_id=conversation_id.
UX & Cancel
- FR-014:
submission.cancel()— native cancel. Partial text preserved. - FR-015: Per-user lock via in-memory dict in handler prevents concurrent sends within same user session.
GET /api/agent/conversations/{id}/activeas additional gate for multi-tab. Gradio handles concurrent users by default — no globalconcurrency_limitneeded. - FR-016: Tool-call visibility via structured JSON metadata in
ChatMessage.metadata, not emoji string parsing.{type: "tool_start", tool: "...", input: {...}}. - FR-017: Confirmation card renders from
ChatMessage.metadata.confirm_required.
Auth
- FR-018: Browser → nginx → Gradio. nginx forwards
Authorizationheader. Gradio handler extracts viarequest: gr.Request.headers. - FR-019: Gradio → FastAPI: service JWT from
POST /api/auth/service-token. Dual identity: service JWT authenticates agent, user JWT authorizes tool calls. - FR-020: Stateless JWT validation in Gradio using shared
JWT_SECRET. No DB required for auth.
File Upload
- FR-021: PDF (pdfplumber), XLSX (openpyxl), JSON, CSV, PNG, JPEG.
multimodal=Truehandles upload. Parser inbackend/src/agent/document_parser.py.
Backward Compat
- FR-022: Existing
/api/assistantREST preserved. Old@assistant_tool→@DEPRECATEDTombstone. - FR-023: Existing Svelte UX patterns preserved: drawer/embedded variants, focus-target sync.
Additional
-
FR-024: Audit logging via
LoggingMiddleware— all interactions toassistant_audit. -
FR-025: RU/EN i18n keys for all new UI strings.
-
FR-026: Resume
submit()MUST wait for primary stream cleanup via per-conversation mutex before loading checkpoint. Primary acquires lock on start, releases on stream end. Resume waits for lock release (max 2s timeout). -
FR-027: Deployment MUST run
PostgresSaver.setup()on agent container startup to create checkpoint tables (checkpoints,checkpoint_writes,checkpoint_migrations). Managed via startup hook. -
FR-028: User messages exceeding 100,000 characters (~25k tokens) are truncated to 100,000 characters with
[...truncated]suffix appended. Truncation occurs at sentence boundary when possible. -
FR-029: When conversation is archived (soft-deleted), agent MUST clear associated checkpoints from
langgraph-checkpoint-postgrestables for thatthread_id. -
FR-030: Context budget protection: before every agent run, hybrid
get_tools_for_query()MUST return an intent-scoped subset (5-10 tools, 50-75% token reduction vs full 24-tool catalog). Keyword primary with embedding fallback ensures synonym/typo coverage. Full 24-tool schema injection is forbidden for ≤8k context models. -
FR-031: Dashboard prefetch MUST be bounded and compact (default cap
AGENT_PREFETCH_DASHBOARD_LIMIT=25). Prefetch trigger MUST use intent-aware matching — fire for search/list intent, exclude creation/diagnostic patterns (как создать,почему,speed,performance).prefetch_availableflag suppressessearch_dashboardsin the tool schema. -
FR-032:
/agentMUST expose operator debug/reference info:conv_id, pendingthread_id, connection/streaming state, env/user, message count, last message id, active tool-call count, and current error. The block must be toggleable and copyable as JSON. -
FR-033: DEV mode transport MUST be deterministic: frontend connects to the same origin proxy path,
run.shexports backend/Gradio URLs, and Gradio port fallback is opt-in only. -
FR-034 — Hybrid Intent Router: The agent MUST use a two-tier tool selection strategy. Primary: word-boundary-aware keyword matching (regex
\bguards fortool,env,доступ).show_capabilitiesis always included; no early return blocks other intents. Fallback (triggered when primary <3 tools): embedding-based cosine similarity between user query and tool description vectors via_embedding_router.py. Top-K aboveEMBEDDING_SIMILARITY_THRESHOLD(default 0.65) merged with keyword results. Graceful degradation to keyword-only if embedding model unavailable. -
FR-035 — Negation Guard:
fast_confirmation_tool()MUST pre-check for negation patterns before inferring tool intent. Regex\b(?:не|нет|no|don't|do not|stop|отмена|отмени)\b. If matched, returnNone— bypass fast-track and let LLM handle the negated request. -
FR-036 — Embedding Router Infrastructure:
backend/src/agent/_embedding_router.pyMUST provide: (a) tool description corpus (RU+EN, 1-3 sentences per tool) for embedding; (b) lazy-loadedparaphrase-multilingual-MiniLM-L12-v2model (configurable viaEMBEDDING_MODELenv var); (c) pre-embedded tool descriptions at load time; (d)embedding_top_k(query)returning tool names above cosine threshold. Fails gracefully (returns[]) whensentence-transformersunavailable. -
FR-037 — Hidden Runtime Context: The agent handler MUST pass runtime context to the LLM without persisting or rendering it as user text. Runtime context includes current ISO datetime, current environment, relative-time interpretation rules, and compact dashboard prefetch when available. Saved conversation history and generated titles MUST use the clean user message.
-
FR-038 — Environment Sync:
/agentMUST syncenv_idbefore each send from the selected environment store, with a localStorage fallback during initial route hydration. Debug info MUST showenv_idso missing context is visible during diagnosis. -
FR-039 — Operator Process Strip:
/agentMUST show user-facing operational status and process steps for agent work. Minimum steps: context readiness, task understanding/LLM wait, tool execution, confirmation, result. If backend lacks explicit progress metadata, the UI MAY derive step state from streaming/tool/confirmation/error state, but this must be documented as heuristic. -
FR-040 — First-Activity Timeout Recovery: If a stream enters
streamingbut no token, tool call, confirmation, or checkpoint/thread id arrives within 60 seconds, the frontend MUST cancel/return the submission where possible and show an actionable recovery panel. -
FR-041 — Clean Visible Message Text: UI history, current user bubbles, and conversation titles MUST strip hidden prefetch/runtime/upload blocks. System-only titles SHOULD normalize to a human-readable system label.
-
FR-042 — Diagnostics Menu: Debug/reference actions MUST be grouped behind a diagnostics menu. The menu MUST expose debug-panel toggle, JSON copy, and LLM status check; it MUST be keyboard dismissible and not render raw JSON into chat.
Success Criteria
- SC-001: Tool selection ≥92% accuracy (keyword 85% + embedding fallback covers synonym/typo remainder)
- SC-002: First token <1.5s
- SC-003: Dangerous ops 100% confirmation
- SC-004: RBAC bypass 0%
- SC-005: History survives restart
- SC-006: Multi-step ≥30% faster than manual UI
- SC-007: File upload ≤10MB, no degradation
- SC-008: Hybrid router ensures 50-75% token reduction (5-10 tools vs 24 full catalog). Embedding model lazy-loaded on first fallback call (cold-start ~2s), subsequent calls 5-20ms.
- SC-009: No hidden runtime/prefetch/upload context is visible in chat bubbles or conversation list titles.
- SC-010: A no-first-event stream surfaces recovery controls within 60-65 seconds.
- SC-011: Operator can copy diagnostics and verify LLM status from
/agentwithout exposing debug fields as primary chat actions.
Dependencies
langgraph>=0.2,langchain-core>=0.3,langchain-openai>=0.3,langgraph-checkpoint-postgres(NEW — replaces sqlite)gradio>=5.0,pdfplumber,openpyxlsentence-transformers>=3.0(NEW — embedding fallback for hybrid router; graceful degradation if unavailable)@gradio/clientnpm- Docker: new
superset-tools-agentcontainer, nginx proxy/api/agent/gradio - Deprecated:
backend/src/api/routes/assistant/_tool_registry.py→ Tombstone
@{ AgentChat.ADR.HybridRouter [C:4] [TYPE ADR]
@BRIEF Hybrid keyword+embedding tool router replaces pure substring matching. @RATIONALE Pure substring matching caused P0 (tool⊂tools blocks all tools) and cannot handle synonyms ("панели"≠"дашборды") or typos ("дашборд"). Full 24-tool catalog causes "Tool Paralysis" in models <27B params. Embedding-only breaks on negations ("не делай бэкап" ≈ "сделай бэкап"). Hybrid: keyword for speed+determinism+negation, embedding for synonyms+typos. @REJECTED Full catalog (all 24 tools) — causes Tool Paralysis in Gemma. @REJECTED Embedding-only — blind to negations, adds 5-20ms to every request. @REJECTED Pure substring (with fixes) — cannot handle synonyms or typos. @LAYER Agent Service
@} AgentChat.ADR.HybridRouter
#endregion AgentChat.Spec