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

220 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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 (tooltools, envenvironment, доступдоступные). 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 (tooltools 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