From 0ee5d532ba4bd7eb9cc6bc4a7fcc9f026fb27df5 Mon Sep 17 00:00:00 2001 From: OG T Date: Fri, 10 Apr 2026 07:34:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(rag):=20=E6=96=B0=E5=A2=9E=20RAG=20Router?= =?UTF-8?q?=20+=20=E6=8E=9B=E8=BC=89=E5=88=B0=20main.py=20(Phase=2033=20AD?= =?UTF-8?q?R-067)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/api/src/api/v1/rag.py | 102 +++++++++++++++++++++++++++++++++++++ apps/api/src/main.py | 4 ++ 2 files changed, 106 insertions(+) create mode 100644 apps/api/src/api/v1/rag.py diff --git a/apps/api/src/api/v1/rag.py b/apps/api/src/api/v1/rag.py new file mode 100644 index 00000000..6aa8b1bf --- /dev/null +++ b/apps/api/src/api/v1/rag.py @@ -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) diff --git a/apps/api/src/main.py b/apps/api/src/main.py index a9d54d8a..6f073adc 100644 --- a/apps/api/src/main.py +++ b/apps/api/src/main.py @@ -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