from datetime import datetime from typing import Optional from fastapi import APIRouter, HTTPException from pydantic import BaseModel from wiki_engine import db from wiki_engine import service router = APIRouter(prefix="/api/wiki", tags=["wiki"]) class WikiSyncFileRequest(BaseModel): kb_id: Optional[str] = None kb_name: Optional[str] = None use_llm: bool = False background: bool = True class WikiSyncKbRequest(BaseModel): kb_id: Optional[str] = None kb_name: Optional[str] = None limit: Optional[int] = None use_llm: bool = False background: bool = True class WikiSearchRequest(BaseModel): query: str kb_id: Optional[str] = None kb_name: Optional[str] = None limit: int = 8 include_slices: bool = True class WikiDeleteKbRequest(BaseModel): kb_id: Optional[str] = None kb_name: Optional[str] = None class WikiDeleteFileRequest(BaseModel): file_id: str class WikiDeleteTimeRequest(BaseModel): before: Optional[datetime] = None after: Optional[datetime] = None kb_id: Optional[str] = None kb_name: Optional[str] = None class WikiDiffKbRequest(BaseModel): kb_id: Optional[str] = None kb_name: Optional[str] = None include_children: bool = False limit: int = 200 parsed_only: bool = True class WikiReconcileKbRequest(BaseModel): kb_id: Optional[str] = None kb_name: Optional[str] = None include_children: bool = False limit: int = 200 sync_missing: bool = True rebuild_stale: bool = True delete_orphan: bool = False use_llm: bool = False dry_run: bool = True @router.post("/init") async def init_wiki(): await db.init_wiki_tables() return {"success": True} @router.post("/sync/file/{file_id}") async def sync_file(file_id: str, request: WikiSyncFileRequest): kb_id = await _resolve_optional_kb_id(request.kb_id, request.kb_name) if request.background: job_id = await service.start_sync_job( target_type="file", target_id=file_id, kb_id=kb_id, use_llm=request.use_llm, ) return {"success": True, "job_id": job_id, "status": "running", "job": await db.get_job(job_id)} return await service.sync_file(file_id=file_id, kb_id=kb_id, use_llm=request.use_llm) @router.post("/sync/kb") async def sync_kb(request: WikiSyncKbRequest): kb_id = await _resolve_required_kb_id(request.kb_id, request.kb_name) if request.background: job_id = await service.start_sync_job( target_type="kb", target_id=kb_id, kb_id=kb_id, limit=request.limit, use_llm=request.use_llm, ) return {"success": True, "job_id": job_id, "status": "running", "job": await db.get_job(job_id)} return await service.sync_kb(kb_id=kb_id, limit=request.limit, use_llm=request.use_llm) @router.get("/sync/jobs/{job_id}") async def get_sync_job(job_id: str): job = await db.get_job(job_id) if not job: raise HTTPException(status_code=404, detail="sync job not found") return {"success": True, "job": job} @router.post("/search") async def search_wiki(request: WikiSearchRequest): if not request.query.strip(): raise HTTPException(status_code=400, detail="query is empty") kb_id = await _resolve_optional_kb_id(request.kb_id, request.kb_name) return await service.search_wiki( query=request.query, kb_id=kb_id, limit=request.limit, include_slices=request.include_slices, ) @router.post("/evidence/search") async def search_evidence(request: WikiSearchRequest): if not request.query.strip(): raise HTTPException(status_code=400, detail="query is empty") kb_id = await _resolve_optional_kb_id(request.kb_id, request.kb_name) return await service.evidence_search( query=request.query, kb_id=kb_id, limit=request.limit, ) @router.get("/pages/{page_id}") async def get_page(page_id: int): page = await service.get_page(page_id) if not page: raise HTTPException(status_code=404, detail="wiki page not found") return {"success": True, "page": page} @router.get("/stats") async def get_stats(): return {"success": True, "stats": await service.get_stats()} @router.get("/queue/stats") async def get_queue_stats(): return {"success": True, "stats": await service.get_queue_stats()} @router.get("/tree") async def get_kb_tree(): return await service.get_kb_tree() @router.post("/diff/kb") async def diff_kb(request: WikiDiffKbRequest): kb_id = await _resolve_required_kb_id(request.kb_id, request.kb_name) return await service.diff_kb( kb_id=kb_id, include_children=request.include_children, limit=request.limit, parsed_only=request.parsed_only, ) @router.post("/reconcile/kb") async def reconcile_kb(request: WikiReconcileKbRequest): kb_id = await _resolve_required_kb_id(request.kb_id, request.kb_name) return await service.reconcile_kb( kb_id=kb_id, include_children=request.include_children, limit=request.limit, sync_missing=request.sync_missing, rebuild_stale=request.rebuild_stale, delete_orphan=request.delete_orphan, use_llm=request.use_llm, dry_run=request.dry_run, ) @router.post("/delete/by-kb") async def delete_by_kb(request: WikiDeleteKbRequest): kb_id = await _resolve_required_kb_id(request.kb_id, request.kb_name) return await service.delete_by_kb(kb_id) @router.post("/delete/by-file") async def delete_by_file(request: WikiDeleteFileRequest): if not request.file_id.strip(): raise HTTPException(status_code=400, detail="file_id is empty") return await service.delete_by_file(request.file_id) @router.post("/delete/by-time") async def delete_by_time(request: WikiDeleteTimeRequest): if request.before is None and request.after is None: raise HTTPException(status_code=400, detail="before or after is required") kb_id = await _resolve_optional_kb_id(request.kb_id, request.kb_name) return await service.delete_by_time( before=request.before, after=request.after, kb_id=kb_id, ) async def _resolve_required_kb_id(kb_id: Optional[str], kb_name: Optional[str]) -> str: resolved = await _resolve_optional_kb_id(kb_id, kb_name) if not resolved: raise HTTPException(status_code=400, detail="kb_name or kb_id is required") return resolved async def _resolve_optional_kb_id(kb_id: Optional[str], kb_name: Optional[str]) -> Optional[str]: if kb_id and kb_id.strip(): return kb_id.strip() if not kb_name or not kb_name.strip(): return None resolved = await service.resolve_kb_reference(kb_name.strip()) if not resolved.get("success"): raise HTTPException(status_code=404, detail=resolved) return str(resolved["kb_id"])