Files
ss-tools/specs/033-gradio-agent-chat/spec.md
2026-06-09 11:44:20 +03:00

13 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 interrupt()/Command(resume=...). No custom WebSocket, no REST confirmations, no @assistant_tool for new code. @RATIONALE LangGraph chosen over LangChain AgentExecutor: interrupt() + Command(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 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: Draft | Last revised: 2026-06-08 (consistency sweep)

Architecture Overview

Browser (SvelteKit)                           Docker
┌──────────────────────┐      nginx           ┌─────────────────────┐
│ @gradio/client       │──→ /api/agent/gradio─→│ Gradio Agent :7860  │
│ submit("/chat",      │     proxy+JWT         │ gr.ChatInterface    │
│   {message},         │                       │ create_agent(model, │
│   conversation_id)   │                       │   tools=[@tool...], │
│ for await(event) {}  │                       │   middleware=[HitL], │
│ submission.cancel()  │                       │   checkpointer=PG)  │
│                      │                       │ @tool→HTTP→FastAPI  │
│ REST /api/assistant/*│──────────────────────→│_____________________│
└──────────────────────┘                              │ 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 HumanInTheLoopMiddleware 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 RunnableWithMessageHistory 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 RunnableWithMessageHistory loads previous context from PostgreSQL.
  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. One-line creation.
  • FR-005: Native @tool functions in backend/src/agent/tools.py. Each calls FastAPI REST. Old @assistant_tool registry → @DEPRECATED Tombstone.
  • 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 interrupt() in dangerous tool nodes (deploy_dashboard, execute_migration, commit_changes). 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. If checkpoint not found: yield metadata.type="error" code="CHECKPOINT_EXPIRED". If found: invoke Command(resume={"action":"confirm"}, config={"thread_id":conversation_id}). HITL resume waits for primary stream cleanup via per-conversation mutex (max 2s).

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: RunnableWithMessageHistory auto-loads context from PostgreSQL on resume. Svelte passes conversation_id via additional_inputs only. The Gradio handler receives history from Gradio's built-in ChatInterface (mandatory parameter) but MUST ignore it — loading context from PostgreSQL by thread_id=conversation_id instead.

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.

Success Criteria

  • SC-001: Tool selection ≥85% accuracy
  • 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

Dependencies

  • langgraph>=0.2, langchain-core>=0.3, langchain-openai>=0.3, langgraph-checkpoint-postgres (NEW — replaces sqlite)
  • gradio>=5.0, pdfplumber, openpyxl
  • @gradio/client npm
  • Docker: new ss-tools-agent container, nginx proxy /api/agent/gradio
  • Deprecated: backend/src/api/routes/assistant/_tool_registry.py → Tombstone

#endregion AgentChat.Spec