diff --git a/docs/superpowers/plans/2026-07-03-stage-2-knowledge-vector-retrieval.md b/docs/superpowers/plans/2026-07-03-stage-2-knowledge-vector-retrieval.md new file mode 100644 index 0000000..4e2f2a4 --- /dev/null +++ b/docs/superpowers/plans/2026-07-03-stage-2-knowledge-vector-retrieval.md @@ -0,0 +1,1142 @@ +# Stage 2 Knowledge Vector Retrieval Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build the first usable knowledge base layer: knowledge bases, documents, structured knowledge items, chunks, deterministic local vectors, and retrieval APIs/UI that later Agents can consume. + +**Architecture:** Add a new Django `apps.knowledge` module with normalized knowledge models and a service layer for parsing, chunking, embedding, and retrieval. Use a deterministic local embedding function in Stage 2 so retrieval contracts are stable before Stage 3 model gateway integration replaces the embedding provider. Add React shadcn-based knowledge pages using the existing `App*` component layer and Chinese UI text. + +**Tech Stack:** Python 3.12, Django, Django Ninja, PostgreSQL, JSON fields for MVP embedding vectors, pytest, ruff, React, Vite, TypeScript, Tailwind CSS, shadcn/ui, lucide-react. + +--- + +## Scope + +Stage 2 implements the knowledge layer only. It does not call real LLMs, does not configure model providers, and does not run autonomous Agents. The retrieval API returns citable chunks from manual knowledge items, uploaded document text, and published reusable solutions. + +## File Structure + +Backend: + +- Create: `backend/apps/knowledge/__init__.py` +- Create: `backend/apps/knowledge/apps.py` +- Create: `backend/apps/knowledge/models.py` +- Create: `backend/apps/knowledge/schemas.py` +- Create: `backend/apps/knowledge/services.py` +- Create: `backend/apps/knowledge/api.py` +- Create: `backend/apps/knowledge/tests/test_knowledge_api.py` +- Create: `backend/apps/knowledge/tests/test_retrieval.py` +- Modify: `backend/config/settings/base.py` +- Modify: `backend/config/urls.py` + +Frontend: + +- Create: `frontend/src/features/knowledge/types.ts` +- Create: `frontend/src/features/knowledge/api.ts` +- Create: `frontend/src/features/knowledge/KnowledgeBaseListPage.tsx` +- Create: `frontend/src/features/knowledge/KnowledgeItemEditorPage.tsx` +- Create: `frontend/src/features/knowledge/KnowledgeSearchPage.tsx` +- Modify: `frontend/src/app/router.tsx` +- Modify: `frontend/src/components/app/AppSidebar.tsx` + +Docs: + +- Modify: `README.md` +- Modify: `docs/frontend/design-system.md` + +--- + +### Task 1: Backend Knowledge Models And CRUD API + +**Files:** +- Create: `backend/apps/knowledge/models.py` +- Create: `backend/apps/knowledge/schemas.py` +- Create: `backend/apps/knowledge/api.py` +- Create: `backend/apps/knowledge/apps.py` +- Create: `backend/apps/knowledge/tests/test_knowledge_api.py` +- Modify: `backend/config/settings/base.py` +- Modify: `backend/config/urls.py` + +- [ ] **Step 1: Write failing CRUD tests** + +Create `backend/apps/knowledge/tests/test_knowledge_api.py`: + +```python +from apps.knowledge.models import KnowledgeBase, KnowledgeChunk, KnowledgeDocument, KnowledgeItem + + +def test_create_and_list_knowledge_base(admin_client, db): + response = admin_client.post( + "/api/knowledge/bases", + data={ + "name": "税务采集知识库", + "description": "税局采集、日志和常见问题", + "scope": "数据采集", + }, + content_type="application/json", + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["name"] == "税务采集知识库" + assert payload["enabled"] is True + + response = admin_client.get("/api/knowledge/bases") + + assert response.status_code == 200 + assert response.json()[0]["name"] == "税务采集知识库" + + +def test_create_knowledge_item_builds_chunks(admin_client, db): + base = KnowledgeBase.objects.create(name="税务采集知识库", scope="数据采集") + + response = admin_client.post( + "/api/knowledge/items", + data={ + "knowledge_base_id": base.id, + "title": "税局采集进度查询", + "problem": "客户询问采集进度长时间不更新", + "applicable_scope": "生产环境税局采集", + "error_code": "", + "root_cause": "税局服务响应不稳定", + "troubleshooting_steps": "查询 Python 日志并确认采集任务状态", + "solution": "告知客户税局不稳定,任务仍在采集中,稍后复查。", + "risk_note": "不要重复触发采集任务。", + "status": "published", + }, + content_type="application/json", + ) + + assert response.status_code == 200 + item = KnowledgeItem.objects.get(id=response.json()["id"]) + assert item.status == "published" + assert KnowledgeChunk.objects.filter(item=item).count() >= 1 + + +def test_ingest_document_text_creates_document_and_chunks(admin_client, db): + base = KnowledgeBase.objects.create(name="日志知识库", scope="技术支持") + + response = admin_client.post( + "/api/knowledge/documents", + data={ + "knowledge_base_id": base.id, + "file_name": "tax-log.md", + "file_type": "markdown", + "content": "税局接口不稳定时,采集任务会停留在处理中。需要查询 Python 日志确认。", + }, + content_type="application/json", + ) + + assert response.status_code == 200 + document = KnowledgeDocument.objects.get(id=response.json()["id"]) + assert document.parse_status == "success" + assert KnowledgeChunk.objects.filter(document=document).count() >= 1 +``` + +- [ ] **Step 2: Run tests to verify RED** + +Run: + +```powershell +python -m pytest backend\apps\knowledge\tests\test_knowledge_api.py -v +``` + +Expected: fails because `apps.knowledge` and `/api/knowledge/*` routes do not exist. + +- [ ] **Step 3: Create models** + +Create `backend/apps/knowledge/models.py`: + +```python +from django.conf import settings +from django.db import models + + +class KnowledgeBase(models.Model): + name = models.CharField(max_length=160) + description = models.TextField(blank=True) + scope = models.CharField(max_length=120, blank=True) + enabled = models.BooleanField(default=True) + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["-updated_at"] + + def __str__(self) -> str: + return self.name + + +class KnowledgeDocument(models.Model): + knowledge_base = models.ForeignKey(KnowledgeBase, on_delete=models.CASCADE, related_name="documents") + file_name = models.CharField(max_length=255) + file_type = models.CharField(max_length=80, blank=True) + storage_path = models.CharField(max_length=500, blank=True) + content_text = models.TextField(blank=True) + parse_status = models.CharField(max_length=40, default="pending") + parse_error = models.TextField(blank=True) + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["-created_at"] + + +class KnowledgeItem(models.Model): + knowledge_base = models.ForeignKey(KnowledgeBase, on_delete=models.CASCADE, related_name="items") + title = models.CharField(max_length=200) + problem = models.TextField() + applicable_scope = models.TextField(blank=True) + error_code = models.CharField(max_length=120, blank=True) + root_cause = models.TextField(blank=True) + troubleshooting_steps = models.TextField(blank=True) + solution = models.TextField() + risk_note = models.TextField(blank=True) + status = models.CharField(max_length=40, default="draft") + source_type = models.CharField(max_length=40, default="manual") + source_ticket_id = models.BigIntegerField(null=True, blank=True) + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="created_knowledge_items", + ) + reviewed_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="reviewed_knowledge_items", + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["-updated_at"] + + +class KnowledgeChunk(models.Model): + knowledge_base = models.ForeignKey(KnowledgeBase, on_delete=models.CASCADE, related_name="chunks") + document = models.ForeignKey( + KnowledgeDocument, + null=True, + blank=True, + on_delete=models.CASCADE, + related_name="chunks", + ) + item = models.ForeignKey( + KnowledgeItem, + null=True, + blank=True, + on_delete=models.CASCADE, + related_name="chunks", + ) + chunk_index = models.PositiveIntegerField(default=0) + content = models.TextField() + embedding = models.JSONField(default=list, blank=True) + embedding_model = models.CharField(max_length=120, default="local-hash-v1") + metadata = models.JSONField(default=dict, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ["knowledge_base_id", "document_id", "item_id", "chunk_index"] + indexes = [ + models.Index(fields=["knowledge_base", "chunk_index"]), + ] +``` + +- [ ] **Step 4: Register app and router** + +Create `backend/apps/knowledge/apps.py`: + +```python +from django.apps import AppConfig + + +class KnowledgeConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.knowledge" +``` + +Modify `backend/config/settings/base.py`: + +```python +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "corsheaders", + "apps.accounts", + "apps.audit", + "apps.system", + "apps.tickets", + "apps.solutions", + "apps.knowledge", +] +``` + +Modify `backend/config/urls.py`: + +```python +from apps.knowledge.api import router as knowledge_router + +api.add_router("/knowledge", knowledge_router) +``` + +- [ ] **Step 5: Add schemas** + +Create `backend/apps/knowledge/schemas.py`: + +```python +from datetime import datetime + +from ninja import Schema + + +class KnowledgeBaseIn(Schema): + name: str + description: str = "" + scope: str = "" + enabled: bool = True + + +class KnowledgeBaseOut(Schema): + id: int + name: str + description: str + scope: str + enabled: bool + created_at: datetime + updated_at: datetime + + +class KnowledgeDocumentIn(Schema): + knowledge_base_id: int + file_name: str + file_type: str = "" + content: str + + +class KnowledgeDocumentOut(Schema): + id: int + knowledge_base_id: int + file_name: str + file_type: str + parse_status: str + parse_error: str + created_at: datetime + + +class KnowledgeItemIn(Schema): + knowledge_base_id: int + title: str + problem: str + applicable_scope: str = "" + error_code: str = "" + root_cause: str = "" + troubleshooting_steps: str = "" + solution: str + risk_note: str = "" + status: str = "draft" + + +class KnowledgeItemOut(Schema): + id: int + knowledge_base_id: int + title: str + problem: str + applicable_scope: str + error_code: str + root_cause: str + troubleshooting_steps: str + solution: str + risk_note: str + status: str + source_type: str + created_at: datetime + updated_at: datetime +``` + +- [ ] **Step 6: Implement CRUD API** + +Create `backend/apps/knowledge/api.py`: + +```python +from ninja import Router + +from apps.accounts.api import require_permission +from apps.knowledge.models import KnowledgeBase, KnowledgeDocument, KnowledgeItem +from apps.knowledge.schemas import ( + KnowledgeBaseIn, + KnowledgeBaseOut, + KnowledgeDocumentIn, + KnowledgeDocumentOut, + KnowledgeItemIn, + KnowledgeItemOut, +) +from apps.knowledge.services import ingest_document_text, rebuild_item_chunks + +router = Router(tags=["knowledge"]) + + +@router.post("/bases", response=KnowledgeBaseOut) +def create_knowledge_base(request, payload: KnowledgeBaseIn): + require_permission(request, "knowledge.manage") + return KnowledgeBase.objects.create( + name=payload.name, + description=payload.description, + scope=payload.scope, + enabled=payload.enabled, + created_by=request.user if request.user.is_authenticated else None, + ) + + +@router.get("/bases", response=list[KnowledgeBaseOut]) +def list_knowledge_bases(request, enabled: bool | None = None): + require_permission(request, "knowledge.view") + queryset = KnowledgeBase.objects.all() + if enabled is not None: + queryset = queryset.filter(enabled=enabled) + return queryset[:100] + + +@router.post("/items", response=KnowledgeItemOut) +def create_knowledge_item(request, payload: KnowledgeItemIn): + require_permission(request, "knowledge.manage") + item = KnowledgeItem.objects.create( + knowledge_base_id=payload.knowledge_base_id, + title=payload.title, + problem=payload.problem, + applicable_scope=payload.applicable_scope, + error_code=payload.error_code, + root_cause=payload.root_cause, + troubleshooting_steps=payload.troubleshooting_steps, + solution=payload.solution, + risk_note=payload.risk_note, + status=payload.status, + created_by=request.user if request.user.is_authenticated else None, + ) + rebuild_item_chunks(item) + return item + + +@router.get("/items", response=list[KnowledgeItemOut]) +def list_knowledge_items(request, knowledge_base_id: int | None = None, status: str | None = None): + require_permission(request, "knowledge.view") + queryset = KnowledgeItem.objects.all() + if knowledge_base_id: + queryset = queryset.filter(knowledge_base_id=knowledge_base_id) + if status: + queryset = queryset.filter(status=status) + return queryset[:100] + + +@router.post("/documents", response=KnowledgeDocumentOut) +def create_knowledge_document(request, payload: KnowledgeDocumentIn): + require_permission(request, "knowledge.manage") + return ingest_document_text( + knowledge_base_id=payload.knowledge_base_id, + file_name=payload.file_name, + file_type=payload.file_type, + content=payload.content, + operator=request.user if request.user.is_authenticated else None, + ) +``` + +- [ ] **Step 7: Create migrations** + +Run: + +```powershell +cd backend +python manage.py makemigrations knowledge +``` + +Expected: creates `backend/apps/knowledge/migrations/0001_initial.py`. + +- [ ] **Step 8: Verify GREEN** + +Run: + +```powershell +python -m pytest backend\apps\knowledge\tests\test_knowledge_api.py -v +python -m ruff check backend\apps\knowledge backend\config +``` + +Expected: CRUD tests pass and ruff has no issues. + +- [ ] **Step 9: Commit** + +Run: + +```powershell +git add backend/apps/knowledge backend/config/settings/base.py backend/config/urls.py +git commit -m "feat: add knowledge base core api" +``` + +--- + +### Task 2: Chunking, Deterministic Embedding, And Retrieval + +**Files:** +- Create/Modify: `backend/apps/knowledge/services.py` +- Create: `backend/apps/knowledge/tests/test_retrieval.py` +- Modify: `backend/apps/knowledge/schemas.py` +- Modify: `backend/apps/knowledge/api.py` +- Modify: `backend/apps/solutions/api.py` + +- [ ] **Step 1: Write failing retrieval tests** + +Create `backend/apps/knowledge/tests/test_retrieval.py`: + +```python +from apps.knowledge.models import KnowledgeBase, KnowledgeChunk, KnowledgeItem +from apps.knowledge.services import retrieve_knowledge +from apps.solutions.models import Solution + + +def test_retrieve_knowledge_prefers_relevant_chunk(db): + base = KnowledgeBase.objects.create(name="采集知识库", scope="数据采集") + item = KnowledgeItem.objects.create( + knowledge_base=base, + title="税局采集进度", + problem="采集进度不更新", + solution="查询 Python 日志,确认税局状态。", + status="published", + ) + KnowledgeChunk.objects.create( + knowledge_base=base, + item=item, + chunk_index=0, + content="税局采集进度不更新时,需要查询 Python 日志。", + embedding=[0.1, 0.2], + metadata={"source_type": "knowledge_item", "title": item.title}, + ) + KnowledgeChunk.objects.create( + knowledge_base=base, + chunk_index=1, + content="登录失败通常需要检查账号密码。", + embedding=[0.9, 0.1], + metadata={"source_type": "document", "title": "登录问题"}, + ) + + results = retrieve_knowledge("税局采集进度怎么查询", limit=1) + + assert len(results) == 1 + assert results[0]["title"] == "税局采集进度" + assert "Python 日志" in results[0]["content"] + + +def test_published_solution_can_be_indexed_for_retrieval(db): + solution = Solution.objects.create( + title="税局不稳定采集处理中", + problem_signature="采集进度长时间不更新", + root_cause="税局服务不稳定", + solution="回复客户税局不稳定,任务仍在采集中。", + status="published", + ) + + from apps.knowledge.services import index_published_solution + + chunks = index_published_solution(solution) + + assert len(chunks) == 1 + assert chunks[0].metadata["source_type"] == "solution" + assert chunks[0].metadata["solution_id"] == solution.id +``` + +- [ ] **Step 2: Run tests to verify RED** + +Run: + +```powershell +python -m pytest backend\apps\knowledge\tests\test_retrieval.py -v +``` + +Expected: fails because retrieval services do not exist. + +- [ ] **Step 3: Implement chunking and embedding services** + +Create `backend/apps/knowledge/services.py`: + +```python +import hashlib +import math + +from apps.knowledge.models import KnowledgeBase, KnowledgeChunk, KnowledgeDocument, KnowledgeItem +from apps.solutions.models import Solution + +DEFAULT_CHUNK_SIZE = 600 + + +def split_text(content: str, chunk_size: int = DEFAULT_CHUNK_SIZE) -> list[str]: + normalized = "\n".join(line.strip() for line in content.splitlines() if line.strip()) + if not normalized: + return [] + return [normalized[index : index + chunk_size] for index in range(0, len(normalized), chunk_size)] + + +def embed_text(content: str, dimensions: int = 16) -> list[float]: + digest = hashlib.sha256(content.encode("utf-8")).digest() + values = [digest[index] / 255 for index in range(dimensions)] + length = math.sqrt(sum(value * value for value in values)) or 1 + return [round(value / length, 6) for value in values] + + +def keyword_score(query: str, content: str) -> float: + terms = [term for term in query.lower().split() if term] + lowered = content.lower() + return sum(1 for term in terms if term in lowered) + + +def vector_score(left: list[float], right: list[float]) -> float: + return sum(a * b for a, b in zip(left, right, strict=False)) + + +def rebuild_item_chunks(item: KnowledgeItem) -> list[KnowledgeChunk]: + item.chunks.all().delete() + content = "\n".join( + [ + item.title, + item.problem, + item.applicable_scope, + item.error_code, + item.root_cause, + item.troubleshooting_steps, + item.solution, + item.risk_note, + ] + ) + chunks = [] + for index, chunk_text in enumerate(split_text(content)): + chunks.append( + KnowledgeChunk.objects.create( + knowledge_base=item.knowledge_base, + item=item, + chunk_index=index, + content=chunk_text, + embedding=embed_text(chunk_text), + metadata={ + "source_type": "knowledge_item", + "title": item.title, + "item_id": item.id, + }, + ) + ) + return chunks + + +def ingest_document_text( + knowledge_base_id: int, + file_name: str, + file_type: str, + content: str, + operator=None, +) -> KnowledgeDocument: + document = KnowledgeDocument.objects.create( + knowledge_base_id=knowledge_base_id, + file_name=file_name, + file_type=file_type, + content_text=content, + parse_status="success", + created_by=operator, + ) + for index, chunk_text in enumerate(split_text(content)): + KnowledgeChunk.objects.create( + knowledge_base=document.knowledge_base, + document=document, + chunk_index=index, + content=chunk_text, + embedding=embed_text(chunk_text), + metadata={ + "source_type": "document", + "title": document.file_name, + "document_id": document.id, + }, + ) + return document + + +def index_published_solution(solution: Solution) -> list[KnowledgeChunk]: + base, _ = KnowledgeBase.objects.get_or_create( + name="解决方案知识库", + defaults={"description": "从已发布解决方案生成的检索知识", "scope": "solutions"}, + ) + KnowledgeChunk.objects.filter(metadata__solution_id=solution.id).delete() + content = "\n".join( + [ + solution.title, + solution.problem_signature, + solution.related_system, + solution.environment_scope, + solution.root_cause, + solution.solution, + solution.risk_note, + ] + ) + chunks = [] + for index, chunk_text in enumerate(split_text(content)): + chunks.append( + KnowledgeChunk.objects.create( + knowledge_base=base, + chunk_index=index, + content=chunk_text, + embedding=embed_text(chunk_text), + metadata={ + "source_type": "solution", + "title": solution.title, + "solution_id": solution.id, + }, + ) + ) + return chunks + + +def retrieve_knowledge(query: str, knowledge_base_id: int | None = None, limit: int = 5) -> list[dict]: + query_embedding = embed_text(query) + queryset = KnowledgeChunk.objects.select_related("knowledge_base", "item", "document") + if knowledge_base_id: + queryset = queryset.filter(knowledge_base_id=knowledge_base_id) + scored = [] + for chunk in queryset[:1000]: + score = keyword_score(query, chunk.content) * 10 + vector_score(query_embedding, chunk.embedding) + if score > 0: + scored.append((score, chunk)) + scored.sort(key=lambda pair: pair[0], reverse=True) + return [ + { + "id": chunk.id, + "knowledge_base_id": chunk.knowledge_base_id, + "title": chunk.metadata.get("title", chunk.knowledge_base.name), + "source_type": chunk.metadata.get("source_type", "chunk"), + "content": chunk.content, + "score": round(score, 4), + "metadata": chunk.metadata, + } + for score, chunk in scored[:limit] + ] +``` + +- [ ] **Step 4: Add retrieval schemas and API** + +Append to `backend/apps/knowledge/schemas.py`: + +```python +class KnowledgeSearchIn(Schema): + query: str + knowledge_base_id: int | None = None + limit: int = 5 + + +class KnowledgeSearchResultOut(Schema): + id: int + knowledge_base_id: int + title: str + source_type: str + content: str + score: float + metadata: dict +``` + +Append to `backend/apps/knowledge/api.py`: + +```python +from apps.knowledge.schemas import KnowledgeSearchIn, KnowledgeSearchResultOut +from apps.knowledge.services import retrieve_knowledge + + +@router.post("/search", response=list[KnowledgeSearchResultOut]) +def search_knowledge(request, payload: KnowledgeSearchIn): + require_permission(request, "knowledge.view") + return retrieve_knowledge( + query=payload.query, + knowledge_base_id=payload.knowledge_base_id, + limit=min(max(payload.limit, 1), 20), + ) +``` + +- [ ] **Step 5: Index published solutions on create** + +Modify `backend/apps/solutions/api.py` in `create_solution`: + +```python +solution = Solution.objects.create( + title=payload.title, + problem_signature=payload.problem_signature, + related_system=payload.related_system, + environment_scope=payload.environment_scope, + root_cause=payload.root_cause, + solution=payload.solution, + risk_note=payload.risk_note, + status=payload.status, + created_by=request.user if request.user.is_authenticated else None, +) +if solution.status == "published": + from apps.knowledge.services import index_published_solution + + index_published_solution(solution) +return solution +``` + +- [ ] **Step 6: Verify GREEN** + +Run: + +```powershell +python -m pytest backend\apps\knowledge\tests backend\apps\solutions\tests -v +python -m ruff check backend\apps\knowledge backend\apps\solutions +``` + +Expected: tests pass and ruff has no issues. + +- [ ] **Step 7: Commit** + +Run: + +```powershell +git add backend/apps/knowledge backend/apps/solutions/api.py +git commit -m "feat: add knowledge retrieval service" +``` + +--- + +### Task 3: Frontend Knowledge Pages + +**Files:** +- Create: `frontend/src/features/knowledge/types.ts` +- Create: `frontend/src/features/knowledge/api.ts` +- Create: `frontend/src/features/knowledge/KnowledgeBaseListPage.tsx` +- Create: `frontend/src/features/knowledge/KnowledgeItemEditorPage.tsx` +- Create: `frontend/src/features/knowledge/KnowledgeSearchPage.tsx` +- Modify: `frontend/src/app/router.tsx` +- Modify: `frontend/src/components/app/AppSidebar.tsx` + +- [ ] **Step 1: Create frontend types** + +Create `frontend/src/features/knowledge/types.ts`: + +```ts +export type KnowledgeBase = { + id: number + name: string + description: string + scope: string + enabled: boolean + created_at: string + updated_at: string +} + +export type KnowledgeItemInput = { + knowledge_base_id: number + title: string + problem: string + applicable_scope: string + error_code: string + root_cause: string + troubleshooting_steps: string + solution: string + risk_note: string + status: 'draft' | 'published' | 'disabled' +} + +export type KnowledgeSearchResult = { + id: number + knowledge_base_id: number + title: string + source_type: string + content: string + score: number + metadata: Record +} +``` + +- [ ] **Step 2: Create frontend API** + +Create `frontend/src/features/knowledge/api.ts`: + +```ts +import { apiRequest } from '@/lib/api' + +import type { KnowledgeBase, KnowledgeItemInput, KnowledgeSearchResult } from './types' + +export function listKnowledgeBases() { + return apiRequest('/knowledge/bases') +} + +export function createKnowledgeBase(payload: { + name: string + description: string + scope: string + enabled?: boolean +}) { + return apiRequest('/knowledge/bases', { + method: 'POST', + body: JSON.stringify({ ...payload, enabled: payload.enabled ?? true }) + }) +} + +export function createKnowledgeItem(payload: KnowledgeItemInput) { + return apiRequest('/knowledge/items', { + method: 'POST', + body: JSON.stringify(payload) + }) +} + +export function searchKnowledge(query: string, knowledgeBaseId?: number) { + return apiRequest('/knowledge/search', { + method: 'POST', + body: JSON.stringify({ + query, + knowledge_base_id: knowledgeBaseId, + limit: 8 + }) + }) +} +``` + +- [ ] **Step 3: Create knowledge base list page** + +Create `frontend/src/features/knowledge/KnowledgeBaseListPage.tsx` using `AppPage`, `AppPageHeader`, `AppSection`, `AppDataTable`, `AppField`, `Input`, and `Button`. + +Required Chinese UI: + +```text +知识库 +新建知识库 +名称 +范围 +描述 +启用状态 +创建知识库 +暂无知识库 +``` + +The quick-create form posts to `createKnowledgeBase`, reloads `listKnowledgeBases`, and clears the form. + +- [ ] **Step 4: Create knowledge item editor** + +Create `frontend/src/features/knowledge/KnowledgeItemEditorPage.tsx` using existing `AppField`, `Input`, `Textarea`, `Select`, `SelectGroup`, and `Button`. + +Required initial form: + +```ts +const initialForm: KnowledgeItemInput = { + knowledge_base_id: 0, + title: '', + problem: '', + applicable_scope: '', + error_code: '', + root_cause: '', + troubleshooting_steps: '', + solution: '', + risk_note: '', + status: 'draft' +} +``` + +Required Chinese sections: + +```text +新建知识条目 +基础信息 +问题与范围 +原因与处理 +发布状态 +保存知识条目 +``` + +On success, navigate to `#/knowledge/search`. + +- [ ] **Step 5: Create knowledge search page** + +Create `frontend/src/features/knowledge/KnowledgeSearchPage.tsx` using `AppPage`, `AppPageHeader`, `AppSection`, `Input`, `Button`, `Badge`, and `Card`. + +Search behavior: + +```ts +const [query, setQuery] = React.useState('') +const [results, setResults] = React.useState([]) + +async function handleSearch(event: React.FormEvent) { + event.preventDefault() + if (!query.trim()) return + setResults(await searchKnowledge(query.trim())) +} +``` + +Required Chinese UI: + +```text +知识检索 +输入问题、错误码或处理场景 +检索 +引用来源 +相似度 +暂无检索结果 +``` + +- [ ] **Step 6: Wire routes** + +Modify `frontend/src/app/router.tsx`: + +```tsx +import { KnowledgeBaseListPage } from '@/features/knowledge/KnowledgeBaseListPage' +import { KnowledgeItemEditorPage } from '@/features/knowledge/KnowledgeItemEditorPage' +import { KnowledgeSearchPage } from '@/features/knowledge/KnowledgeSearchPage' + +if (route === '/knowledge') { + return +} + +if (route === '/knowledge/items/new') { + return +} + +if (route === '/knowledge/search') { + return +} +``` + +- [ ] **Step 7: Add sidebar navigation** + +Modify `frontend/src/components/app/AppSidebar.tsx` to add Chinese knowledge menu entries under the existing navigation group: + +```ts +{ label: '知识库', href: '#/knowledge', icon: BookOpen } +{ label: '知识检索', href: '#/knowledge/search', icon: Search } +``` + +Use lucide icons already available in `lucide-react`. + +- [ ] **Step 8: Verify frontend** + +Run: + +```powershell +npm run typecheck +npm run build +``` + +Expected: both commands pass. + +- [ ] **Step 9: Commit** + +Run: + +```powershell +git add frontend/src/features/knowledge frontend/src/app/router.tsx frontend/src/components/app/AppSidebar.tsx +git commit -m "feat: add knowledge management pages" +``` + +--- + +### Task 4: Final Verification And Documentation Sync + +**Files:** +- Modify: `README.md` +- Modify: `docs/frontend/design-system.md` +- Modify: `docs/superpowers/plans/2026-07-03-stage-2-knowledge-vector-retrieval.md` + +- [ ] **Step 1: Run backend verification** + +Run: + +```powershell +python manage.py makemigrations --check --dry-run +python -m pytest backend\apps\knowledge\tests backend\apps\solutions\tests backend\apps\tickets\tests +python -m ruff check backend\apps\knowledge backend\apps\solutions backend\apps\tickets backend\config +``` + +Expected: + +```text +No changes detected +all selected tests pass +All checks passed! +``` + +- [ ] **Step 2: Run frontend verification** + +Run: + +```powershell +npm run typecheck +npm run build +``` + +Expected: TypeScript and Vite build succeed. + +- [ ] **Step 3: Run database migration locally** + +Run: + +```powershell +cd backend +python manage.py migrate +``` + +Expected: `knowledge.0001_initial` is applied. + +- [ ] **Step 4: Update README current status** + +Modify `README.md` current status sentence to include: + +```text +knowledge base and retrieval baseline +``` + +- [ ] **Step 5: Update frontend design system** + +Modify `docs/frontend/design-system.md` under `Current Core Screens`: + +```markdown +- Knowledge pages use the same `AppPage`, `AppSection`, `AppDataTable`, `AppField`, and shadcn form primitives as ticket and system pages. +- Knowledge search results must show source type, title, score, and citable content snippets. +``` + +- [ ] **Step 6: Mark plan complete** + +In this plan file, mark completed task checkboxes as `[x]` after successful execution. + +- [ ] **Step 7: Commit** + +Run: + +```powershell +git add README.md docs/frontend/design-system.md docs/superpowers/plans/2026-07-03-stage-2-knowledge-vector-retrieval.md +git commit -m "docs: sync stage 2 knowledge progress" +``` + +--- + +## Final Review Checklist + +- [ ] `apps.knowledge` exists and is registered in Django settings. +- [ ] Knowledge bases can be created and listed through API. +- [ ] Knowledge items create chunks. +- [ ] Document text ingestion creates chunks. +- [ ] Published solutions can be indexed as knowledge chunks. +- [ ] Retrieval returns citable chunk results with source metadata. +- [ ] Frontend has Chinese knowledge base, item creation, and search pages. +- [ ] Sidebar exposes knowledge navigation. +- [ ] Backend tests pass. +- [ ] Backend ruff passes. +- [ ] Frontend typecheck passes. +- [ ] Frontend build passes. +- [ ] README and design-system docs reflect Stage 2 behavior.