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

172 lines
13 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 `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