231 lines
6.8 KiB
Python
231 lines
6.8 KiB
Python
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"])
|