diff --git a/.kilocode/rules/specify-rules.md b/.kilocode/rules/specify-rules.md index e5745c83..05afb2d3 100644 --- a/.kilocode/rules/specify-rules.md +++ b/.kilocode/rules/specify-rules.md @@ -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) diff --git a/specs/033-gradio-agent-chat/fixtures/api/conversations_list_empty.json b/specs/033-gradio-agent-chat/fixtures/api/conversations_list_empty.json new file mode 100644 index 00000000..ee6c05db --- /dev/null +++ b/specs/033-gradio-agent-chat/fixtures/api/conversations_list_empty.json @@ -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}}} diff --git a/specs/033-gradio-agent-chat/fixtures/api/conversations_list_external_fail.json b/specs/033-gradio-agent-chat/fixtures/api/conversations_list_external_fail.json new file mode 100644 index 00000000..886f2eb5 --- /dev/null +++ b/specs/033-gradio-agent-chat/fixtures/api/conversations_list_external_fail.json @@ -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"}}}} diff --git a/specs/033-gradio-agent-chat/fixtures/api/conversations_list_invalid_page.json b/specs/033-gradio-agent-chat/fixtures/api/conversations_list_invalid_page.json new file mode 100644 index 00000000..2793799b --- /dev/null +++ b/specs/033-gradio-agent-chat/fixtures/api/conversations_list_invalid_page.json @@ -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"}}}} diff --git a/specs/033-gradio-agent-chat/fixtures/api/conversations_list_missing_auth.json b/specs/033-gradio-agent-chat/fixtures/api/conversations_list_missing_auth.json new file mode 100644 index 00000000..9416e1d9 --- /dev/null +++ b/specs/033-gradio-agent-chat/fixtures/api/conversations_list_missing_auth.json @@ -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"}}}} diff --git a/specs/033-gradio-agent-chat/fixtures/api/conversations_list_valid.json b/specs/033-gradio-agent-chat/fixtures/api/conversations_list_valid.json new file mode 100644 index 00000000..a102f566 --- /dev/null +++ b/specs/033-gradio-agent-chat/fixtures/api/conversations_list_valid.json @@ -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 + } + } +} diff --git a/specs/033-gradio-agent-chat/fixtures/api/history_not_found.json b/specs/033-gradio-agent-chat/fixtures/api/history_not_found.json new file mode 100644 index 00000000..5020c640 --- /dev/null +++ b/specs/033-gradio-agent-chat/fixtures/api/history_not_found.json @@ -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"}}}} diff --git a/specs/033-gradio-agent-chat/fixtures/api/history_valid.json b/specs/033-gradio-agent-chat/fixtures/api/history_valid.json new file mode 100644 index 00000000..3ab18da3 --- /dev/null +++ b/specs/033-gradio-agent-chat/fixtures/api/history_valid.json @@ -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"}}} diff --git a/specs/033-gradio-agent-chat/fixtures/api/service_token_invalid_secret.json b/specs/033-gradio-agent-chat/fixtures/api/service_token_invalid_secret.json new file mode 100644 index 00000000..a2d21165 --- /dev/null +++ b/specs/033-gradio-agent-chat/fixtures/api/service_token_invalid_secret.json @@ -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"}}}} diff --git a/specs/033-gradio-agent-chat/fixtures/api/service_token_valid.json b/specs/033-gradio-agent-chat/fixtures/api/service_token_valid.json new file mode 100644 index 00000000..50ac9a75 --- /dev/null +++ b/specs/033-gradio-agent-chat/fixtures/api/service_token_valid.json @@ -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"}}} diff --git a/specs/033-gradio-agent-chat/fixtures/manifest.md b/specs/033-gradio-agent-chat/fixtures/manifest.md new file mode 100644 index 00000000..f8023f74 --- /dev/null +++ b/specs/033-gradio-agent-chat/fixtures/manifest.md @@ -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 diff --git a/specs/033-gradio-agent-chat/fixtures/model/cancel_generation.json b/specs/033-gradio-agent-chat/fixtures/model/cancel_generation.json new file mode 100644 index 00000000..0e1550bd --- /dev/null +++ b/specs/033-gradio-agent-chat/fixtures/model/cancel_generation.json @@ -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}}} diff --git a/specs/033-gradio-agent-chat/fixtures/model/rejected_websocket.json b/specs/033-gradio-agent-chat/fixtures/model/rejected_websocket.json new file mode 100644 index 00000000..b4dfcefd --- /dev/null +++ b/specs/033-gradio-agent-chat/fixtures/model/rejected_websocket.json @@ -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"} diff --git a/specs/033-gradio-agent-chat/fixtures/model/resume_confirm.json b/specs/033-gradio-agent-chat/fixtures/model/resume_confirm.json new file mode 100644 index 00000000..97a66c09 --- /dev/null +++ b/specs/033-gradio-agent-chat/fixtures/model/resume_confirm.json @@ -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}}} diff --git a/specs/033-gradio-agent-chat/fixtures/model/resume_deny.json b/specs/033-gradio-agent-chat/fixtures/model/resume_deny.json new file mode 100644 index 00000000..2ac6474a --- /dev/null +++ b/specs/033-gradio-agent-chat/fixtures/model/resume_deny.json @@ -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}}} diff --git a/specs/033-gradio-agent-chat/fixtures/model/send_message_valid.json b/specs/033-gradio-agent-chat/fixtures/model/send_message_valid.json new file mode 100644 index 00000000..d6e37b3f --- /dev/null +++ b/specs/033-gradio-agent-chat/fixtures/model/send_message_valid.json @@ -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":[]}}} diff --git a/specs/033-gradio-agent-chat/plan.md b/specs/033-gradio-agent-chat/plan.md index fbab1b2c..036d3a1b 100644 --- a/specs/033-gradio-agent-chat/plan.md +++ b/specs/033-gradio-agent-chat/plan.md @@ -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**: Gradio≥5.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) diff --git a/specs/033-gradio-agent-chat/tasks.md b/specs/033-gradio-agent-chat/tasks.md index afb87a11..19806cfe 100644 --- a/specs/033-gradio-agent-chat/tasks.md +++ b/specs/033-gradio-agent-chat/tasks.md @@ -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 diff --git a/specs/033-gradio-agent-chat/traceability.md b/specs/033-gradio-agent-chat/traceability.md new file mode 100644 index 00000000..25ac040c --- /dev/null +++ b/specs/033-gradio-agent-chat/traceability.md @@ -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