feat(rag): 新增 RAG Router + 掛載到 main.py (Phase 33 ADR-067)
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 13m11s

- rag.py: POST /index, POST /query, GET /stats 三端點
- stats 委派給 KnowledgeRAGService.get_stats()(leWOOOgo 合規)
- main.py: include_router rag_v1.router

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-10 07:34:06 +08:00
parent e605b7192b
commit 0ee5d532ba
2 changed files with 106 additions and 0 deletions

102
apps/api/src/api/v1/rag.py Normal file
View File

@@ -0,0 +1,102 @@
"""
RAG 知識庫 API Router - Phase 33
===================================
leWOOOgo 原則: Router 只做 HTTP 轉發,業務邏輯在 KnowledgeRAGService
版本: v1.0
建立: 2026-04-10 (台北時區)
建立者: Claude Code (Phase 33 ADR-067)
"""
from fastapi import APIRouter, BackgroundTasks, HTTPException
from pydantic import BaseModel
from src.services.knowledge_rag_service import get_knowledge_rag_service
router = APIRouter(prefix="/rag", tags=["RAG Knowledge Base"])
class RagQueryRequest(BaseModel):
question: str
top_k: int = 5
class RagQueryResponse(BaseModel):
answer: str
question: str
class RagIndexResponse(BaseModel):
status: str
message: str
@router.post("/index", response_model=RagIndexResponse, summary="觸發知識庫全量索引")
async def trigger_index(background_tasks: BackgroundTasks) -> RagIndexResponse:
"""
觸發文件向量化索引(背景執行)
索引來源:
- docs/runbooks/*.md
- docs/adr/*.md
- docs/LOGBOOK.md
- .agents/skills/*.md
"""
background_tasks.add_task(_run_index)
return RagIndexResponse(
status="accepted",
message="索引已排程背景執行中nomic-embed-text @ Ollama 188",
)
@router.post("/query", response_model=RagQueryResponse, summary="語義查詢知識庫")
async def query_rag(request: RagQueryRequest) -> RagQueryResponse:
"""語義搜尋知識庫,用 deepseek-r1:14b 生成回答"""
svc = get_knowledge_rag_service()
answer = await svc.query(request.question, top_k=request.top_k)
return RagQueryResponse(answer=answer, question=request.question)
@router.get("/stats", summary="索引統計")
async def rag_stats() -> dict:
"""取得知識庫索引統計chunk 數量等)"""
svc = get_knowledge_rag_service()
return await svc.get_stats()
# ============================================================
# Background helper
# ============================================================
async def _run_index() -> None:
"""背景:掃描所有文件來源並向量化"""
import structlog
from pathlib import Path
logger = structlog.get_logger(__name__)
svc = get_knowledge_rag_service()
sources = [
Path("docs/runbooks"),
Path("docs/adr"),
Path("docs"),
Path(".agents/skills"),
]
total = 0
for source_dir in sources:
if not source_dir.exists():
continue
pattern = "*.md"
for md_file in source_dir.glob(pattern):
try:
count = await svc.index_document(
file_path=md_file,
source_type=source_dir.parts[0] if len(source_dir.parts) == 1 else source_dir.parts[1],
)
total += count
logger.debug("rag_indexed", file=str(md_file), chunks=count)
except Exception as e:
logger.warning("rag_index_file_failed", file=str(md_file), error=str(e))
logger.info("rag_index_complete", total_chunks=total)

View File

@@ -59,6 +59,7 @@ from src.api.v1 import (
signoz_webhook as signoz_webhook_v1, # Phase 21: SignOz → Telegram (ADR-037)
)
from src.api.v1 import drift as drift_v1 # Phase 25 P2: Config Drift Detection
from src.api.v1 import rag as rag_v1 # Phase 33 ADR-067: RAG 知識庫
from src.api.v1 import monitoring as monitoring_v1 # 2026-04-03: 監控工具狀態
from src.api.v1 import stats as stats_v1 # Phase 6.5: Statistics Analytics
from src.api.v1 import telegram as telegram_v1 # Phase 5.4: Telegram Gateway
@@ -478,6 +479,9 @@ app.include_router(
app.include_router(
drift_v1.router, prefix="/api/v1", tags=["Drift Detection"]
) # Phase 25 P2: Config Drift Detection
app.include_router(
rag_v1.router, prefix="/api/v1", tags=["RAG Knowledge Base"]
) # Phase 33 ADR-067: RAG 知識庫
app.include_router(
errors_v1.router, prefix="/api/v1", tags=["Errors"]
) # #40: Sentry 錯誤 BFF API