This commit is contained in:
2026-06-09 10:10:26 +03:00
parent a7fb06cd8e
commit 25b7a6d423
19 changed files with 297 additions and 58 deletions

View File

@@ -10,6 +10,7 @@ Auto-generated from all feature plans. Last updated: 2026-05-08
- Python 3.9+ (backend), TypeScript (frontend Svelte 5 runes-only) + FastAPI 0.126, SQLAlchemy, APScheduler 3.11, httpx 0.28 (already present), anyio 4.12 (already present) (032-translate-requests-httpx)
- PostgreSQL 16 (unchanged — DB operations via asyncio.to_thread) (032-translate-requests-httpx)
- Python 3.9+ (backend), TypeScript (frontend Svelte 5 runes-only) + FastAPI, SQLAlchemy, Gradio ≥5.0, LangChain ≥0.3, langchain-openai (backend); SvelteKit 5, Vite, Tailwind CSS, @gradio/client (frontend) (033-gradio-agent-chat)
- PostgreSQL 16 (persistence + checkpoints via langgraph-checkpoint-postgres) (033-gradio-agent-chat)
- Python 3.13+ (backend), JavaScript/TypeScript (frontend Svelte 5) + FastAPI 0.115+, SQLAlchemy 2.0+, APScheduler 3.x, Pydantic v2 (backend); SvelteKit 2.x, Svelte 5.43+, Vite 7.x, Tailwind CSS 3.x (frontend) (028-llm-datasource-supeset)
@@ -30,9 +31,9 @@ cd src [ONLY COMMANDS FOR ACTIVE TECHNOLOGIES][ONLY COMMANDS FOR ACTIVE TECHNOLO
Python 3.13+ (backend), JavaScript/TypeScript (frontend Svelte 5): Follow standard conventions
## Recent Changes
- 033-gradio-agent-chat: Added PostgreSQL 16 (persistence + checkpoints via langgraph-checkpoint-postgres)
- 033-gradio-agent-chat: Added Python 3.9+ (backend), TypeScript (frontend Svelte 5 runes-only) + FastAPI, SQLAlchemy, Gradio ≥5.0, LangChain ≥0.3, langchain-openai (backend); SvelteKit 5, Vite, Tailwind CSS, @gradio/client (frontend)
- 032-translate-requests-httpx: Added Python 3.9+ (backend), TypeScript (frontend Svelte 5 runes-only) + FastAPI 0.126, SQLAlchemy, APScheduler 3.11, httpx 0.28 (already present), anyio 4.12 (already present)
- 031-maintenance-banner: Added Python 3.9+ (backend), JavaScript/TypeScript (frontend Svelte 5 runes) + FastAPI 0.126, SQLAlchemy 2.0, APScheduler 3.11 (backend); SvelteKit 2.x, Svelte 5.43, Vite 7.x, Tailwind CSS 3.x (frontend)
<!-- MANUAL ADDITIONS START -->

View File

@@ -0,0 +1 @@
{"fixture_id":"FX_AgentChat.Conversations.ListEmpty","verifies":"AgentChat.Api.ListConversations","edge":"empty_result","input":{"method":"GET","path":"/api/assistant/conversations","query":{"page":1,"page_size":20},"headers":{"Authorization":"Bearer eyJ..."}},"expected":{"status":200,"body":{"items":[],"has_next":false,"active_total":0,"archived_total":0}}}

View File

@@ -0,0 +1 @@
{"fixture_id":"FX_AgentChat.Conversations.ListExternalFail","verifies":"AgentChat.Api.ListConversations","edge":"external_fail","input":{"method":"GET","path":"/api/assistant/conversations","query":{"page":1,"page_size":20},"headers":{"Authorization":"Bearer eyJ..."}},"expected":{"status":500,"body":{"error":{"code":"INTERNAL_ERROR","detail":"Database connection failed"}}}}

View File

@@ -0,0 +1 @@
{"fixture_id":"FX_AgentChat.Conversations.ListInvalidPage","verifies":"AgentChat.Api.ListConversations","edge":"invalid_type","input":{"method":"GET","path":"/api/assistant/conversations","query":{"page":-1,"page_size":20},"headers":{"Authorization":"Bearer eyJ..."}},"expected":{"status":422,"body":{"error":{"code":"VALIDATION_ERROR","detail":"page must be >= 1"}}}}

View File

@@ -0,0 +1 @@
{"fixture_id":"FX_AgentChat.Conversations.ListMissingAuth","verifies":"AgentChat.Api.ListConversations","edge":"missing_field","input":{"method":"GET","path":"/api/assistant/conversations","query":{"page":1,"page_size":20},"headers":{}},"expected":{"status":401,"body":{"error":{"code":"UNAUTHORIZED","detail":"Missing Authorization header"}}}}

View File

@@ -0,0 +1,33 @@
{
"fixture_id": "FX_AgentChat.Conversations.ListValid",
"verifies": "AgentChat.Api.ListConversations",
"edge": "valid_request",
"input": {
"method": "GET",
"path": "/api/assistant/conversations",
"query": { "page": 1, "page_size": 20 },
"headers": { "Authorization": "Bearer eyJhbGciOiJIUzI1NiIs..." }
},
"expected": {
"status": 200,
"body": {
"items": [
{
"id": "conv-001",
"title": "Анализ дашбордов",
"updated_at": "2026-06-08T10:00:00Z",
"message_count": 12
},
{
"id": "conv-002",
"title": "Миграция staging→prod",
"updated_at": "2026-06-07T18:30:00Z",
"message_count": 5
}
],
"has_next": false,
"active_total": 5,
"archived_total": 2
}
}
}

View File

@@ -0,0 +1 @@
{"fixture_id":"FX_AgentChat.History.NotFound","verifies":"AgentChat.Api.GetHistory","edge":"invalid_conversation_id","input":{"method":"GET","path":"/api/assistant/history","query":{"conversation_id":"nonexistent","page":1,"page_size":30},"headers":{"Authorization":"Bearer eyJ..."}},"expected":{"status":404,"body":{"error":{"code":"NOT_FOUND","detail":"Conversation not found"}}}}

View File

@@ -0,0 +1 @@
{"fixture_id":"FX_AgentChat.History.Valid","verifies":"AgentChat.Api.GetHistory","edge":"valid_conversation_id","input":{"method":"GET","path":"/api/assistant/history","query":{"conversation_id":"conv-001","page":1,"page_size":30},"headers":{"Authorization":"Bearer eyJ..."}},"expected":{"status":200,"body":{"items":[{"id":"msg-001","conversation_id":"conv-001","role":"user","text":"Покажи дашборды","state":null,"tool_calls":null,"attachments":null,"created_at":"2026-06-08T09:00:00Z"},{"id":"msg-002","conversation_id":"conv-001","role":"assistant","text":"Вот список дашбордов: ...","state":"completed","tool_calls":[{"tool":"search_dashboards","input":{"query":"*"},"output":{"count":5},"error":null,"status":"completed"}],"attachments":null,"created_at":"2026-06-08T09:00:05Z"}],"has_next":false,"conversation_id":"conv-001"}}}

View File

@@ -0,0 +1 @@
{"fixture_id":"FX_AgentChat.ServiceToken.InvalidSecret","verifies":"AgentChat.Api.ServiceToken","edge":"invalid_secret","input":{"method":"POST","path":"/api/auth/service-token","body":{"service_secret":"wrong-secret"},"headers":{}},"expected":{"status":401,"body":{"error":{"code":"UNAUTHORIZED","detail":"Invalid service secret"}}}}

View File

@@ -0,0 +1 @@
{"fixture_id":"FX_AgentChat.ServiceToken.Valid","verifies":"AgentChat.Api.ServiceToken","edge":"valid_secret","input":{"method":"POST","path":"/api/auth/service-token","body":{"service_secret":"correct-secret-value"},"headers":{}},"expected":{"status":200,"body":{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","token_type":"bearer","expires_in":86400,"role":"agent"}}}

View File

@@ -0,0 +1,158 @@
#region FixtureManifest [C:3] [TYPE ADR] [SEMANTICS test,fixture,agent-chat]
@defgroup Fixtures Canonical test fixtures for Gradio Agent Chat (LangGraph stack).
## @{ Fixture FX_AgentChat.Conversations.ListValid [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Valid paginated conversation list response.
@RELATION VERIFIES -> [AgentChat.Api.ListConversations]
@TEST_FIXTURE: list_valid -> fixtures/api/conversations_list_valid.json
@TEST_EDGE: valid_request -> 200 with items[] and meta
## @} Fixture FX_AgentChat.Conversations.ListValid
## @{ Fixture FX_AgentChat.Conversations.ListEmpty [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Empty conversation list — new user, no conversations.
@RELATION VERIFIES -> [AgentChat.Api.ListConversations]
@TEST_FIXTURE: list_empty -> fixtures/api/conversations_list_empty.json
@TEST_EDGE: empty_result -> items=[], totals=0
## @} Fixture FX_AgentChat.Conversations.ListEmpty
## @{ Fixture FX_AgentChat.Conversations.ListMissingAuth [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Missing Authorization header → 401.
@RELATION VERIFIES -> [AgentChat.Api.ListConversations]
@TEST_FIXTURE: list_missing_auth -> fixtures/api/conversations_list_missing_auth.json
@TEST_EDGE: missing_field (Authorization) -> 401 UNAUTHORIZED
## @} Fixture FX_AgentChat.Conversations.ListMissingAuth
## @{ Fixture FX_AgentChat.Conversations.ListInvalidPage [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Invalid page parameter → 422 validation error.
@RELATION VERIFIES -> [AgentChat.Api.ListConversations]
@TEST_FIXTURE: list_invalid_page -> fixtures/api/conversations_list_invalid_page.json
@TEST_EDGE: invalid_type (page=-1) -> 422 VALIDATION_ERROR
## @} Fixture FX_AgentChat.Conversations.ListInvalidPage
## @{ Fixture FX_AgentChat.Conversations.ListExternalFail [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Database connection failure → 500.
@RELATION VERIFIES -> [AgentChat.Api.ListConversations]
@TEST_FIXTURE: list_external_fail -> fixtures/api/conversations_list_external_fail.json
@TEST_EDGE: external_fail (DB down) -> 500 INTERNAL_ERROR
## @} Fixture FX_AgentChat.Conversations.ListExternalFail
## @{ Fixture FX_AgentChat.History.Valid [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Valid paginated history response for existing conversation.
@RELATION VERIFIES -> [AgentChat.Api.GetHistory]
@TEST_FIXTURE: history_valid -> fixtures/api/history_valid.json
@TEST_EDGE: valid_conversation_id -> 200 with messages[]
## @} Fixture FX_AgentChat.History.Valid
## @{ Fixture FX_AgentChat.History.NotFound [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Non-existent conversation → 404.
@RELATION VERIFIES -> [AgentChat.Api.GetHistory]
@TEST_FIXTURE: history_not_found -> fixtures/api/history_not_found.json
@TEST_EDGE: invalid_conversation_id -> 404 NOT_FOUND
## @} Fixture FX_AgentChat.History.NotFound
## @{ Fixture FX_AgentChat.History.MissingField [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Missing conversation_id parameter → 422.
@RELATION VERIFIES -> [AgentChat.Api.GetHistory]
@TEST_FIXTURE: history_missing_field -> fixtures/api/history_missing_field.json
@TEST_EDGE: missing_field (conversation_id) -> 422 VALIDATION_ERROR
## @} Fixture FX_AgentChat.History.MissingField
## @{ Fixture FX_AgentChat.Delete.Valid [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Valid soft-delete (archive) of owned conversation.
@RELATION VERIFIES -> [AgentChat.Api.DeleteConversation]
@TEST_FIXTURE: delete_valid -> fixtures/api/delete_valid.json
@TEST_EDGE: valid_conversation_id -> 200, is_archived=true
## @} Fixture FX_AgentChat.Delete.Valid
## @{ Fixture FX_AgentChat.Delete.NotOwner [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Deleting another user's conversation → 403.
@RELATION VERIFIES -> [AgentChat.Api.DeleteConversation]
@TEST_FIXTURE: delete_not_owner -> fixtures/api/delete_not_owner.json
@TEST_EDGE: not_owner -> 403 FORBIDDEN
## @} Fixture FX_AgentChat.Delete.NotOwner
## @{ Fixture FX_AgentChat.Delete.NotFound [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Deleting already-deleted conversation → 404.
@RELATION VERIFIES -> [AgentChat.Api.DeleteConversation]
@TEST_FIXTURE: delete_not_found -> fixtures/api/delete_not_found.json
@TEST_EDGE: already_deleted -> 404 NOT_FOUND
## @} Fixture FX_AgentChat.Delete.NotFound
## @{ Fixture FX_AgentChat.ServiceToken.Valid [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Valid service token request → JWT with role agent.
@RELATION VERIFIES -> [AgentChat.Api.ServiceToken]
@TEST_FIXTURE: token_valid -> fixtures/api/service_token_valid.json
@TEST_EDGE: valid_secret -> 200, role=agent
## @} Fixture FX_AgentChat.ServiceToken.Valid
## @{ Fixture FX_AgentChat.ServiceToken.InvalidSecret [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Wrong service secret → 401.
@RELATION VERIFIES -> [AgentChat.Api.ServiceToken]
@TEST_FIXTURE: token_invalid_secret -> fixtures/api/service_token_invalid_secret.json
@TEST_EDGE: invalid_secret -> 401 UNAUTHORIZED
## @} Fixture FX_AgentChat.ServiceToken.InvalidSecret
## @{ Fixture FX_AgentChat.Document.ParsePdf.Valid [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Valid PDF → extracted text.
@RELATION VERIFIES -> [AgentChat.Document.ParsePdf]
@TEST_FIXTURE: pdf_valid -> fixtures/api/document_parse_pdf_valid.json
@TEST_EDGE: valid_pdf -> text extracted
## @} Fixture FX_AgentChat.Document.ParsePdf.Valid
## @{ Fixture FX_AgentChat.Document.ParsePdf.Encrypted [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Encrypted PDF → ParseError.
@RELATION VERIFIES -> [AgentChat.Document.ParsePdf]
@TEST_FIXTURE: pdf_encrypted -> fixtures/api/document_parse_pdf_encrypted.json
@TEST_EDGE: encrypted_pdf -> ParseError
## @} Fixture FX_AgentChat.Document.ParsePdf.Encrypted
## @{ Fixture FX_AgentChat.Document.ParseXlsx.Valid [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Valid XLSX → structured sheet data.
@RELATION VERIFIES -> [AgentChat.Document.ParseXlsx]
@TEST_FIXTURE: xlsx_valid -> fixtures/api/document_parse_xlsx_valid.json
@TEST_EDGE: valid_xlsx -> sheets parsed
## @} Fixture FX_AgentChat.Document.ParseXlsx.Valid
## @{ Fixture FX_AgentChat.Document.ParseXlsx.PasswordProtected [C:2] [TYPE Block] [SEMANTICS test,fixture,api]
@BRIEF Password-protected XLSX → ParseError.
@RELATION VERIFIES -> [AgentChat.Document.ParseXlsx]
@TEST_FIXTURE: xlsx_password -> fixtures/api/document_parse_xlsx_password.json
@TEST_EDGE: password_protected -> ParseError with clear message
## @} Fixture FX_AgentChat.Document.ParseXlsx.PasswordProtected
## @{ Fixture FX_AgentChat.Model.SendMessage.Valid [C:2] [TYPE Block] [SEMANTICS test,fixture,model]
@BRIEF Valid sendMessage invocation — transitions to streaming state.
@RELATION VERIFIES -> [AgentChat.Model]
@TEST_FIXTURE: send_valid -> fixtures/model/send_message_valid.json
@TEST_INVARIANT: StreamingStateGatesInput -> VERIFIED_BY: [Test.AgentChat.Model]
## @} Fixture FX_AgentChat.Model.SendMessage.Valid
## @{ Fixture FX_AgentChat.Model.CancelGeneration [C:2] [TYPE Block] [SEMANTICS test,fixture,model]
@BRIEF Cancel during streaming — returns to idle, partial text preserved.
@RELATION VERIFIES -> [AgentChat.Model]
@TEST_FIXTURE: cancel_streaming -> fixtures/model/cancel_generation.json
@TEST_EDGE: cancel_during_streaming -> idle, partialText preserved
## @} Fixture FX_AgentChat.Model.CancelGeneration
## @{ Fixture FX_AgentChat.Model.ResumeConfirm [C:2] [TYPE Block] [SEMANTICS test,fixture,model]
@BRIEF HITL resume confirm — transitions from awaiting_confirmation to streaming.
@RELATION VERIFIES -> [AgentChat.Model]
@TEST_FIXTURE: resume_confirm -> fixtures/model/resume_confirm.json
@TEST_EDGE: confirm_from_awaiting -> streaming, pendingThreadId cleared
## @} Fixture FX_AgentChat.Model.ResumeConfirm
## @{ Fixture FX_AgentChat.Model.ResumeDeny [C:2] [TYPE Block] [SEMANTICS test,fixture,model]
@BRIEF HITL resume deny — transitions to idle.
@RELATION VERIFIES -> [AgentChat.Model]
@TEST_FIXTURE: resume_deny -> fixtures/model/resume_deny.json
@TEST_EDGE: deny_from_awaiting -> idle
## @} Fixture FX_AgentChat.Model.ResumeDeny
## @{ Fixture FX_AgentChat.Model.RejectedPath [C:2] [TYPE Block] [SEMANTICS test,fixture,model]
@BRIEF @REJECTED: WebSocket model cannot be resurrected — verify no WebSocket imports.
@RELATION VERIFIES -> [AgentChat.Model]
@TEST_FIXTURE: rejected_ws -> fixtures/model/rejected_websocket.json
@TEST_INVARIANT: NoWebSocketImports -> VERIFIED_BY: [Test.AgentChat.Model.RejectedPaths]
## @} Fixture FX_AgentChat.Model.RejectedPath
#endregion FixtureManifest

View File

@@ -0,0 +1 @@
{"fixture_id":"FX_AgentChat.Model.CancelGeneration","verifies":"AgentChat.Model","edge":"cancel_during_streaming","input":{"action":"cancelGeneration","state_before":{"streamingState":"streaming","partialText":"Вот результаты поиска: найдено 3 дашборда"}},"expected":{"state_after":{"streamingState":"idle","partialText":"Вот результаты поиска: найдено 3 дашборда","isInputLocked":false}}}

View File

@@ -0,0 +1 @@
{"fixture_id":"FX_AgentChat.Model.RejectedPath","verifies":"AgentChat.Model","invariant":"NoWebSocketImports","edge":"rejected_path","input":{"check":"source_imports","state_before":{}},"expected":{"result":"PASS","assertions":["No 'WebSocket' in AgentChatModel.svelte.ts imports","No 'tabRole' type in model","No 'follower_notify' handler","No 'takeoverSession' action"]},"rationale":"@REJECTED WebSocket-based model — verify it cannot be resurrected"}

View File

@@ -0,0 +1 @@
{"fixture_id":"FX_AgentChat.Model.ResumeConfirm","verifies":"AgentChat.Model","edge":"confirm_from_awaiting","input":{"action":"resumeConfirm","args":["confirm"],"state_before":{"streamingState":"awaiting_confirmation","pendingThreadId":"thread-001","currentConversationId":"conv-001"}},"expected":{"state_after":{"streamingState":"streaming","pendingThreadId":null}}}

View File

@@ -0,0 +1 @@
{"fixture_id":"FX_AgentChat.Model.ResumeDeny","verifies":"AgentChat.Model","edge":"deny_from_awaiting","input":{"action":"resumeConfirm","args":["deny"],"state_before":{"streamingState":"awaiting_confirmation","pendingThreadId":"thread-001","currentConversationId":"conv-001"}},"expected":{"state_after":{"streamingState":"idle","pendingThreadId":null}}}

View File

@@ -0,0 +1 @@
{"fixture_id":"FX_AgentChat.Model.SendMessage.Valid","verifies":"AgentChat.Model","invariant":"StreamingStateGatesInput","input":{"action":"sendMessage","args":["Покажи дашборды"],"state_before":{"streamingState":"idle","connectionState":"connected","currentConversationId":"conv-001"}},"expected":{"state_after":{"streamingState":"streaming","isInputLocked":true,"error":null,"partialText":"","activeToolCalls":[]}}}

View File

@@ -1,71 +1,39 @@
# Implementation Plan: Gradio Agent Chat (LangChain v1 Native Stack)
# Implementation Plan: Gradio Agent Chat (LangGraph)
**Branch**: `033-gradio-agent-chat` | **Date**: 2026-06-08 | **Spec**: `spec.md`
## Summary
Integrate Gradio as agent backend, Svelte frontend via `@gradio/client submit()`. LangGraph agent: `create_react_agent()` + `@tool` + `interrupt()`/`Command(resume=...)` + `RunnableWithMessageHistory` + `PostgresSaver`. Structured JSON metadata streaming. No REST confirmations, no custom WebSocket.
Gradio agent backend + Svelte frontend via `@gradio/client submit()`. LangGraph `create_react_agent()` + `interrupt()`/`Command(resume=...)` + `PostgresSaver` + `RunnableWithMessageHistory`. Structured JSON metadata streaming. No REST confirmations, no custom WebSocket.
## Technical Context
**Language**: Python 3.9+/TypeScript Svelte 5 runes
**Key Dependencies**: Gradio5.0, LangChain≥1.0, langchain-openai≥1.0, langgraph-checkpoint, pdfplumber, openpyxl (back); @gradio/client (front)
**Storage**: PostgreSQL 16 (persistence + checkpoints via langgraph-checkpoint-postgres)
**Testing**: pytest (back), vitest L1 model + L2 UX (front)
**Frontend**: SvelteKit SPA, model-first (AgentChatModel.svelte.ts C4), runes-only
**Target**: Linux/Docker, modern browsers
**Performance**: First token <1.5s, streaming 60fps, file upload 10MB
**Language**: Python 3.9+/TypeScript Svelte 5 runes
**Key Dependencies**: gradio>=5.0, langgraph>=0.2, langchain-core>=0.3, langchain-openai>=0.3, langgraph-checkpoint-postgres, pdfplumber, openpyxl (back); @gradio/client (front)
**Storage**: PostgreSQL 16 (persistence + checkpoints via langgraph-checkpoint-postgres)
**Testing**: pytest (back), vitest L1 model + L2 UX (front)
**Frontend**: SvelteKit SPA, model-first (AgentChatModel.svelte.ts C4), runes-only
**Performance**: First token <1.5s, streaming 60fps, file upload 10MB
## Constitution Check — All 8 Principles Passed
## Constitution Check — Passed
| Principle | Compliance |
|-----------|-----------|
| I. Semantic Contract First | All 18 contracts use #region/#endregion, hierarchical IDs, C1-C4 tiers |
| II. Decision Memory | @RATIONALE/@REJECTED on all 8 research decisions; T030 deprecates @assistant_tool |
| III. External Orchestrator | Gradio agent calls FastAPI services; no Superset plugin coupling |
| IV. Module Discipline | All modules <400 lines; model ~220 lines |
| V. RBAC | User JWT forwarded to each tool call (closure factory); permission checks in FastAPI |
| VI. Svelte 5 Runes | $state/$derived only; no legacy syntax |
| VII. Test-Driven C3+ | @TEST_EDGE on all C3+ contracts; 10 test tasks |
| VIII. Attention-Optimized | ATTN_1-4 validated; one-line anchors, hierarchical IDs, shared @SEMANTICS, 150 lines |
All 8 principles satisfied: semantic contracts, decision memory, external orchestrator, module discipline, RBAC, Svelte 5 runes, test-driven C3+, attention-optimized.
## Project Structure
### New/Changed Files
```
backend/src/agent/
├── app.py # gr.ChatInterface + handler + resume protocol
├── langchain_setup.py # create_agent() + middleware + checkpointer
├── tools.py # @tool functions → FastAPI REST
├── middleware.py # LoggingMiddleware, risk config
├── document_parser.py # PDF + XLSX parsing
└── run.py # entrypoint
backend/src/api/routes/
├── agent_conversations.py # CRUD + multi-tab gate
└── auth.py # POST /api/auth/service-token
backend/src/models/agent.py # AgentConversation, AgentMessage
backend/src/schemas/agent.py # Pydantic schemas
frontend/src/lib/models/AgentChatModel.svelte.ts # submit() lifecycle
frontend/src/lib/components/assistant/
├── AssistantChatPanel.svelte # adapted for @gradio/client submit()
├── ConversationList.svelte # new
├── ToolCallCard.svelte # new
├── ConfirmationCard.svelte # new
└── ConnectionIndicator.svelte # new
frontend/src/routes/agent/+page.svelte # new
frontend/src/types/agent.ts # TypeScript DTOs
docker/Dockerfile.agent # new
docker/docker-compose.yml # updated
docker/nginx.conf # updated: proxy /api/agent/gradio
backend/src/agent/ — app.py, langgraph_setup.py, tools.py, middleware.py, document_parser.py
backend/src/api/routes/ — agent_conversations.py, auth.py
backend/src/models/ — agent.py
backend/src/schemas/ — agent.py
frontend/src/lib/models/ — AgentChatModel.svelte.ts (rewritten)
frontend/src/lib/components/assistant/ — ConversationList, ToolCallCard, ConfirmationCard, ConnectionIndicator
frontend/src/routes/agent/ — +page.svelte
frontend/src/types/ — agent.ts
docker/ — Dockerfile.agent, docker-compose.yml updated, nginx.conf updated
```
## Deprecated
`backend/src/api/routes/assistant/_tool_registry.py` + `_tool_*.py` `@DEPRECATED` Tombstone. Old `@assistant_tool` registry preserved for FR-020.
#endregion (plan.md)
`backend/src/api/routes/assistant/_tool_registry.py` `@DEPRECATED` Tombstone (FR-022).
#endregion (plan summary)

View File

@@ -32,6 +32,9 @@
- [ ] 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
@@ -64,7 +67,10 @@
- [ ] 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/yellow.
- [ ] T028 [US1] Verify UX against `ux_reference.md`: streaming tokens appear in real time, Stop button halts generation, connection dot green/red.
- [ ] 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
---
@@ -190,8 +196,11 @@
- [ ] T061 [US5] Write `backend/tests/test_agent/test_conversation_api.py` — test: list returns paginated, search filters by title, history returns messages, delete archives conversation, 404 on invalid id, 403 on other user's conversation.
- [ ] 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`
- [ ] T064 [US5] Verify: `cd frontend && npm run test -- --reporter=verbose`
- [ ] 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)
@@ -288,6 +297,15 @@
- [ ] T091 Rejected-path regression test — verify that `/api/assistant` REST endpoints still function (FR-020 backward compat). Existing `backend/tests/test_assistant_api.py` passes unmodified.
- [ ] T092 Verify ADR guardrails: `@REJECTED full Svelte replacement` → SvelteKit routes intact; `@REJECTED custom SSE/WebSocket`@gradio/client used exclusively; `@REJECTED separate auth mechanism` → JWT reused.
- [ ] 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
- [ ] 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
- [ ] T095 [P] Fixture coverage audit — verify every C3+ contract in `contracts/modules.md` has ≥1 fixture in `fixtures/manifest.md`. Report uncovered contracts.
@POST All C3+ contracts covered by at least 1 fixture
---
## Task Summary
@@ -302,8 +320,8 @@
| P6 — US4 (Context) | P2 | T045-T051 | — |
| P7 — US5 (Persistence) | P3 | T052-T064 | — |
| P8 — US6 (Files) | P2 | T065-T072 | — |
| P9 — Polish + Gaps | — | T073-T092 | 8 |
| **Total** | | **92** | **16** |
| P9 — Polish + Gaps | — | T073-T095 | 12 |
| **Total** | | **95** | **19** |
## Story Independence

View File

@@ -0,0 +1,48 @@
#region Traceability [C:3] [TYPE ADR] [SEMANTICS traceability,rtm,agent-chat]
@defgroup Trace Matrix Requirements → Model → API → Fixture → Task → Test for Gradio Agent Chat.
## Traceability Matrix
| Story | Screen | Model | Fixture | API Endpoint | Backend Task | Frontend Task | Test |
|-------|--------|-------|---------|-------------|:-----------:|:------------:|------|
| **US1**: Streaming Chat | AssistantChatPanel | AgentChat.Model | FX_AgentChat.Model.SendMessage.Valid | `submit("/chat")` | T015, T017 | T018, T019 | Test.AgentChat.Model |
| **US1**: Cancel | AssistantChatPanel | AgentChat.Model | FX_AgentChat.Model.CancelGeneration | `submission.cancel()` | — | T020 | Test.AgentChat.Model |
| **US1**: Reconnect | AssistantChatPanel | AgentChat.Model | — | `Client.connect()` | — | T022 | Test.AgentChat.Model |
| **US2**: Tool Selection | AssistantChatPanel | AgentChat.Tools | — | `POST /api/git/...` via tool | T026, T028 | T031 | Test.AgentChat.Tools |
| **US2**: HITL Confirm | AssistantChatPanel | AgentChat.Model | FX_AgentChat.Model.ResumeConfirm | `submit("/chat",...,[convId,"confirm"])` | T029 | T031 | Test.AgentChat.Model |
| **US2**: HITL Deny | AssistantChatPanel | AgentChat.Model | FX_AgentChat.Model.ResumeDeny | `submit("/chat",...,[convId,"deny"])` | T029 | T031 | Test.AgentChat.Model |
| **US3**: Tool Visibility | AssistantChatPanel | AgentChat.Model | — | — | — | T040, T041 | Test.AgentChat.ToolCallCard |
| **US4**: Context | AssistantChatPanel | AgentChat.LangGraph | — | `RunnableWithMessageHistory` | T045 | — | Test.AgentChat.LangGraph |
| **US5**: List Conversations | ConversationList | AgentChat.Model | FX_AgentChat.Conversations.ListValid | `GET /api/assistant/conversations` | T052, T053 | T059 | Test.AgentChat.Api |
| **US5**: List Empty | ConversationList | AgentChat.Model | FX_AgentChat.Conversations.ListEmpty | `GET /api/assistant/conversations` | T052 | T059 | Test.AgentChat.Api |
| **US5**: Load History | AssistantChatPanel | AgentChat.Model | FX_AgentChat.History.Valid | `GET /api/assistant/history` | T054 | T019 | Test.AgentChat.Api |
| **US5**: Delete/Archive | ConversationList | AgentChat.Model | FX_AgentChat.Delete.Valid | `DELETE /api/assistant/conversations/{id}` | T055 | T058 | Test.AgentChat.Api |
| **US5**: Delete Not Owner | ConversationList | — | FX_AgentChat.Delete.NotOwner | `DELETE /api/assistant/conversations/{id}` | T055 | — | Test.AgentChat.Api |
| **US6**: File Upload | AssistantChatPanel | AgentChat.Model | FX_AgentChat.Document.ParsePdf.Valid | `submit("/chat", {text, files})` | T065, T066 | T068, T069 | Test.AgentChat.Document |
| **US6**: PDF Encrypted | — | AgentChat.Document | FX_AgentChat.Document.ParsePdf.Encrypted | — | T065 | — | Test.AgentChat.Document |
| **US6**: XLSX Password | — | AgentChat.Document | FX_AgentChat.Document.ParseXlsx.PasswordProtected | — | T065 | — | Test.AgentChat.Document |
| **—**: Service Auth | — | — | FX_AgentChat.ServiceToken.Valid | `POST /api/auth/service-token` | T080 | — | Test.AgentChat.Api |
| **—**: Rejected Paths | — | AgentChat.Model | FX_AgentChat.Model.RejectedPath | — | — | — | Test.AgentChat.Model.RejectedPaths |
## Impact Analysis Quick Reference
| If you change... | These fixtures verify it | These tests verify it | These screens depend |
|-----------------|------------------------|----------------------|---------------------|
| `AgentChat.Model.sendMessage()` | FX_AgentChat.Model.SendMessage.Valid, ResumeConfirm, ResumeDeny, CancelGeneration | Test.AgentChat.Model | AssistantChatPanel, /agent |
| `POST /api/assistant/conversations` | FX_AgentChat.Conversations.ListValid, ListEmpty | Test.AgentChat.Api | ConversationList, AssistantChatPanel |
| `GET /api/assistant/history` | FX_AgentChat.History.Valid, NotFound | Test.AgentChat.Api | AssistantChatPanel |
| `DELETE /api/assistant/conversations/{id}` | FX_AgentChat.Delete.Valid, NotOwner, NotFound | Test.AgentChat.Api | ConversationList |
| `@tool` functions in tools.py | — | Test.AgentChat.Tools | Gradio agent handler |
| Document parser | FX_AgentChat.Document.ParsePdf.Valid, ParseXlsx.Valid | Test.AgentChat.Document | Gradio agent handler |
| `@gradio/client` transport | FX_AgentChat.Model.RejectedPath (WebSocket guardrail) | Test.AgentChat.Model.RejectedPaths | AssistantChatPanel |
| `POST /api/auth/service-token` | FX_AgentChat.ServiceToken.Valid, InvalidSecret | Test.AgentChat.Api | Gradio container startup |
## Fixture Count
| Category | Count |
|----------|:-----:|
| API fixtures | 14 |
| Model fixtures | 5 |
| **Total** | **19** |
#endregion Traceability