refactor(ai): 模組化重構 - NVIDIA chat 移至 NvidiaProvider
符合 feedback_lewooogo_modular_enforcement.md 規範: - 移除 openclaw.py 中的 _call_nvidia() (重複邏輯) - 新增 NvidiaProvider.chat() 方法 - 更新 INvidiaProvider Protocol - openclaw.py 改用 get_nvidia_provider().chat() - 測試移至 test_nvidia_chat.py 架構層次: - Router → Service → Provider (正確) - 禁止 Service 層重複實作已存在的 Provider 功能 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,21 @@ class INvidiaProvider(Protocol):
|
||||
"""關閉資源"""
|
||||
...
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
prompt: str,
|
||||
model: str = ...,
|
||||
temperature: float = ...,
|
||||
max_tokens: int = ...,
|
||||
) -> tuple[str, bool, int, float]:
|
||||
"""
|
||||
一般對話 (非 Tool Calling) - 2026-03-29 ogt 新增
|
||||
|
||||
Returns:
|
||||
tuple: (response_text, success, total_tokens, cost_usd)
|
||||
"""
|
||||
...
|
||||
|
||||
# =============================================================================
|
||||
# 常量定義
|
||||
# =============================================================================
|
||||
@@ -635,6 +650,142 @@ class NvidiaProvider:
|
||||
if tc.valid and tc.tool_name and self.is_high_risk_tool(tc.tool_name)
|
||||
]
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
prompt: str,
|
||||
model: str | None = None,
|
||||
temperature: float = 0.1,
|
||||
max_tokens: int = 2048,
|
||||
) -> tuple[str, bool, int, float]:
|
||||
"""
|
||||
一般對話 (非 Tool Calling) - 用於 RCA 分析
|
||||
|
||||
2026-03-29 ogt: 新增,符合模組化規範
|
||||
從 openclaw.py 遷移,統一由 NvidiaProvider 處理所有 NVIDIA API 呼叫
|
||||
|
||||
Args:
|
||||
prompt: 對話內容
|
||||
model: 模型名稱 (預設從 ModelRegistry 取得)
|
||||
temperature: 溫度
|
||||
max_tokens: 最大輸出 Token
|
||||
|
||||
Returns:
|
||||
tuple: (response_text, success, total_tokens, cost_usd)
|
||||
"""
|
||||
start_time = time.perf_counter()
|
||||
|
||||
# OTEL Span
|
||||
with _tracer.start_as_current_span("nvidia_chat") as span:
|
||||
span.set_attribute("ai.provider", "nvidia")
|
||||
|
||||
# Circuit Breaker 檢查
|
||||
if not self._circuit_breaker.can_execute():
|
||||
span.set_attribute("ai.error", "circuit_breaker_open")
|
||||
NVIDIA_REQUESTS_TOTAL.labels(status="circuit_open", tool_name="chat").inc()
|
||||
logger.warning("nvidia_chat_circuit_breaker_open")
|
||||
return "Circuit Breaker OPEN - NVIDIA API 暫時不可用", False, 0, 0.0
|
||||
|
||||
# 檢查 API Key
|
||||
if not self._api_key:
|
||||
span.set_attribute("ai.error", "api_key_not_set")
|
||||
return "NVIDIA_API_KEY not configured", False, 0, 0.0
|
||||
|
||||
# 從 ModelRegistry 取得模型
|
||||
from src.services.model_registry import get_model_registry
|
||||
registry = get_model_registry()
|
||||
model_name = model or registry.get_model("nvidia", "rca")
|
||||
|
||||
span.set_attribute("ai.model", model_name)
|
||||
|
||||
logger.info(
|
||||
"nvidia_chat_request_start",
|
||||
model=model_name,
|
||||
prompt_length=len(prompt),
|
||||
)
|
||||
|
||||
# Langfuse 追蹤
|
||||
with LangfuseTraceContext(
|
||||
name="nvidia_chat",
|
||||
metadata={"model": model_name, "task": "rca"},
|
||||
) as langfuse_ctx:
|
||||
try:
|
||||
client = await self._get_client()
|
||||
|
||||
response = await client.post(
|
||||
NVIDIA_API_URL,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self._api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"model": model_name,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
"response_format": {"type": "json_object"},
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
self._circuit_breaker.record_success()
|
||||
|
||||
text = data["choices"][0]["message"]["content"]
|
||||
|
||||
# Token 用量
|
||||
usage = data.get("usage", {})
|
||||
prompt_tokens = usage.get("prompt_tokens", 0)
|
||||
completion_tokens = usage.get("completion_tokens", 0)
|
||||
total_tokens = usage.get("total_tokens", prompt_tokens + completion_tokens)
|
||||
|
||||
# NVIDIA NIM 免費 tier = $0
|
||||
cost_usd = 0.0
|
||||
|
||||
latency_ms = (time.perf_counter() - start_time) * 1000
|
||||
span.set_attribute("ai.latency_ms", latency_ms)
|
||||
span.set_attribute("ai.total_tokens", total_tokens)
|
||||
|
||||
# Prometheus
|
||||
NVIDIA_REQUESTS_TOTAL.labels(status="success", tool_name="chat").inc()
|
||||
NVIDIA_LATENCY_SECONDS.labels(tool_name="chat").observe(latency_ms / 1000)
|
||||
|
||||
# Langfuse
|
||||
langfuse_ctx.trace.generation(
|
||||
name="nvidia_chat",
|
||||
model=model_name,
|
||||
input=prompt[:500],
|
||||
output=text[:500],
|
||||
metadata={
|
||||
"total_tokens": total_tokens,
|
||||
"cost_usd": cost_usd,
|
||||
"latency_ms": round(latency_ms, 2),
|
||||
},
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"nvidia_chat_response_received",
|
||||
model=model_name,
|
||||
response_length=len(text),
|
||||
prompt_tokens=prompt_tokens,
|
||||
completion_tokens=completion_tokens,
|
||||
total_tokens=total_tokens,
|
||||
latency_ms=round(latency_ms, 2),
|
||||
)
|
||||
|
||||
return text, True, total_tokens, cost_usd
|
||||
|
||||
except httpx.TimeoutException as e:
|
||||
self._circuit_breaker.record_failure()
|
||||
NVIDIA_REQUESTS_TOTAL.labels(status="timeout", tool_name="chat").inc()
|
||||
logger.warning("nvidia_chat_timeout", error=str(e))
|
||||
return f"Timeout: {e}", False, 0, 0.0
|
||||
|
||||
except Exception as e:
|
||||
self._circuit_breaker.record_failure()
|
||||
NVIDIA_REQUESTS_TOTAL.labels(status="error", tool_name="chat").inc()
|
||||
logger.warning("nvidia_chat_failed", error=str(e), error_type=type(e).__name__)
|
||||
return str(e), False, 0, 0.0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 單例與工廠函數
|
||||
|
||||
Reference in New Issue
Block a user