feat(rag): Phase 33 RAG API 端點 — /knowledge/rag/index + query + stats
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 14m35s

ADR-067 Phase 33: pgvector RAG 三個 HTTP 端點
- POST /knowledge/rag/index — 索引文件到 rag_chunks
- GET /knowledge/rag/query — embed→knn→生成答案
- GET /knowledge/rag/stats — chunks 統計 (透過 Service 層)
- 修正: rag_stats 移至 KnowledgeRAGService.get_stats() (leWOOOgo 積木化)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-10 02:00:59 +08:00
parent 63e840ae42
commit e605b7192b
2 changed files with 56 additions and 0 deletions

View File

@@ -152,3 +152,48 @@ async def archive_entry(entry_id: str) -> None:
success = await service.archive_entry(entry_id)
if not success:
raise HTTPException(status_code=404, detail="Knowledge entry not found")
# =============================================================================
# Phase 33 (ADR-067 2026-04-10): RAG pgvector 端點
# =============================================================================
@router.post("/rag/index", status_code=200)
async def rag_index_document(body: dict) -> dict:
"""
索引文件到 RAG 知識庫 (pgvector rag_chunks 表)
body: {source, source_id, title, text, metadata?}
"""
from src.services.knowledge_rag_service import get_knowledge_rag_service
svc = get_knowledge_rag_service()
ok = await svc.index_document(
source=body.get("source", "manual"),
source_id=body.get("source_id", ""),
title=body.get("title", ""),
text=body.get("text", ""),
metadata=body.get("metadata", {}),
)
return {"ok": ok, "title": body.get("title", "")}
@router.get("/rag/query")
async def rag_query(
q: str = Query(..., min_length=1, description="RAG 查詢問題"),
top_k: int = Query(5, ge=1, le=20),
) -> dict:
"""
RAG 查詢 — embedding → pgvector knn → 生成繁中回答
"""
from src.services.knowledge_rag_service import get_knowledge_rag_service
svc = get_knowledge_rag_service()
answer = await svc.query(q, top_k=top_k)
return {"question": q, "answer": answer}
@router.get("/rag/stats")
async def rag_stats() -> dict:
"""RAG 知識庫統計"""
from src.services.knowledge_rag_service import get_knowledge_rag_service
svc = get_knowledge_rag_service()
return await svc.get_stats()

View File

@@ -215,6 +215,17 @@ class KnowledgeRAGService:
start += chunk_size - overlap
return [c for c in chunks if c.strip()]
async def get_stats(self) -> dict:
"""RAG 知識庫統計"""
from src.db.base import get_db_context
from sqlalchemy import text
async with get_db_context() as db:
row = await db.execute(
text("SELECT COUNT(*) as total, COUNT(DISTINCT source) as sources FROM rag_chunks")
)
r = row.one()
return {"total_chunks": r.total, "sources": r.sources}
async def close(self) -> None:
if self._http and not self._http.is_closed:
await self._http.aclose()