Files
ss-tools/specs/033-gradio-agent-chat/spec.md
2026-07-03 16:47:10 +03:00

24 KiB
Raw Blame History

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

  • /agent is served by SvelteKit AgentChat.svelte + AgentChatModel.svelte.ts; the browser connects through @gradio/client to absolute ${window.location.origin}/api/agent/gradio.
  • /agent synchronizes the selected Superset environment from environmentContextStore and falls back to localStorage.selected_env_id after initializeEnvironmentContext() so Gradio receives env_id before 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.sh starts FastAPI, frontend, and Gradio with explicit BACKEND_URL, GRADIO_URL, GRADIO_SERVER_PORT; Gradio no longer silently falls back to a random port unless GRADIO_ALLOW_PORT_FALLBACK=true.
  • backend/src/agent/tools.py is the agent tool SSOT. It exposes 24 native LangChain @tool functions: 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.py is 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 via paraphrase-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 by AGENT_PREFETCH_DASHBOARD_LIMIT (default 25).
  • HITL guardrails use LangGraph interrupt_before for dangerous tools. Resume creates the agent with interrupt_before=[] so the confirmed checkpoint does not pause again before the same tool.
  • The /agent header 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_environments normalize 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.ts 88 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:

  1. Given Gradio running, When Svelte calls submit(), Then tokens stream in real time via event.data chunks.
  2. 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:

  1. Given user query, When agent processes it, Then agent selects and invokes @tool functions from backend/src/agent/tools.py.
  2. Given tool requires confirmation, When agent calls it, Then LangGraph interrupt_before pauses execution, UI shows confirmation card.
  3. 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:

  1. Given agent triggers a tool, When metadata.type="tool_start" received, Then UI shows card with tool name (mono font) and spinner.
  2. Given tool completes, When metadata.type="tool_end" received, Then spinner becomes green checkmark, result expandable on click.
  3. 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:

  1. Given prior messages in conversation, When user uses pronouns/implicit references, Then agent resolves them via PostgreSQL conversation persistence and LangGraph checkpoint context.
  2. Given conversation exceeds 4000 tokens, When new message sent, Then older messages are summarized/truncated before being passed to LLM.
  3. 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:

  1. Given past conversations exist, When user opens chat panel, Then conversations listed with auto-titles (truncated first message 60 chars), dates, message counts.
  2. Given past conversation selected, When user sends new message, Then handler uses conversation_id / thread_id and persisted messages/checkpoints instead of Gradio-provided history.
  3. Given conversation archived, When user deletes, Then is_archived=true, removed from active list, visible in archive filter.
  4. Given conversation archived, When associated checkpoints cleared, Then no zombie threads remain in langgraph-checkpoint-postgres tables.

Story 6 — File Upload & Document Analysis (P2)

Independent Test: Upload PDF report, type "выдели риски". Agent extracts text and responds with structured analysis.

Acceptance:

  1. Given user attaches PDF, When message sent, Then backend extracts text via pdfplumber, injects as system message context.
  2. Given user attaches XLSX, When message sent, Then backend parses sheets via openpyxl, presents structured data to agent.
  3. Given user attaches unsupported format (.exe, .zip), When upload attempted, Then UI shows validation error with supported format list.
  4. 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}/active rejects second tab
  • Network lost → detect disconnect, offer retry

Functional Requirements

Transport & Core

  • FR-001: Gradio gr.ChatInterface(type="messages", multimodal=True). Consumable via @gradio/client submit("/chat", payload) with additional_inputs=conversation_id.
  • FR-002: Svelte frontend uses @gradio/client submit() exclusively. No predict(), no custom WebSocket.
  • FR-003: Streaming via LangGraph agent.astream_events() → yield → event.data chunks. Frontend iterates for await (event of submission).

Agent & Tools

  • FR-004: LangGraph create_react_agent(model, tools, checkpointer=PostgresSaver) from langgraph.prebuilt. tools MUST be the intent-scoped subset returned by the hybrid get_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 @tool functions in backend/src/agent/tools.py. Each calls FastAPI REST. Old @assistant_tool registry → @DEPRECATED Tombstone and is not used by the agent runtime.
  • FR-006: @tool functions use Pydantic BaseModel for args_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_before for 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_before node → primary submit() stream yields metadata.type="confirm_required" with thread_id and terminates. User confirms → second submit("/chat", msg, additional_inputs=[conversation_id, "confirm"]) → handler loads checkpoint by thread_id and recreates the graph with interrupt_before=[] for the resumed run so the same tool is not interrupted again. If checkpoint not found: yield metadata.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_id via additional_inputs. The Gradio handler receives history from Gradio's built-in ChatInterface (mandatory parameter) but MUST ignore it; persisted history is maintained through agent_conversations / agent_messages, and LangGraph continuity uses thread_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}/active as additional gate for multi-tab. Gradio handles concurrent users by default — no global concurrency_limit needed.
  • 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 Authorization header. Gradio handler extracts via request: 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=True handles upload. Parser in backend/src/agent/document_parser.py.

Backward Compat

  • FR-022: Existing /api/assistant REST preserved. Old @assistant_tool@DEPRECATED Tombstone.
  • FR-023: Existing Svelte UX patterns preserved: drawer/embedded variants, focus-target sync.

Additional

  • FR-024: Audit logging via LoggingMiddleware — all interactions to assistant_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-postgres tables for that thread_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_available flag suppresses search_dashboards in the tool schema.

  • FR-032: /agent MUST expose operator debug/reference info: conv_id, pending thread_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.sh exports 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 \b guards for tool, env, доступ). show_capabilities is 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 above EMBEDDING_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, return None — bypass fast-track and let LLM handle the negated request.

  • FR-036 — Embedding Router Infrastructure: backend/src/agent/_embedding_router.py MUST provide: (a) tool description corpus (RU+EN, 1-3 sentences per tool) for embedding; (b) lazy-loaded paraphrase-multilingual-MiniLM-L12-v2 model (configurable via EMBEDDING_MODEL env var); (c) pre-embedded tool descriptions at load time; (d) embedding_top_k(query) returning tool names above cosine threshold. Fails gracefully (returns []) when sentence-transformers unavailable.

  • 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: /agent MUST sync env_id before each send from the selected environment store, with a localStorage fallback during initial route hydration. Debug info MUST show env_id so missing context is visible during diagnosis.

  • FR-039 — Operator Process Strip: /agent MUST 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 streaming but 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 /agent without 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, openpyxl
  • sentence-transformers>=3.0 (NEW — embedding fallback for hybrid router; graceful degradation if unavailable)
  • @gradio/client npm
  • Docker: new superset-tools-agent container, 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