Files
ss-tools/specs/033-gradio-agent-chat/spec.md
2026-06-30 19:05:17 +03:00

20 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-06-30 (hybrid router, negation guard, embedding fallback)

Current Implementation Facts (2026-06-30)

  • /agent is served by SvelteKit AgentChat.svelte + AgentChatModel.svelte.ts; the browser connects through @gradio/client to absolute ${window.location.origin}/api/agent/gradio.
  • 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 a collapsible debug/reference block with conv_id, pending thread_id, connection state, streaming state, env/user, message count, last message id, active tool-call count, and current error.
  • Current verification: backend agent tests 375 passed; frontend AgentChatModel.test.ts 73 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
  • 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.

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.

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