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:
@@ -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())
|
||||
|
||||
234
apps/api/src/plugins/mcp/providers/rag_provider.py
Normal file
234
apps/api/src/plugins/mcp/providers/rag_provider.py
Normal 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
|
||||
@@ -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 (秒)
|
||||
|
||||
Reference in New Issue
Block a user