Files
kbqa-system/docs/superpowers/plans/2026-07-13-kbqa-mvp-implementation.md
T
2026-07-13 11:18:02 +08:00

32 KiB
Raw Blame History

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

.
├── .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
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
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:

@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.

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
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
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.16.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:

@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
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:

@dataclass(frozen=True, slots=True)
class MessageSourceInput:
    document_id: str
    chunk_id: str
    citation_label: str
    score: float
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
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
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

@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:

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.

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.

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
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
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

@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:

@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
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

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
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<T>, Document, Chunk, Session, Message, Source, ApiError, and a discriminated SSE union for status/sources/token/done/error.

  • Step 4: Implement clients

apiRequest<T>() 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
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
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
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
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
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
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
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.