refactor(rag): 架構審查修正 — leWOOOgo 合規 + 去重 + httpx 關機
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
- C2: _run_index() 業務邏輯移入 KnowledgeRAGService.index_all_sources()
Router 層只做 background_tasks.add_task(_run_index) 轉發
- C3: glob("*.md") → rglob("*.md") — 掃描巢狀子目錄
- C4: docstring 修正 Ollama 188 → 111
- I2: index_document() 先刪舊版本 (_delete_by_source_id) 避免重複累積
- I3: debug endpoint 改用 settings.OLLAMA_URL 取代硬碼 IP
- I4: main.py shutdown 加入 get_knowledge_rag_service().close()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -75,8 +75,9 @@ async def rag_debug() -> dict:
|
||||
ollama_ok: bool | str = False
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as c:
|
||||
from src.core.config import get_settings as _gs
|
||||
r = await c.post(
|
||||
"http://192.168.0.111:11434/api/embeddings",
|
||||
f"{_gs().OLLAMA_URL}/api/embeddings",
|
||||
json={"model": "nomic-embed-text", "prompt": "test"},
|
||||
)
|
||||
ollama_ok = r.status_code == 200 if r.status_code == 200 else f"http_{r.status_code}"
|
||||
@@ -98,38 +99,6 @@ async def rag_stats() -> dict:
|
||||
# ============================================================
|
||||
|
||||
async def _run_index() -> None:
|
||||
"""背景:掃描所有文件來源並向量化"""
|
||||
import structlog
|
||||
from pathlib import Path
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
"""背景:委派給 KnowledgeRAGService.index_all_sources() 執行"""
|
||||
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
|
||||
for md_file in source_dir.glob("*.md"):
|
||||
try:
|
||||
text = md_file.read_text(encoding="utf-8", errors="ignore")
|
||||
source_type = source_dir.parts[-1] # e.g. "runbooks", "adr", "skills"
|
||||
ok = await svc.index_document(
|
||||
source=source_type,
|
||||
source_id=str(md_file),
|
||||
title=md_file.stem,
|
||||
text=text,
|
||||
)
|
||||
if ok:
|
||||
total += 1
|
||||
logger.debug("rag_indexed", file=str(md_file))
|
||||
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)
|
||||
await svc.index_all_sources()
|
||||
|
||||
@@ -296,6 +296,9 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
# Phase 5.4: Close Telegram Gateway
|
||||
telegram_gw = get_telegram_gateway()
|
||||
await telegram_gw.close()
|
||||
# Phase 33: Close RAG Service httpx client (ADR-067)
|
||||
from src.services.knowledge_rag_service import get_knowledge_rag_service
|
||||
await get_knowledge_rag_service().close()
|
||||
# Phase 5: Close HTTP Clients (統帥鐵律: 連線池回收)
|
||||
await close_all_http_clients()
|
||||
# Phase 6.1.1: Close Redis Pool (統帥鐵律: Redis 連線池回收)
|
||||
|
||||
@@ -11,7 +11,7 @@ AWOOOI — Knowledge RAG Service (Phase 33, ADR-067)
|
||||
- 初期 < 100 筆: 線性搜尋
|
||||
- 超過 100 筆: 執行 CREATE INDEX ivfflat (手動觸發)
|
||||
|
||||
向量模型: nomic-embed-text (Ollama 188, 768維)
|
||||
向量模型: nomic-embed-text (Ollama 111, 768維) — 188:11434 被 NetworkPolicy v1.3 封閉
|
||||
生成模型: qwen2.5:7b-instruct (Ollama 111)
|
||||
|
||||
2026-04-10 Claude Sonnet 4.6 Asia/Taipei
|
||||
@@ -87,6 +87,8 @@ class KnowledgeRAGService:
|
||||
將文件向量化並儲存到 pgvector
|
||||
自動分段 (每段 500 字, overlap 100)
|
||||
"""
|
||||
# 先刪舊版本,避免重複索引累積 (I2 審查修正 2026-04-10)
|
||||
await self._delete_by_source_id(source_id)
|
||||
chunks = self._chunk_text(text, chunk_size=500, overlap=100)
|
||||
success = 0
|
||||
for chunk in chunks:
|
||||
@@ -143,6 +145,18 @@ class KnowledgeRAGService:
|
||||
logger.error("rag_pgvector_search_failed", error=str(e))
|
||||
return []
|
||||
|
||||
async def _delete_by_source_id(self, source_id: str) -> None:
|
||||
try:
|
||||
from src.db.base import get_db_context
|
||||
from sqlalchemy import text
|
||||
async with get_db_context() as db:
|
||||
await db.execute(
|
||||
text("DELETE FROM rag_chunks WHERE source_id = :source_id"),
|
||||
{"source_id": source_id},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("rag_delete_old_chunks_failed", source_id=source_id, error=str(e))
|
||||
|
||||
async def _save_chunk(
|
||||
self,
|
||||
source: str, source_id: str, title: str,
|
||||
@@ -216,6 +230,41 @@ class KnowledgeRAGService:
|
||||
start += chunk_size - overlap
|
||||
return [c for c in chunks if c.strip()]
|
||||
|
||||
async def index_all_sources(self) -> int:
|
||||
"""
|
||||
掃描所有知識來源並向量化(供 /rag/index 端點呼叫)
|
||||
來源: docs/runbooks/, docs/adr/, docs/, .agents/skills/
|
||||
回傳成功索引的文件數
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
for md_file in source_dir.rglob("*.md"):
|
||||
try:
|
||||
text = md_file.read_text(encoding="utf-8", errors="ignore")
|
||||
ok = await self.index_document(
|
||||
source=source_dir.parts[-1],
|
||||
source_id=str(md_file),
|
||||
title=md_file.stem,
|
||||
text=text,
|
||||
)
|
||||
if ok:
|
||||
total += 1
|
||||
logger.debug("rag_source_indexed", file=str(md_file))
|
||||
except Exception as e:
|
||||
logger.warning("rag_source_index_failed", file=str(md_file), error=str(e))
|
||||
logger.info("rag_index_all_complete", total_docs=total)
|
||||
return total
|
||||
|
||||
async def get_stats(self) -> dict:
|
||||
"""RAG 知識庫統計"""
|
||||
from src.db.base import get_db_context
|
||||
|
||||
Reference in New Issue
Block a user