#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