# KBQA MVP Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build the approved local single-user dual-agent knowledge-base application from document upload through real-model RAG, SSE chat, persistent history, and browser-verified cleanup/recovery. **Architecture:** A React three-column workbench talks to one FastAPI process over REST and POST SSE. SQLite is the business source of truth, a durable SQLite-backed single-consumer queue drives indexing, Milvus Lite stores vectors, and LangChain 1.x `create_agent` composes a main agent with a research subagent exposed as a tool. **Tech Stack:** Python 3.12, uv, FastAPI, SQLAlchemy 2.x async, aiosqlite, Alembic, LangChain 1.x, langchain-openai, PyMilvus/Milvus Lite, LangSmith, React, TypeScript, Vite, React Router, zustand, Tailwind CSS, shadcn/ui, npm, pytest, agent-browser. ## Global Constraints - SQLite is the only relational database; do not add PostgreSQL, psycopg, JSONB, Redis, Celery, or another queue service. - Milvus Lite uses one local file and stores no duplicate chunk正文数据. - Use `langchain.agents.create_agent`; do not use `langgraph.prebuilt.create_react_agent` or LangChain 0.3.x APIs. - Both agents use `qwen3.5-flash`; embeddings use `text-embedding-v4` with exactly 1024 dimensions. - Strict mode: every knowledge answer researches first; empty evidence returns a deterministic insufficiency response. - Store user-visible sessions/messages/sources in SQLite; internal agent/tool messages stay in LangSmith. - Index asynchronously with a durable SQLite job; restore queued/running/deleting work at startup. - Run exactly one Uvicorn worker. - Support only UTF-8 Markdown/TXT and text-extractable PDF; do not add OCR. - Do not use TDD. Implement first, then smoke-check; add automated tests only in Task 12. - Do not mock Chat or Embedding models. Model-path tests belong to the explicitly enabled live suite. - Use `uv` for Python and `npm` for Node.js. - Use the user-authorized credentials from ignored `1.md` only for local real calls; map them into ignored `.env`, never print/commit them, and recommend rotation after acceptance. - Use the ignored root `test.txt` as the user-provided large real knowledge file through `KBQA_LIVE_TEST_FILE`. - Test the frontend with `agent-browser` against real Vite and FastAPI processes. --- ## Planned File Structure ```text . ├── .python-version ├── .env.example ├── README.md ├── backend/ │ ├── pyproject.toml │ ├── uv.lock │ ├── alembic.ini │ ├── alembic/{env.py,versions/0001_initial.py} │ ├── src/kbqa/ │ │ ├── {main,config,database,lifecycle,models}.py │ │ ├── api/{errors,middleware,router}.py │ │ ├── health/routes.py │ │ ├── documents/{models,schemas,repository,service,routes}.py │ │ ├── indexing/{loaders,splitter,worker}.py │ │ ├── rag/{types,embeddings,vector_store,retriever}.py │ │ ├── chat/{models,schemas,repository,sse,service,routes}.py │ │ └── agents/{context,llm,prompts,tools,middleware,factory}.py │ └── tests/{fixtures,unit,integration,live}/ └── web/ ├── package.json ├── vite.config.ts ├── src/{main.tsx,index.css} ├── src/app/{router,AppLayout}.tsx ├── src/lib/{api,types,sse}.ts ├── src/components/ui/ └── src/features/{documents,chat}/ ``` --- ### Task 1: Secure bootstrap and dependency lock **Files:** - Create: `.python-version`, `.env.example`, `README.md` - Create: `backend/pyproject.toml`, `backend/src/kbqa/__init__.py` - Modify: `.gitignore` - Keep local and ignored: `1.md`, `test.txt` **Interfaces:** - Produces: Python 3.12 environment and environment-variable contract used by every later task. - [ ] **Step 1: Prepare authorized local credentials without exposing them** Confirm `git check-ignore -v .env 1.md test.txt` reports all paths ignored. Copy `1.md` to mode-600 `.env` without printing it, rename `DASHSCOPE_API_KEY`/`DASHSCOPE_BASE_URL`/`MODEL` keys to their `KBQA_` application names, retain standard `LANGSMITH_` names, and add `KBQA_LIVE_TEST_FILE` pointing to the absolute root `test.txt`. Never show the resulting file in tool output. - [ ] **Step 2: Pin Python and define dependencies** Create `.python-version` containing `3.12`. Create `backend/pyproject.toml` with Python `>=3.12,<3.13` and these bounded dependencies: `fastapi`, `uvicorn[standard]`, `sqlalchemy[asyncio]>=2,<3`, `aiosqlite`, `alembic`, `pydantic-settings`, `python-multipart`, `pypdf`, `langchain>=1,<2`, `langchain-openai>=1,<2`, `langchain-text-splitters>=1,<2`, and `pymilvus[milvus-lite]>=2.6,<3`. Add dev dependencies `httpx`, `pytest`, `pytest-asyncio`, `pytest-timeout`, and `ruff`. Configure Hatchling for `src/kbqa`, pytest marker `live`, Ruff Python 3.12 and line length 100. - [ ] **Step 3: Define safe configuration examples** Create `.env.example` with every variable from Spec section 16, including empty `KBQA_LIVE_TEST_FILE`. Leave keys empty; use `KBQA_` for application config and standard `LANGSMITH_` names for LangSmith. - [ ] **Step 4: Lock and synchronize** ```bash uv lock --project backend uv sync --project backend --all-groups uv run --project backend python -c "import langchain; print(langchain.__version__)" ``` Expected: dependency resolution succeeds and LangChain reports `1.x`. - [ ] **Step 5: Document local commands** `README.md` must cover setup, migration, backend/frontend startup, ordinary/live checks, credential rotation, single-worker constraint, and state that the approved Spec is authoritative while `architecture.md` is reference-only. - [ ] **Step 6: Commit** ```bash git add .python-version .env.example README.md .gitignore backend git commit -m "chore: bootstrap kbqa workspace" ``` --- ### Task 2: FastAPI foundation, settings, database, and health **Files:** - Create: `backend/src/kbqa/config.py`, `database.py`, `lifecycle.py`, `main.py` - Create: `backend/src/kbqa/api/errors.py`, `middleware.py`, `router.py` - Create: `backend/src/kbqa/health/routes.py` **Interfaces:** - Produces: `Settings`, `get_settings()`, `Database`, `get_session()`, `AppError`, `api_router`, `create_app()`. - [ ] **Step 1: Implement typed settings** Define all Spec defaults with `SettingsConfigDict(env_file=".env", env_prefix="KBQA_", extra="ignore")` and: ```python @lru_cache def get_settings() -> Settings: ... def validate_live_settings(settings: Settings) -> None: ... ``` Only model paths call `validate_live_settings`; app import and `/health` must not require credentials. - [ ] **Step 2: Implement SQLite async infrastructure** Create `Base(DeclarativeBase)` and a `Database` holding `AsyncEngine` plus `async_sessionmaker[AsyncSession]`. A SQLAlchemy connect listener executes `PRAGMA foreign_keys=ON`, `PRAGMA journal_mode=WAL`, and `PRAGMA busy_timeout=5000`. ```python class Database: def __init__(self, url: str) -> None: ... async def dispose(self) -> None: ... async def get_session(request: Request) -> AsyncIterator[AsyncSession]: ... ``` - [ ] **Step 3: Implement safe errors and request IDs** Define `AppError(code, message, status_code, retryable=False)`. Handle it, `RequestValidationError`, and unexpected exceptions using the approved JSON envelope. Middleware accepts/generates `X-Request-ID`, stores it on `request.state`, and returns it in headers. - [ ] **Step 4: Implement app factory and health** `create_app()` builds resources in lifespan, configures only explicit localhost CORS origins, includes `/api/v1`, and exposes `/health`. Before Task 4 exists, health may use a minimal `MilvusClient(uri).list_collections()` probe; Task 4 replaces that probe with `MilvusVectorStore.ping()`. It never calls Chat/Embedding. - [ ] **Step 5: Smoke verify** ```bash uv run --project backend ruff check backend/src uv run --project backend uvicorn kbqa.main:app --app-dir backend/src --port 8000 curl http://127.0.0.1:8000/health ``` Expected: lint passes and health reports `app`, `sqlite`, and `milvus` states. - [ ] **Step 6: Commit** ```bash git add backend/src/kbqa git commit -m "feat: add backend application foundation" ``` --- ### Task 3: SQLite models, repositories, and initial migration **Files:** - Create: `backend/src/kbqa/documents/models.py`, `repository.py` - Create: `backend/src/kbqa/chat/models.py`, `repository.py` - Create: `backend/src/kbqa/models.py` - Create: `backend/alembic.ini`, `backend/alembic/env.py`, `backend/alembic/versions/0001_initial.py` **Interfaces:** - Produces ORM classes `Document`, `IndexJob`, `Chunk`, `ChatSession`, `Message`, `MessageSource` and their repositories. - [ ] **Step 1: Implement exact ORM schema** Use SQLAlchemy 2.x `Mapped[...]`. Reproduce every field, check/unique constraint, cascade, and index from Spec sections 6.1–6.6. UUID strings are generated in Python; timestamps use one UTC helper. - [ ] **Step 2: Implement document repository** Define the input contract in `documents/repository.py`: ```python @dataclass(frozen=True, slots=True) class StoredUpload: document_id: str filename: str stored_path: str file_type: str mime_type: str file_size: int sha256: str ``` ```python class DocumentRepository: async def create_pending(self, upload: StoredUpload) -> Document: ... async def page(self, status: str | None, offset: int, limit: int) -> tuple[list[Document], int]: ... async def get(self, document_id: str) -> Document | None: ... async def page_chunks(self, document_id: str, offset: int, limit: int) -> tuple[list[Chunk], int]: ... async def claim_job(self, job_id: str) -> IndexJob | None: ... async def mark_indexed(self, document_id: str, chunk_count: int) -> None: ... async def mark_failed(self, document_id: str, code: str, message: str) -> None: ... async def requeue_unfinished(self) -> list[str]: ... ``` `claim_job` atomically changes queued→running and pending→indexing. - [ ] **Step 3: Implement chat repository** Define the persistence-only source input in `chat/repository.py`: ```python @dataclass(frozen=True, slots=True) class MessageSourceInput: document_id: str chunk_id: str citation_label: str score: float ``` ```python class ChatRepository: async def create_session(self, title: str | None) -> ChatSession: ... async def page_sessions(self, offset: int, limit: int) -> tuple[list[ChatSession], int]: ... async def load_recent_messages(self, session_id: str, limit: int) -> list[Message]: ... async def page_messages(self, session_id: str, offset: int, limit: int) -> tuple[list[Message], int]: ... async def begin_turn(self, session_id: str, query: str) -> tuple[Message, Message]: ... async def complete_answer(self, message_id: str, content: str, sources: list[MessageSourceInput]) -> None: ... async def fail_answer(self, message_id: str, error_code: str) -> None: ... async def delete_session(self, session_id: str) -> bool: ... ``` `begin_turn` allocates consecutive order indexes and sets first-question title in one transaction. - [ ] **Step 4: Configure Alembic** Use async migration setup, import `kbqa.models`, and write `0001_initial.py` explicitly so names/cascades are reviewable. - [ ] **Step 5: Verify migration** ```bash uv run --project backend alembic -c backend/alembic.ini upgrade head uv run --project backend python -c "import sqlite3; db=sqlite3.connect('data/kbqa.sqlite3'); print(sorted(r[0] for r in db.execute(\"select name from sqlite_master where type='table'\")))" ``` Expected: six application tables plus `alembic_version`. - [ ] **Step 6: Commit** ```bash git add backend/alembic.ini backend/alembic backend/src/kbqa git commit -m "feat: add sqlite persistence model" ``` --- ### Task 4: RAG storage and durable document indexing **Files:** - Create: `backend/src/kbqa/rag/{types,embeddings,vector_store,retriever}.py` - Create: `backend/src/kbqa/indexing/{loaders,splitter,worker}.py` - Create: `backend/src/kbqa/documents/service.py` - Modify: `backend/src/kbqa/lifecycle.py` **Interfaces:** - Produces: `SourceCandidate`, `PreparedChunk`, `EmbeddingProvider`, `MilvusVectorStore`, `KnowledgeRetriever`, `DocumentService`, `IndexWorker`. - [ ] **Step 1: Define RAG contracts** ```python @dataclass(frozen=True, slots=True) class SourceCandidate: chunk_id: str document_id: str filename: str order_index: int page_number: int | None content: str score: float @dataclass(frozen=True, slots=True) class PreparedChunk: id: str document_id: str order_index: int page_number: int | None content: str @dataclass(frozen=True, slots=True) class VectorHit: chunk_id: str document_id: str order_index: int score: float ``` - [ ] **Step 2: Implement loaders and splitter** Validate `%PDF-`, UTF-8 text, and non-empty extracted PDF text. Preserve page numbers. Split with `RecursiveCharacterTextSplitter(chunk_size, chunk_overlap, length_function=len)` and generate deterministic UUIDv5 IDs from `document_id:order_index`. - [ ] **Step 3: Implement real embedding provider** Wrap `OpenAIEmbeddings` with DashScope base URL/key, `text-embedding-v4`, and `dimensions=1024`: ```python class EmbeddingProvider: async def embed_documents(self, texts: list[str]) -> list[list[float]]: ... async def embed_query(self, text: str) -> list[float]: ... ``` Validate every returned vector has length 1024 and map upstream failures to stable `AppError` codes. - [ ] **Step 4: Implement Milvus Lite wrapper** Create `kbqa_chunks` with VARCHAR `chunk_id` primary key, VARCHAR `document_id`, INT64 `order_index`, FLOAT_VECTOR `vector`, AUTOINDEX, and COSINE. ```python class MilvusVectorStore: def ensure_collection(self) -> None: ... def upsert(self, chunks: list[PreparedChunk], vectors: list[list[float]]) -> None: ... def search(self, vector: list[float], limit: int) -> list[VectorHit]: ... def delete_document(self, document_id: str) -> None: ... def ping(self) -> None: ... ``` Run blocking PyMilvus calls with `asyncio.to_thread` at service boundaries. - [ ] **Step 5: Implement retriever** `KnowledgeRetriever.retrieve(query, top_k)` embeds the query, requests extra Milvus candidates, batch-loads SQLite chunks joined to indexed documents, preserves ranking, and returns at most `top_k` sources. - [ ] **Step 6: Implement durable worker** `IndexWorker.start()` requeues unfinished jobs and starts one task. `_process()` claims, clears old chunks/vectors, loads/splits, calls real embeddings, writes chunks, upserts vectors, and marks success. Catch ordinary exceptions to mark failed; re-raise `CancelledError`. `stop()` cancels and awaits the worker. - [ ] **Step 7: Implement upload/deletion service** `DocumentService.upload()` validates 20 MiB, extension/MIME/content, atomically writes `data/raw/{uuid}.{ext}`, creates document/job, enqueues, and returns. `delete()` marks deleting, idempotently removes vectors/file, then deletes SQLite rows. Startup recovery resumes deleting records and treats absent external data as success. - [ ] **Step 8: Verify without model calls and commit** Run Ruff and a script that creates the SQLite schema/Milvus collection but never calls embeddings. ```bash git add backend/src/kbqa git commit -m "feat: add durable document indexing pipeline" ``` --- ### Task 5: Document HTTP API **Files:** - Create: `backend/src/kbqa/documents/schemas.py`, `routes.py` - Modify: `backend/src/kbqa/api/router.py` **Interfaces:** - Produces: all Spec section 12.1 endpoints and stable `DocumentResponse`, `ChunkResponse`, and `Page[T]` shapes. - [ ] **Step 1: Define response schemas** Use Pydantic v2 `from_attributes=True`. `DocumentResponse` includes status/error/chunk count/timestamps; `ChunkResponse` includes order/page/content; `Page` includes `items`, `total`, `offset`, `limit`. - [ ] **Step 2: Implement all document routes** Implement upload/list/detail/chunks/retry/delete. Enforce `offset >= 0` and `1 <= limit <= 100`. Return 202 for upload/retry, 204 delete, 404 missing, 409 invalid retry state, 413 size, and 415 format. - [ ] **Step 3: Smoke-check non-model branches over real HTTP** Start Uvicorn and use curl to list, reject an unsupported extension, return 404 for a random UUID, and verify request IDs. Do not upload a valid file until Task 12 live verification. - [ ] **Step 4: Commit** ```bash git add backend/src/kbqa/documents backend/src/kbqa/api/router.py git commit -m "feat: expose document management api" ``` --- ### Task 6: Session persistence and history API **Files:** - Create: `backend/src/kbqa/chat/schemas.py`, `routes.py` - Modify: `backend/src/kbqa/api/router.py` **Interfaces:** - Produces: create/list/delete sessions and paginated message history; reserves `/chat/stream` for Task 8. - [ ] **Step 1: Define session/message/source schemas** `MessageResponse` includes `status`, safe `error_code`, and sources joined from live documents/chunks. Parse citation labels missing source rows into `deleted_source_labels` so the frontend can render “来源已删除” without deleted content. - [ ] **Step 2: Implement session routes** Implement `POST /sessions`, `GET /sessions`, `GET /sessions/{id}/messages`, and `DELETE /sessions/{id}` with exact 201/200/204/404 behavior. - [ ] **Step 3: Smoke-check restart persistence** Create a session over HTTP, list it, restart Uvicorn, list/fetch it again, delete it, then verify it no longer exists. - [ ] **Step 4: Commit** ```bash git add backend/src/kbqa/chat backend/src/kbqa/api/router.py git commit -m "feat: add persistent chat sessions" ``` --- ### Task 7: LangChain 1.x dual-agent core **Files:** - Create: `backend/src/kbqa/agents/{context,llm,prompts,tools,middleware,factory}.py` **Interfaces:** - Produces: `AgentBundle.main_agent`, research agent, `research` tool, `search_knowledge_base` tool, and typed custom stream payloads. - Consumes: `KnowledgeRetriever`, Settings, request/session metadata. - [ ] **Step 1: Define runtime context** ```python @dataclass(frozen=True, slots=True) class ResearchContext: retriever: KnowledgeRetriever request_id: str @dataclass(frozen=True, slots=True) class ChatRunContext: retriever: KnowledgeRetriever request_id: str session_id: str assistant_message_id: str ``` Tool artifacts are serialized `SourceCandidate` lists; never use a global mutable collector. - [ ] **Step 2: Implement model factories** Build `ChatOpenAI` with DashScope-compatible URL/key, `qwen3.5-flash`, async streaming, timeout, and bounded retries. Instantiate lazily, not at import. Add LangSmith `main-agent`/`research-agent` tags and request/session/message metadata per invocation. - [ ] **Step 3: Implement search tool** Use `@tool(response_format="content_and_artifact")`. Accept `query: str` plus injected `ToolRuntime[ResearchContext]`, call `retrieve(query, top_k=5)`, and return readable text plus complete source artifact. - [ ] **Step 4: Build research agent** Use `langchain.agents.create_agent(..., tools=[search_knowledge_base], context_schema=ResearchContext)`. Limit each run to three searches. Prompt it to use only tool evidence, reformulate weak searches, and return a research summary rather than a direct user answer. - [ ] **Step 5: Implement main-agent research tool** The `research(question, runtime)` tool invokes the research agent with `ainvoke`, extracts every search ToolMessage artifact, deduplicates by chunk ID keeping the highest score, caps at eight, emits custom `sources` or `no_evidence` through the stream writer, and returns the summary plus `[S1]…[S8]` evidence. - [ ] **Step 6: Enforce research-before-answer** Implement LangChain 1.x `AgentMiddleware.awrap_model_call`. Before a successful research ToolMessage exists in the current turn, override the model with required tool choice; afterward allow the final answer. Bound model calls and reject a second research call. - [ ] **Step 7: Build main agent** Use `create_agent` with only `research`, strict prompt, middleware, and `ChatRunContext`. The prompt allows only supplied labels and requires inline citations. Return both compiled graphs in: ```python @dataclass(frozen=True, slots=True) class AgentBundle: main_agent: CompiledStateGraph research_agent: CompiledStateGraph ``` - [ ] **Step 8: Static-check without calling the model** Inspect the compiled agents: main exposes only `research`, researcher exposes only `search_knowledge_base`, and repository search finds no `create_react_agent` import. - [ ] **Step 9: Commit** ```bash git add backend/src/kbqa/agents git commit -m "feat: add langchain dual-agent workflow" ``` --- ### Task 8: SSE chat orchestration and endpoint **Files:** - Create: `backend/src/kbqa/chat/sse.py`, `service.py` - Modify: `backend/src/kbqa/chat/routes.py`, `backend/src/kbqa/lifecycle.py` **Interfaces:** - Produces: `ChatService.stream(session_id, query, request_id)` yielding encoded SSE bytes and `POST /api/v1/chat/stream`. - [ ] **Step 1: Implement SSE encoding** ```python def encode_event(event: str, data: Mapping[str, object]) -> bytes: ... def encode_ping() -> bytes: ... ``` Serialize compact single-line UTF-8 JSON, terminate each event with two newlines, and encode heartbeat as `: ping\n\n`. - [ ] **Step 2: Implement per-session lock registry** Key process-local locks by session UUID. Reject concurrent generation with 409 and always release in `finally`. This is valid only because the app runs one process. - [ ] **Step 3: Implement stream lifecycle** Begin the SQLite turn, load 20 recent messages, invoke `main_agent.astream` with `stream_mode=["messages", "custom", "updates"]` and `version="v2"`, then map: - immediate `status: researching`; - source artifact → `sources`, then `status: answering`; - empty evidence → close agent stream and emit deterministic insufficiency token; - final main-agent text chunks after answering begins → `token`; - 15-second inactivity → ping; - validated/persisted answer → `done`. Only labels in the source set are valid. Unknown labels produce `INVALID_CITATION`, mark failed, and emit `error` instead of `done`. - [ ] **Step 4: Handle disconnects and failures** Propagate `CancelledError`, close the LangChain async generator, mark assistant failed, and never persist partial content. Convert DashScope timeout/upstream errors to safe SSE errors. Persist success before `done`. - [ ] **Step 5: Expose POST route** Validate non-empty bounded query. Return `StreamingResponse` with `text/event-stream`, `Cache-Control: no-cache`, `X-Accel-Buffering: no`, and request ID. - [ ] **Step 6: Verify protocol without model calls** Run Ruff and feed Chinese token/source/error/ping payloads through the encoder to verify exact framing. - [ ] **Step 7: Commit** ```bash git add backend/src/kbqa/chat backend/src/kbqa/lifecycle.py git commit -m "feat: stream dual-agent answers over sse" ``` --- ### Task 9: React workbench foundation **Files:** - Create: Vite React TypeScript project under `web/` - Create: `web/src/app/router.tsx`, `AppLayout.tsx` - Create: `web/src/lib/api.ts`, `types.ts`, `sse.ts` - Create/Generate: `web/src/components/ui/*` - Modify: `web/vite.config.ts`, `web/src/index.css` **Interfaces:** - Produces: typed REST client, incremental POST-SSE parser, routes, responsive shell, and shared shadcn primitives. - [ ] **Step 1: Scaffold and install** Use `npm create vite@latest web -- --template react-ts`. Add React Router, zustand, lucide-react, Tailwind CSS, shadcn/ui dependencies, ESLint, Vitest, and button/dialog-drawer/badge/textarea/scroll-area/tooltip/skeleton primitives. Commit `package-lock.json`. - [ ] **Step 2: Configure proxy and visual tokens** Proxy `/api` and `/health` to `http://127.0.0.1:8000`. Implement the approved deep-green, warm-orange, off-white palette as CSS variables and use a distinct reading style for evidence text. - [ ] **Step 3: Define shared types** Mirror backend DTOs: `Page`, `Document`, `Chunk`, `Session`, `Message`, `Source`, `ApiError`, and a discriminated SSE union for `status/sources/token/done/error`. - [ ] **Step 4: Implement clients** `apiRequest()` parses unified errors and request ID. `streamSSE()` uses fetch POST + `ReadableStream`, retains incomplete UTF-8/lines across chunks, supports LF/CRLF, dispatches complete events, and accepts `AbortSignal`. - [ ] **Step 5: Implement routes and shell** Create `/chat`, `/chat/:sessionId`, `/documents`, `/documents/:documentId`. At ≥1100px show all columns; below 1100px evidence is a drawer; below 768px navigation is also a drawer. - [ ] **Step 6: Static-check and commit** ```bash npm --prefix web run lint npm --prefix web run build git add web git commit -m "feat: add responsive react workbench" ``` --- ### Task 10: Document management frontend **Files:** - Create: `web/src/features/documents/api.ts`, `useDocuments.ts` - Create: `web/src/features/documents/DocumentsPage.tsx`, `DocumentList.tsx`, `DocumentDetail.tsx`, `UploadDocument.tsx` - Modify: `web/src/app/router.tsx`, `web/src/app/AppLayout.tsx` **Interfaces:** - Produces: upload, list/filter, two-second active-status polling, chunk pagination, retry, deletion confirmation, and selected-document evidence panel. - [ ] **Step 1: Implement typed document API** Expose `uploadDocument`, `listDocuments`, `getDocument`, `listChunks`, `retryDocument`, and `deleteDocument` with exact backend paths. - [ ] **Step 2: Implement polling state** Poll every two seconds only while a visible document is pending/indexing/deleting; stop on unmount and terminal states. Preserve selected document route across refresh. - [ ] **Step 3: Build approved document UI** Left shows navigation and status counts. Center shows upload, filters, filename/type/chunk count/status and retry/delete actions. Right shows metadata and paginated chunks. Never display stack traces. - [ ] **Step 4: Smoke-check real non-model branches** Run both services; reject an unsupported file, show empty state, navigate to a missing document, and check responsive drawers without valid upload/model calls. - [ ] **Step 5: Check and commit** ```bash npm --prefix web run lint npm --prefix web run build git add web/src git commit -m "feat: add document management workspace" ``` --- ### Task 11: Chat, streaming, history, and evidence frontend **Files:** - Create: `web/src/features/chat/api.ts`, `store.ts`, `useChatStream.ts` - Create: `web/src/features/chat/ChatPage.tsx`, `SessionList.tsx`, `MessageList.tsx`, `ChatComposer.tsx`, `EvidencePanel.tsx` - Modify: `web/src/app/router.tsx`, `web/src/app/AppLayout.tsx` **Interfaces:** - Produces: persistent session navigation, one active stream, incremental answer, abort/retry, source selection, deleted-source labels, and history reload. - [ ] **Step 1: Implement session API and store** Expose create/list/delete sessions and message history. Store only current session ID, transient phase/text/sources/error, and selected evidence; server responses remain authoritative. - [ ] **Step 2: Implement stream hook** Create one `AbortController`; append tokens; replace sources on `sources`; update researching/answering phase; reload history on `done`; expose retry with original query on `error`; abort on route change/unmount. - [ ] **Step 3: Build session and message surfaces** Left lists sessions with new/delete. Center renders user/assistant messages, research phase, failed placeholders, citation chips, and fixed composer disabled during streaming. - [ ] **Step 4: Build evidence panel** Right lists current answer sources and expands one inline with filename, order/page, score, and full chunk. Deleted labels render muted “来源已删除” with no filename/content. - [ ] **Step 5: Implement empty/responsive states** With no indexed documents, guide to `/documents`. Evidence drawer must not destroy center conversation state on medium/mobile widths. - [ ] **Step 6: Check and commit** ```bash npm --prefix web run lint npm --prefix web run build git add web/src git commit -m "feat: add streaming chat and evidence workspace" ``` --- ### Task 12: Post-implementation tests, real-model API suite, and agent-browser E2E **Files:** - Create: `backend/tests/conftest.py` - Create: `backend/tests/fixtures/{knowledge.md,knowledge.txt,knowledge.pdf}` - Create: `backend/tests/unit/{test_file_validation,test_splitter,test_sse}.py` - Create: `backend/tests/integration/{test_sqlite_cascades,test_session_api}.py` - Create: `backend/tests/live/test_live_pipeline.py` - Create: `web/src/lib/sse.test.ts` - Create: `docs/testing/live-test-report.md`, `docs/testing/screenshots/` **Interfaces:** - Produces: ordinary checks, explicitly gated real API/model verification, browser evidence, and final acceptance report. - [ ] **Step 1: Add automated tests after implementation** Test validation, deterministic chunk IDs, SQLite constraints/cascades, session persistence, SSE framing, and frontend incremental parsing. Do not patch/fake Chat or Embedding; ordinary tests avoid those paths. - [ ] **Step 2: Create real knowledge fixtures** Commit UTF-8 Markdown/TXT and a real text-layer PDF with a small unique fact set suitable for a first question, pronoun-based follow-up, and absent-fact question. Generate the PDF once; do not synthesize it during the test. - [ ] **Step 3: Implement black-box live test** Skip unless `KBQA_RUN_LIVE_TESTS=1`. Start real Uvicorn on an available localhost port with isolated SQLite/Milvus/raw paths; use `httpx` over HTTP to: 1. upload all three real files; 2. poll until indexed and assert positive chunk counts; 3. create a session and parse real SSE; 4. assert researching→sources→answering→token→done; 5. assert sources refer to uploaded files/retrievable chunks; 6. ask a pronoun follow-up and verify persisted history; 7. ask an absent fact and verify insufficiency; 8. restart Uvicorn and verify persistence; 9. delete documents/sessions and verify files, SQLite relationships, and vectors are no longer retrievable. Assert structure/semantic boundaries, never exact model wording. - [ ] **Step 4: Run ordinary verification** ```bash uv run --project backend ruff format --check backend/src backend/tests uv run --project backend ruff check backend/src backend/tests uv run --project backend pytest backend/tests/unit backend/tests/integration -v npm --prefix web run lint npm --prefix web run test -- --run npm --prefix web run build ``` Expected: all exit 0 without contacting DashScope. - [ ] **Step 5: Run real live verification** ```bash KBQA_RUN_LIVE_TESTS=1 uv run --project backend pytest backend/tests/live/test_live_pipeline.py -v -s ``` Expected: real Chat/Embedding calls appear in LangSmith and the black-box flow passes. - [ ] **Step 6: Run agent-browser E2E** Start one Uvicorn worker and Vite. Use the `agent-browser` skill to upload, observe status, create a session, stream a real answer, expand evidence, follow up, reload history, test desktop/narrow widths, delete data, and capture screenshots. - [ ] **Step 7: Write final report** Record commands, versions, timestamps, LangSmith run IDs/links without secrets, screenshots, results, and non-blocking model variability in `docs/testing/live-test-report.md`. - [ ] **Step 8: Run forbidden-marker audit** ```bash rg -n "postgres|psycopg|JSONB|create_react_agent|langchain.*0\.3|sk-|lsv2_" --glob '!architecture.md' --glob '!docs/superpowers/**' --glob '!.env' ``` Expected: no forbidden implementation or committed secret. - [ ] **Step 9: Commit** ```bash git add backend/tests web/src/lib/sse.test.ts docs/testing git commit -m "test: verify real kbqa workflow" ``` --- ## Execution Checkpoints 1. After Task 3: review schema/migration before indexing depends on it. 2. After Task 5: review document state machine, recovery, and deletion. 3. After Task 8: review dual-agent/SSE strictness before frontend coupling. 4. After Task 11: review complete UI with ordinary checks passing. 5. After Task 12: review LangSmith traces, browser screenshots, cleanup, and acceptance report. ## Execution Notes - Execute with `superpowers:executing-plans`; `subagent-driven-development` is not available in this repository/session. - Use `1.md` credentials only under the user's explicit local-test authorization; never surface their values. Recommend rotation after acceptance. - Never run Uvicorn with more than one worker. - If an installed LangChain 1.x signature differs from a snippet, consult official docs/Context7, preserve this plan's interface/behavior, and record the verified signature in the implementing commit.