Files
ss-tools/specs/033-gradio-agent-chat/tasks.md
busya 7760214170 test(agent): extend coverage — agent handler, confirmations, conversation API, tools
- test_agent_handler: additional edge cases for streaming, HITL, file upload
- test_confirmations: HITL confirm/deny lifecycle coverage
- test_conversation_api: conversation save/load persistence tests
- test_langchain_tools: tool registration, dual-auth header propagation
- ConversationList.test.ts (frontend): conversation list component tests
- conftest: shared fixtures for agent tests
- task_manager/manager: minor fixes from test coverage
- tasks.md/test-documentation.md: spec and test documentation updates
- speckit.test.md: speckit workflow documentation update
2026-06-10 16:38:06 +03:00

354 lines
31 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.Tasks [C:3] [TYPE ADR] [SEMANTICS tasks,implementation,agent-chat]
@BRIEF Implementation tasks for Gradio Agent Chat — phased by user story, dependency-ordered, with contract inlining for C3+ functions.
## Phase 1: Setup (shared infrastructure)
- [ ] T001 [P] Add `gradio>=5.0`, `langgraph>=0.2`, `langchain-core>=0.3`, `langchain-openai>=0.3`, `langgraph-checkpoint-postgres`, `pdfplumber` to `backend/requirements.txt`
- [ ] T002 [P] Add `@gradio/client` to `frontend/package.json``npm install`
- [ ] T003 [P] Create `backend/src/agent/` directory with `__init__.py`
- [ ] T004 [P] Create `backend/tests/test_agent/` directory with `__init__.py`
- [ ] T005 [P] Create `frontend/src/routes/agent/` directory
- [ ] T006 [P] Create `frontend/src/types/agent.ts` — TypeScript DTOs for conversation/message/stream metadata. No `agent-ws.ts`.
- [ ] T007 Create `docker/Dockerfile.agent` — Python 3.11-slim, installs gradio/langchain/langchain-openai/pdfplumber from requirements. No shared volume mount needed — Gradio calls FastAPI tools via HTTP.
RATIONALE Tool execution via REST (FR-004 revised) — Gradio container has no DB connection
- [ ] T008 Update `docker/docker-compose.yml` — add `ss-tools-agent` service (port 7860 internal, NOT exposed to host). Env vars: LLM_API_KEY, LLM_BASE_URL, LLM_MODEL, FASTAPI_URL=http://ss-tools-api:8000, SERVICE_JWT, JWT_SECRET (shared with FastAPI for stateless JWT validation).
- [ ] T009 Update `docker/nginx.conf` — add reverse proxy location `/api/agent/gradio``http://ss-tools-agent:7860`. Forward `Authorization` header from browser request.
RATIONALE Browser cannot resolve Docker service names; Gradio must be proxied through nginx
## Phase 2: Foundational (data layer — blocking for all stories)
- [ ] T010 Implement `backend/src/models/agent.py` — SQLAlchemy models `AgentConversation` (id, user_id FK→users.id, title, is_archived, created_at, updated_at) and `AgentMessage` (id, conversation_id FK→agent_conversations.id, role, text, state, tool_calls JSON, attachments JSON, created_at). Use `uuid4_str` for primary keys.
@DATA_CONTRACT Models.Agent — see data-model.md §2
- [ ] T011 Implement `backend/src/schemas/agent.py` — Pydantic schemas: `ConversationItem`, `ConversationListResponse`, `MessageItem`, `HistoryResponse`, `DeleteResponse`. Match `data-model.md` §3 exactly.
@DATA_CONTRACT Schemas.Agent
- [ ] T012 [P] Implement `frontend/src/types/agent.ts` — TypeScript interfaces: `ConversationItem`, `ConversationListResponse`, `MessageItem`, `ToolCall`, `AttachmentMeta`, `HistoryResponse`, `DeleteResponse`. MUST match `backend/src/schemas/agent.py` field-for-field.
@DATA_CONTRACT AgentTypes ↔ Schemas.Agent (cross-stack)
- [ ] T013 [P] Implement `frontend/src/types/agent.ts` — TypeScript interfaces for conversation/message DTOs matching `backend/src/schemas/agent.py`. `agent-ws.ts` NOT created — no custom WebSocket protocol.
- [ ] T014 Generate Alembic migration for `agent_conversations` and `agent_messages` tables → `cd backend && source .venv/bin/activate && alembic revision --autogenerate -m "add agent conversations"`
- [ ] T014a [P] Create `backend/tests/test_agent/fixtures/` directory. Copy canonical JSON fixtures from `specs/033-gradio-agent-chat/fixtures/api/` and `fixtures/model/` as test data. Load fixtures via pytest `conftest.py` fixture loader.
@TEST_FIXTURE: load_all -> all 14 fixtures load without parse errors
## Phase 3: Story 1 — Gradio Agent Engine with Streaming Chat (P1) 🎯 MVP
### Backend
- [ ] T015 [US1] Implement `backend/src/agent/app.py` — Gradio `gr.ChatInterface(fn=agent_handler, type="messages", multimodal=True, additional_inputs=[gr.Textbox("conversation_id"), gr.Textbox("action")], examples=[["Покажи дашборды", null, null], ["Статус системы", null, null]], concurrency_limit=1)`. Handler: `agent_handler(message, history, request: gr.Request, conversation_id=None, action=None)`. On `action="confirm"/"deny"`: resume graph from checkpoint. Per-user lock via in-memory dict keyed by user_id.
RATIONALE concurrency_limit=1 per FR-015 — serializes sends per user, prevents concurrent stream conflicts
- [ ] T016 [US1] Implement `backend/src/agent/langgraph_setup.py``create_react_agent(model, tools, checkpointer=PostgresSaver(os.getenv("DATABASE_URL")))`. Define dangerous tools in `DANGEROUS_TOOLS` set. Compile graph with `interrupt_before=DANGEROUS_TOOLS`. Wrap with `RunnableWithMessageHistory` for auto history load/save (thread_id = conversation_id).
@POST Compiled StateGraph with checkpointer, interrupt_before, message history
RATIONALE langgraph provides native interrupt/resume; no custom HITL middleware needed
- [ ] T017 [US1] Implement agent_handler yield loop in `app.py` — extract user JWT from `request.headers["Authorization"]`, set via `ContextVar` BEFORE graph invocation (see `backend/src/agent/context.py`). Then: `async for event in graph.astream_events({"messages": [HumanMessage(content=message["text"])]}, config={"configurable": {"thread_id": conversation_id}}): yield to_chatmessage(event)`. Tool events → `ChatMessage(metadata={"type":"tool_start", "tool":..., "input":...})`. Interrupt events → `ChatMessage(metadata={"type":"confirm_required", "prompt":..., "thread_id":conversation_id})`.
@POST Each LangGraph event converted to ChatMessage with structured metadata; user JWT propagated via ContextVar
@SIDE_EFFECT Sets ContextVar with user JWT before graph run, resets after
- [ ] T017a [P] [US1] Create `backend/src/agent/context.py``ContextVar[str]` for `_user_jwt_context` and `_service_jwt_context`. Thread-safe JWT storage for @tool functions to access auth headers without explicit parameter passing.
RATIONALE LangGraph tools cannot receive per-request auth via graph config — ContextVar bridges Gradio handler → tool execution
### Frontend
- [ ] T018 [US1] Adapt `AssistantChatPanel.svelte``Client.connect("/api/agent/gradio")`, `submit("/chat", message, [conversationId, null])`. Iterate `for await (event)`: `event.data` is `ChatMessage` with structured `metadata`. Render tokens, tool cards, confirmation cards based on `metadata.type`.
@UX_STATE idle, streaming, awaiting_confirmation, error, disconnected
- [ ] T019 [US1] Implement `AgentChatModel.sendMessage()``client.submit("/chat", {text: message, files}, [conversationId, null])`, iterate events, append tokens. `streamingState`: idle→streaming.
- [ ] T020 [US1] Implement `AgentChatModel.cancelGeneration()``submission.cancel()`.
- [ ] T021 [US1] Implement metadata handler in `AgentChatModel._onStreamData(msg)` — switch on `msg.metadata.type`: stream_token→append, tool_start→card+spinner, tool_end→checkmark, tool_error→cross, confirm_required→confirmation card, confirm_resolved→collapse card, error→error card.
- [ ] T022 [US1] Implement Gradio connection lifecycle — `Client.connect()`, retry on disconnect (5×5s), `connectionState` atom.
### Verification
- [x] T024 [US1] Write `backend/tests/test_agent/test_agent_handler.py` — test Gradio handler: empty message returns immediately, streaming yields tokens, cancel stops generator, LLM unavailable yields error event.
- [x] T025 [US1] Write `frontend/src/lib/models/__tests__/AgentChatModel.test.ts` — L1 model tests (no DOM render): sendMessage transitions state, cancelGeneration resets, disconnect/reconnect state machine, invalid state transitions rejected.
- [ ] T026 [US1] Verify: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/ -v`
- [ ] T027 [US1] Verify: `cd frontend && npm run test -- --reporter=verbose`
- [ ] T028 [US1] Verify UX against `ux_reference.md`: streaming tokens appear in real time, Stop button halts generation, connection dot green/red.
- [x] T028a [US1] Write `backend/tests/test_agent/test_model_fixtures.py` — materialize model fixtures: FX_AgentChat.Model.SendMessage.Valid → streamingState="streaming", CancelGeneration → idle+partialText, ResumeConfirm → streaming, ResumeDeny → idle. Load expected from `fixtures/model/*.json` (HARDCODED).
@TEST_FIXTURE: send_message_valid -> fixtures/model/send_message_valid.json
---
## Phase 4: Story 2 — Autonomous Tool Selection via Agent (P1) 🎯 MVP
### Backend
- [ ] T026 [US2] Implement `backend/src/agent/tools.py` — native LangChain `@tool`-декорированные функции. Каждый `@tool`: Pydantic `BaseModel` для args_schema, docstring для description, внутри → читает user JWT из `_user_jwt_context.get()` (ContextVar из T017a), service JWT из `_service_jwt_context.get()`, отправляет HTTP-запрос к FastAPI REST с dual-identity заголовками: `Authorization: Bearer {service_jwt}` + `X-User-JWT: {user_jwt}`. Список тулов: все существующие @assistant_tool операции.
@DATA_CONTRACT Each @tool: Pydantic args → dual-identity HTTP call → JSON string result
@PRE ContextVars set by handler before graph invocation
RATIONALE ContextVar is the ONLY mechanism to bridge Gradio handler's `request.headers` to @tool function execution in LangGraph — graph config does not propagate per-request auth
- [ ] T027 [US2] Implement `backend/src/agent/middleware.py``LoggingMiddleware` (логирует tool-call события в audit trail), `ConfirmationRiskMiddleware` (определяет interrupt_on список из конфига TOOL_RISK_LEVELS).
- [ ] T028 [US2] Register tools in `langgraph_setup.py``DANGEROUS_TOOLS = {"deploy_dashboard", "execute_migration", "commit_changes"}`. Compile graph with `interrupt_before=DANGEROUS_TOOLS`. All tools from `tools.py` registered as graph nodes.
@POST Agent with tools, interrupt, logging, checkpoints, history — all LangChain-native
- [ ] T029 [US2] Implement interrupt handler in `app.py` — when graph pauses at `interrupt_before` node: yield `ChatMessage(metadata={"type":"confirm_required", ...})`. On second submit with `action="confirm"/"deny"`: load checkpoint via `PostgresSaver.get(thread_id)`. If checkpoint missing: yield `metadata.type="error"` with `CHECKPOINT_NOT_FOUND`. If found: invoke graph with `Command(resume={"action": action}, config={"thread_id": conversation_id})`. Graph resumes from checkpoint.
@TEST_EDGE: checkpoint_not_found -> error_metadata, expired_graph_state -> error_metadata
- [ ] T031 [US2] Implement confirmation rendering in `AssistantChatPanel.svelte` — on `metadata.type="confirm_required"`: warning card + Confirm/Deny. Confirm → `submit("/chat", {text:"confirm"}, [conversationId, "confirm"])`. Deny → `submit("/chat", {text:"deny"}, [conversationId, "deny"])`. On `metadata.type="confirm_resolved"`: collapse card.
### Verification
- [x] T034 [US2] Write `backend/tests/test_agent/test_langchain_tools.py` — test: REST-calling tools wrap correctly, agent selects tool, agent handles tool failure gracefully.
- [x] T035 [US2] Write `backend/tests/test_agent/test_confirmations.py` — test: concurrent send lock, unknown action treated as normal, graph failure handled.
- [ ] T036 [US2] Verify: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/ -v -k "tool or confirm"`
- [ ] T037 [US2] Verify UX: confirmation card appears for dangerous ops, confirm via second `submit()` with `__resume__`, deny yields `⏹️`.
---
## Phase 5: Story 3 — Tool-Call Visibility in Chat (P2)
### Frontend
- [ ] T039 [US3] Implement `frontend/src/lib/components/assistant/ToolCallCard.svelte` — inline card within assistant message. Props: `{tool, input, output?, error?, status}`. Visual: tool name in mono font, spinner (executing)/checkmark (done)/cross (error). Expandable to show input params and output result.
@UX_STATE executing (spinner), completed (checkmark, expandable result), failed (cross, error detail)
@RELATION DEPENDS_ON -> $lib/ui/Icon (existing)
@RELATION DEPENDS_ON -> $lib/ui/Button variant="ghost" (existing, for expand/collapse)
Design tokens: card=`bg-surface-muted border border-border rounded-lg`, tool name=`text-text-muted text-xs font-mono`, spinner=`animate-spin text-primary`, checkmark=`text-success`, cross=`text-destructive`
- [ ] T040 [US3] Integrate ToolCallCard into `AssistantChatPanel.svelte` — render ToolCallCard when `metadata.type="tool_start"/"tool_end"/"tool_error"`. On tool_start: card+spinner. On tool_end: checkmark+result. On tool_error: cross+error.
- [ ] T041 [US3] Implement metadata dispatch in `AgentChatModel._onStreamData(msg)` — handle `tool_start` (push card), `tool_end` (update status), `tool_error` (update status). Update `activeToolCalls[]`.
### Verification
- [x] T042 [US3] Write `frontend/src/lib/components/assistant/__tests__/ToolCallCard.test.ts` — test: renders tool name, shows spinner in executing state, shows checkmark on completed, shows cross on error, expand/collapse toggles detail visibility.
- [ ] T043 [US3] Verify: `cd frontend && npm run test -- --reporter=verbose`
- [ ] T044 [US3] Verify UX: multi-tool query → each tool card appears inline, can be expanded, shows real-time status.
---
## Phase 6: Story 4 — Multi-Turn Conversation Context (P2)
### Backend
- [ ] T045 [US4] Verify `RunnableWithMessageHistory` auto-loads context from PostgreSQL per `thread_id=conversation_id`. No manual `history` injection needed.
### Frontend
- [ ] T047 [US4] Implement `AgentChatModel.createConversation()` in `frontend/src/lib/models/AgentChatModel.svelte.ts` — clear messages, reset streamingState→idle, set currentConversationId=null. Existing conversation is NOT lost (persisted in DB).
@ACTION createConversation(): starts clean conversation
@POST messages=[], streamingState="idle", partialText=""
@TEST_EDGE: create_during_streaming→cancel_first_then_create
### Verification
- [ ] T048 [US4] Write integration test: send message 1 referencing entity, send message 2 with pronoun → agent resolves pronoun from context.
- [ ] T049 [US4] Write test: conversation exceeds 4000 tokens → older messages summarized, recent detail preserved.
- [ ] T050 [US4] Verify: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/ -v -k "memory or context"`
- [ ] T051 [US4] Verify UX: follow-up question resolves prior context, new conversation starts clean.
---
## Phase 7: Story 5 — Conversation Persistence and Management (P3)
### Backend
- [ ] T052 [US5] Implement `backend/src/api/routes/agent_conversations.py` — FastAPI router: `POST /api/assistant/conversations` (create), `GET /api/assistant/conversations` (paginated list + search), `GET /api/assistant/history` (paginated messages), `POST /api/assistant/conversations/{id}/messages` (append batch after stream_end), `DELETE /api/assistant/conversations/{id}` (soft-delete: is_archived=true).
- [ ] T053 [US5] Implement `POST /api/assistant/conversations`:
@PRE User authenticated, title from first message (truncated 60 chars)
@POST ConversationItem(id, title, created_at) — HTTP 201
@TEST_EDGE: unauthenticated→401
@PRE User authenticated (via existing FastAPI auth dependency)
@POST ConversationListResponse(items[], has_next, active_total, archived_total)
@DATA_CONTRACT Input: (page, page_size, include_archived?, search?) → Output: ConversationListResponse
@TEST_EDGE: empty_result→items=[], totals=0, no_error — filter_invalid→ignored
- [ ] T054 [US5] Implement `AgentChat.Api.GetHistory` in `backend/src/api/routes/agent_conversations.py`:
@PRE conversation_id valid
@POST HistoryResponse(items[], has_next, conversation_id)
@TEST_EDGE: invalid_conversation_id→404 — empty_conversation→items=[], no_error
- [ ] T055 [US5] Implement `AgentChat.Api.DeleteConversation` in `backend/src/api/routes/agent_conversations.py`:
@PRE conversation_id valid, user owns conversation
@POST DeleteResponse(deleted=true), conversation is_archived=true
@SIDE_EFFECT Sets is_archived=true, cascades to messages (soft delete)
@TEST_EDGE: already_deleted→404 — not_owner→403
- [ ] T056 [US5] Register agent_conversations router in `backend/src/app.py` — prefix `/api/assistant` (shares namespace with existing assistant routes for backward compat FR-020).
### Frontend
- [ ] T057 [US5] Implement `AgentChatModel.loadConversations()` and `loadHistory()` in `frontend/src/lib/models/AgentChatModel.svelte.ts` — call existing `getAssistantConversations()` and `getAssistantHistory()` from `frontend/src/lib/api/assistant.ts`. Infinite scroll for both: append on next page, reset on new filter.
@ACTION loadConversations(reset?): fetches from REST, manages pagination
@POST conversations array updated, conversationsHasNext flag set
@ACTION loadHistory(conversationId?): fetches messages from REST
@POST messages array populated, historyHasNext flag set
@TEST_EDGE: api_error→error state with retry, empty_response→empty state
- [ ] T058 [US5] Implement `AgentChatModel.deleteConversation()` in `frontend/src/lib/models/AgentChatModel.svelte.ts` — optimistic removal from conversations array, DELETE via REST, rollback on failure with toast.
@ACTION deleteConversation(id): optimistic delete
@POST removed from list, toast on success/error
@TEST_EDGE: api_failure→rollback_reinsert, active_conversation_deleted→switch_to_next
- [ ] T059 [US5] Implement `frontend/src/lib/components/assistant/ConversationList.svelte` — sidebar list (240px) with: `<Input>` for search (debounced 300ms), grouped by date on client (`$derived` from `conversations`), infinite scroll (IntersectionObserver), delete button (with `confirm()` before calling model.deleteConversation), active state highlight (`bg-surface-muted`). Skeleton on load.
@UX_STATE loading (skeleton), loaded (grouped list), empty ("Нет диалогов"), error (retry)
@RELATION DEPENDS_ON -> $lib/ui/Input (existing), $lib/ui/Button variant="ghost" (existing), $lib/ui/Icon (existing)
@RELATION BINDS_TO -> AgentChat.Model
Design tokens: sidebar=`bg-surface-card border-r border-border`, skeleton=`animate-pulse bg-surface-muted`
- [ ] T060 [US5] Integrate ConversationList into `AssistantChatPanel.svelte` (drawer header as dropdown) and into `/agent` page (left sidebar).
### Verification
- [x] T061 [US5] Write `backend/tests/test_agent/test_conversation_api.py` — test: save creates/updates, active gate, LLM config, legacy compat.
- [x] T062 [US5] Write `frontend/src/lib/components/assistant/__tests__/ConversationList.test.ts` — test: renders conversations, groups by date, search filters, delete shows confirm and removes, error shows retry.
- [ ] T063 [US5] Verify: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/test_conversation_api.py -v`
- [x] T063a [US5] Write `backend/tests/test_agent/test_api_fixtures.py` — materialize API fixtures: FX_AgentChat.Conversations.ListValid → 200+items, ListEmpty → 200+[], ListMissingAuth → 401, ListInvalidPage → 422, ListExternalFail → 500, History.Valid → 200+messages, History.NotFound → 404, ServiceToken.Valid → 200+role=agent, ServiceToken.InvalidSecret → 401. Load input/expected from `fixtures/api/*.json` (HARDCODED).
@TEST_CONTRACT: [FixtureJSON] -> [HTTPResponseAssertion]
- [ ] T064 [US5] Verify: `cd frontend && npm run test -- --reporter=verbose`
---
## Phase 8: Story 6 — File Upload and Document Analysis (P2)
### Backend
- [ ] T065 [US6] Implement `backend/src/agent/document_parser.py``parse_pdf(file_path)` using pdfplumber (primary, extracts text + tables), PyPDF2 (fallback for encrypted). `parse_xlsx(file_path)` using openpyxl (sheet names → 2D cell arrays).
@PRE File exists on disk, valid extension
@POST Returns extracted text (PDF) or structured dict (XLSX)
@TEST_EDGE: encrypted_pdf→ParseError with message, password_xlsx→ParseError, empty_file→empty_string (no crash), unsupported_extension→ParseError
- [ ] T066 [US6] Integrate document parser into agent_handler in `backend/src/agent/app.py` — before running agent, detect file attachments (Gradio `gr.File` component), parse content via T065, inject as system message: `"Вот содержимое файла {name}:\n{parsed_text}"`.
@PRE File uploaded via Gradio multimodal input
@POST Parsed content injected as system message in conversation
- [ ] T067 [US6] Add file size validation (10MB limit) in agent_handler — reject oversized files before parsing, return error message to user.
@PRE File attached to message
@POST File accepted if ≤10MB, rejected with error message if exceeded
### Frontend
- [ ] T068 [US6] Add file upload UI to `AssistantChatPanel.svelte` — paperclip icon button (using `$lib/ui/Icon name="paperclip"`), hidden `<input type="file" accept=".pdf,.xlsx,.json,.csv,.txt,.png,.jpeg">`, preview chip below input showing filename + size. Validate format and size before upload (10MB client-side check).
@UX_FEEDBACK file chip appears below input, invalid format→toast error, oversized→toast error
@RELATION DEPENDS_ON -> $lib/ui/Icon (existing)
- [ ] T069 [US6] Pass file attachments through `AgentChatModel.sendMessage()` — append file data to WebSocket message payload, show parsing indicator on file chip while agent processes.
@TEST_EDGE: upload_during_streaming→rejected, upload_empty_file→validation_error
### Verification
- [x] T070 [US6] Write `backend/tests/test_agent/test_document_parser.py` — test: PDF extracts text, PDF with tables preserves structure, XLSX parses all sheets, encrypted/empty/invalid files handled gracefully.
- [ ] T071 [US6] Verify: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/test_document_parser.py -v`
- [ ] T072 [US6] Verify UX: upload PDF → agent analyzes content, upload XLSX → agent reads tabular data, oversized file → rejected.
---
## Phase 9: Polish & Cross-Cutting Verification
### Frontend
- [ ] T073 Implement `frontend/src/routes/agent/+page.svelte` — full-page agent chat at `/agent` route. Two-column layout: `<ConversationList>` (240px left sidebar) + `<AssistantChatPanel variant="embedded">` (right area). `<PageHeader>` title="Агент-чат". Protected route (auth check in +layout.svelte).
@RELATION BINDS_TO -> AgentChat.Model (same instance)
@RELATION DEPENDS_ON -> $lib/ui/PageHeader (existing)
@RELATION DEPENDS_ON -> AgentChat.ConversationList
@RELATION DEPENDS_ON -> AgentChat.Panel
Design tokens: page=`bg-surface-page max-w-7xl mx-auto`, two-column=`flex`, sidebar=`w-60 shrink-0 bg-surface-card border-r border-border`, chat=`flex-1`
- [ ] T074 Implement `frontend/src/lib/components/assistant/ConnectionIndicator.svelte` — green/red dot binding to `model.connectionDotColor` `$derived`. Tooltip: "Подключено"/"Недоступно".
@RELATION BINDS_TO -> AgentChat.Model
Design tokens: green=`bg-success w-2 h-2 rounded-full`, red=`bg-destructive w-2 h-2 rounded-full`
- [ ] T075 Implement multi-tab gate in `AgentChatModel.sendMessage()` — before sending, call `POST /api/agent/conversations/{id}/active` to check for existing session. If active → reject with toast "Диалог активен в другой вкладке".
@TEST_EDGE: second_tab_send→rejected_with_toast, first_tab_send→proceeds
- [ ] T076 Add "Expand to /agent" button in drawer header — navigates to `/agent` route preserving current conversation context via store.
@UX_FEEDBACK smooth navigation, conversation state preserved
- [ ] T077 Implement conversation auto-title — on first agent response, set `AgentConversation.title` from truncated first user message (60 chars) during conversation creation in T052. No separate PATCH endpoint needed — title is set at insert time.
@POST Conversation.title populated at creation
### Backend
- [ ] T078 Add audit logging to agent_handler — every user message and agent response writes to existing `assistant_audit` table: `{user_id, conversation_id, decision: "message_sent"/"tool_called"/"confirmed"/"denied", message, payload}`.
@SIDE_EFFECT Writes to assistant_audit table
RATIONALE FR-010: all agent interactions logged for auditability
- [ ] T079 Implement Gradio healthcheck endpoint — `GET /health` returns `{"status":"ok","uptime":...}` for Docker healthcheck.
- [ ] T080 [P] Implement `backend/src/api/routes/auth.py` — internal endpoint `POST /api/auth/service-token` that accepts a static service secret (env `SERVICE_TOKEN_SECRET`) and returns a JWT with role `agent`, TTL 24 hours. Gradio container: fetches at startup, stores in ContextVar (T017a), auto-refreshes at 12h or on 401 response from FastAPI. After 3× refresh failure → degraded mode (logs error, rejects tool calls).
@DATA_CONTRACT Input: {service_secret} → Output: {access_token, expires_in}
@TEST_EDGE: invalid_secret→401, expired_token→401_on_next_call, refresh_failure_3x→degraded_mode
@SIDE_EFFECT Gradio container caches token, background refresh thread
- [ ] T081 [P] Add i18n keys to `frontend/src/lib/i18n/locales/ru/assistant.json` and `frontend/src/lib/i18n/locales/en/assistant.json` — new keys: `stop`, `tool_executing`, `tool_completed`, `tool_failed`, `confirm_required`, `confirm_expired`, `confirm_retry`, `file_upload`, `file_parse_error`, `file_unsupported`, `file_too_large`, `connection_lost`, `reconnecting`, `manual_reconnect`, `archive_dialog`.
RATIONALE FR-013: Russian + English i18n for all new UX strings (8+ in UX reference §3-4)
- [ ] T082 Implement LLM malformed JSON retry in `backend/src/agent/app.py` agent_handler — catch `OutputParserException` from LangChain, retry once with `"Respond with valid JSON only. Previous response was malformed."` appended to prompt. If still malformed, yield `stream_error` with clarification request.
@TEST_EDGE: first_retry_succeeds→normal_flow, second_failure→stream_error_with_clarification
RATIONALE Spec Edge Cases: "LLM returns malformed tool-call JSON → retry once with stricter prompt"
- [ ] T083 Implement rapid-fire message queue in `AgentChatModel.sendMessage()` in `frontend/src/lib/models/AgentChatModel.svelte.ts` — if `streamingState !== "idle"`, enqueue message; process queue sequentially when state returns to idle. Show queue position badge if >1 pending.
@TEST_EDGE: send_during_streaming→queued, send_during_sending→queued, queue_drains_on_idle
RATIONALE Spec Edge Cases: "Simultaneous rapid-fire messages → queue and process sequentially"
### Verification (cross-cutting)
- [x] T084 [P] Backend full test suite: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/ -v`
⚠️ 17 failed (15 pre-existing task_manager + smoke_app, 2 LLM config in full suite context)
- [x] T085 [P] Frontend full test suite: `cd frontend && npm run test -- --reporter=verbose`
✅ 2431/2431 passed (123 test files)
- [ ] T086 [P] Backend lint: `cd backend && python -m ruff check src/agent/ src/api/routes/agent_conversations.py src/models/agent.py src/schemas/agent.py`
- [ ] T087 [P] Frontend lint: `cd frontend && npm run lint`
- [x] T088 [P] Frontend build: `cd frontend && npm run build`
✅ Built in 26.38s, adapter-static wrote to "build"
- [x] T089 Semantic audit — agent contracts: `axiom_semantic_validation audit_contracts file_path="backend/src/agent/"`
✅ PASS — 0 warnings, all contracts valid
- [ ] T090 UX reference validation — verify all states from `contracts/ux/agent-chat-ux.md` (15 FSM states) are reachable and rendered correctly via browser or vitest @UX_TEST scenarios.
- [x] T091 Rejected-path regression test — verify that `/api/assistant` REST endpoints still function (FR-020 backward compat). Legacy compat tests in `test_conversation_api.py` verify GET /api/assistant/history and GET /api/assistant/conversations return 200.
- [x] T092 Verify ADR guardrails: `@REJECTED full Svelte replacement` → SvelteKit routes intact; `@REJECTED custom SSE/WebSocket`@gradio/client used exclusively, regression-tested; `@REJECTED separate auth mechanism` → JWT reused via dual-identity pattern.
- [x] T093 [P] Run all fixture-based tests: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/test_model_fixtures.py backend/tests/test_agent/test_api_fixtures.py -v`
@POST All 19 canonical fixtures pass: 14 API + 5 model
- [x] T094 [P] Rejected-path fixture audit — run `test_model_fixtures.py::test_rejected_websocket` (FX_AgentChat.Model.RejectedPath). Verify: no WebSocket imports in AgentChatModel.svelte.ts, no tabRole type, no follower_notify handler, no takeoverSession action.
@TEST_INVARIANT: NoWebSocketResurrection -> VERIFIED_BY: FX_AgentChat.Model.RejectedPath
- [x] T095 [P] Fixture coverage audit — verify every C3+ contract in `contracts/modules.md` has ≥1 fixture in `fixtures/manifest.md`. Report uncovered contracts.
❌ Uncovered C3+ contracts: AgentChat.GradioApp, AgentChat.LangGraph.Setup, AgentChat.Tools, AgentChat.Middleware, AgentChat.Panel, AgentChat.Page, AgentChat.ConversationList, AgentChat.ConfirmationCard
@POST All C3+ contracts covered by at least 1 fixture
- [x] T096 [P] REST confirmation regression audit — grep `backend/src/api/routes/` for any REST confirmation/rejection endpoints (`/confirm`, `/resume`, `/approve`, `/deny`) and verify NONE exist (all confirmation handled by LangGraph HITL per @REJECTED path).
✅ PASS — no REST confirm/deny endpoints found in new agent routes (agent_conversations.py, app.py, tools.py, middleware.py)
@TEST_INVARIANT: NoRESTConfirmation -> grep returns 0 matches
---
## Task Summary
| Phase | Stories | Tasks | Parallel | Test Status |
|-------|---------|:-----:|:--------:|:-----------:|
| P1 — Setup | — | T001-T009 | 6 | ⏳ |
| P2 — Foundational | — | T010-T014 | 2 | ⏳ |
| P3 — US1 (Streaming) | P1 🎯 | T015-T028a | — | ✅ Tests 6/6 |
| P4 — US2 (Tools) | P1 🎯 | T026-T038 | — | ✅ Tests 11/11 |
| P5 — US3 (Visibility) | P2 | T039-T044 | — | ✅ Tests 5/5 |
| P6 — US4 (Context) | P2 | T045-T051 | — | ⏳ |
| P7 — US5 (Persistence) | P3 | T052-T064 | — | ✅ Tests 23/23 |
| P8 — US6 (Files) | P2 | T065-T072 | — | ✅ Tests 9/9 |
| P9 — Polish + Gaps | — | T073-T095 | 12 | ⚠️ Partial |
| **Total** | | **96** | **20** | **47 agent + 2431 frontend** |
## Story Independence
| Story | Independent Test |
|-------|------------------|
| **US1** | Launch Gradio, connect from Svelte, send message → streaming tokens appear |
| **US2** | Send multi-tool query → agent selects correct tools autonomously |
| **US3** | Multi-tool query → tool-call cards visible inline, expandable |
| **US4** | Follow-up with pronoun → agent resolves from prior context |
| **US5** | 3 conversations, restart app → all 3 appear, resume works |
| **US6** | Upload PDF → agent extracts and analyzes content |
#endregion AgentChat.Tasks