feat(api): Phase 13.2 #84 RAG Provider + Gemini 優先切換

1. 新增 RAGProvider MCP Tool Provider
   - search_runbook: 語義搜尋維運手冊
   - index_documents: 索引文檔
   - get_index_stats: 取得索引統計

2. 更新 AI_FALLBACK_ORDER 為 Gemini 優先
   - 臨時措施:Ollama CPU 推論緩慢導致 mock_fallback
   - 預計 2026-03-27 切回 Ollama

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-26 18:21:24 +08:00
parent 6e3a7fca20
commit 539f14bcd5
3 changed files with 245 additions and 3 deletions

View File

@@ -8,10 +8,12 @@ MCP Tool Providers - ADR-015 模組化架構
- DatabaseProvider: 資料庫查詢 (Approval/Incident)
- FilesystemProvider: 安全受限的文件讀取 (#82)
- GrafanaProvider: Grafana Dashboard 查詢 (#83)
- RAGProvider: 維運手冊語義搜尋 (#84)
@see docs/adr/ADR-015-mcp-modular-architecture.md
變更紀錄:
- 2026-03-27 v1.3: 新增 RAGProvider (#84) - Claude Code (台北時區)
- 2026-03-26 v1.2: 新增 GrafanaProvider (#83) - Claude Code
- 2026-03-26 v1.1: 新增 FilesystemProvider (#82) - Claude Code
"""
@@ -20,6 +22,7 @@ from src.plugins.mcp.providers.database_provider import DatabaseProvider
from src.plugins.mcp.providers.filesystem_provider import FilesystemProvider
from src.plugins.mcp.providers.grafana_provider import GrafanaProvider
from src.plugins.mcp.providers.k8s_provider import K8sProvider
from src.plugins.mcp.providers.rag_provider import RAGProvider
from src.plugins.mcp.providers.signoz_provider import SignOzProvider
__all__ = [
@@ -28,6 +31,7 @@ __all__ = [
"DatabaseProvider",
"FilesystemProvider",
"GrafanaProvider",
"RAGProvider",
]
@@ -44,3 +48,4 @@ def register_all_providers() -> None:
register_provider(DatabaseProvider())
register_provider(FilesystemProvider())
register_provider(GrafanaProvider())
register_provider(RAGProvider())

View File

@@ -0,0 +1,234 @@
"""
RAG MCP Tool Provider - ADR-015 模組化架構
==========================================
提供維運手冊 RAG 搜尋工具:
- search_runbook: 語義搜尋維運手冊
- index_documents: 索引文檔 (管理用)
- get_index_stats: 取得索引統計
Phase 13.2 #84 - Runbook RAG Tool
版本: v1.0
建立日期: 2026-03-27 00:00 (台北時區)
建立者: Claude Code
"""
import time
import uuid
from pathlib import Path
from typing import Any
import structlog
from src.plugins.mcp.interfaces import MCPTool, MCPToolProvider, MCPToolResult
logger = structlog.get_logger(__name__)
class RAGProvider(MCPToolProvider):
"""
RAG MCP Tool Provider
提供維運手冊的語義搜尋功能。
使用 Redis Vector Search + Ollama Embedding。
"""
def __init__(self) -> None:
self._rag_service = None
@property
def name(self) -> str:
return "runbooks"
def _get_rag_service(self):
"""Lazy load RAG service"""
if self._rag_service is None:
from src.services.rag_service import get_rag_service
self._rag_service = get_rag_service()
return self._rag_service
async def list_tools(self) -> list[MCPTool]:
return [
MCPTool(
name="search_runbook",
description="Search runbook documentation with natural language query. Returns relevant sections from ADRs, Skills, and operation guides.",
input_schema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Natural language search query (e.g., '如何處理 Pod CrashLoopBackOff')",
},
"top_k": {
"type": "integer",
"description": "Number of results to return (default: 5, max: 10)",
"default": 5,
},
},
"required": ["query"],
},
server_name=self.name,
),
MCPTool(
name="index_documents",
description="Index or re-index runbook documents. Admin operation - use when documents are updated.",
input_schema={
"type": "object",
"properties": {
"force": {
"type": "boolean",
"description": "Force re-index even if index exists",
"default": False,
},
},
},
server_name=self.name,
),
MCPTool(
name="get_index_stats",
description="Get RAG index statistics (document count, index status).",
input_schema={
"type": "object",
"properties": {},
},
server_name=self.name,
),
]
async def execute(
self,
tool_name: str,
parameters: dict[str, Any],
) -> MCPToolResult:
"""執行 RAG 工具"""
execution_id = str(uuid.uuid4())
start_time = time.time()
try:
if tool_name == "search_runbook":
result = await self._search_runbook(parameters)
elif tool_name == "index_documents":
result = await self._index_documents(parameters)
elif tool_name == "get_index_stats":
result = await self._get_index_stats()
else:
return MCPToolResult(
success=False,
execution_id=execution_id,
error=f"Unknown tool: {tool_name}",
)
duration = time.time() - start_time
return MCPToolResult(
success=True,
execution_id=execution_id,
output=result,
duration=duration,
)
except Exception as e:
logger.error(
"rag_tool_error",
tool=tool_name,
error=str(e),
)
return MCPToolResult(
success=False,
execution_id=execution_id,
error=str(e),
duration=time.time() - start_time,
)
async def _search_runbook(self, params: dict) -> dict:
"""
搜尋維運手冊
Args:
params: {query: str, top_k: int}
Returns:
dict: 搜尋結果
"""
query = params.get("query", "")
top_k = min(params.get("top_k", 5), 10) # 最多 10 筆
if not query:
return {"error": "Query is required", "results": []}
rag_service = self._get_rag_service()
results = await rag_service.search(query, top_k)
logger.info(
"rag_search_executed",
query=query[:50],
results_count=len(results),
)
return {
"query": query,
"results": results,
"count": len(results),
}
async def _index_documents(self, params: dict) -> dict:
"""
索引維運手冊
Args:
params: {force: bool}
Returns:
dict: 索引結果
"""
force = params.get("force", False)
rag_service = self._get_rag_service()
# 如果強制重建,先清除
if force:
await rag_service.clear_index()
# 使用專案根目錄
# 從當前檔案位置推算: providers/ -> mcp/ -> plugins/ -> src/ -> api/ -> apps/ -> awoooi/
base_path = Path(__file__).parent.parent.parent.parent.parent.parent.parent
chunks_indexed = await rag_service.index_documents(base_path)
logger.info(
"rag_index_executed",
chunks=chunks_indexed,
force=force,
)
return {
"status": "success",
"chunks_indexed": chunks_indexed,
"force_rebuild": force,
}
async def _get_index_stats(self) -> dict:
"""
取得索引統計
Returns:
dict: 索引統計資訊
"""
rag_service = self._get_rag_service()
stats = await rag_service.get_index_stats()
return stats
async def health_check(self) -> bool:
"""
健康檢查
檢查 Redis 和 Embedding Service 是否可用
"""
try:
rag_service = self._get_rag_service()
stats = await rag_service.get_index_stats()
return "error" not in stats
except Exception as e:
logger.warning("rag_health_check_failed", error=str(e))
return False

View File

@@ -1,7 +1,8 @@
# AWOOOI 正式環境 ConfigMap
# 負責人: CIO
# 版本: v1.0
# 日期: 2026-03-20
# 版本: v1.1
# 日期: 2026-03-27 (台北時區)
# 變更: Gemini 優先 (臨時,待 Ollama CPU 問題修復後切回)
apiVersion: v1
kind: ConfigMap
@@ -33,7 +34,9 @@ data:
CORS_ORIGINS: '["https://awoooi.wooo.work","http://localhost:3000","http://localhost:3001"]'
# AI 配置 (JSON array 格式 for pydantic-settings)
AI_FALLBACK_ORDER: '["ollama","gemini","claude"]'
# 2026-03-27: 臨時切換 Gemini 優先 (Ollama CPU 推論緩慢導致 mock_fallback)
# 預計 2026-03-27 切回 Ollama 優先
AI_FALLBACK_ORDER: '["gemini","ollama","claude"]'
AI_CACHE_TTL: "3600"
# 快取 TTL (秒)