feat(ai): ADR-036 NVIDIA Nemotron Tool Calling 整合

Phase 20 - 提升 Tool Calling 精準度 50% → 83.3%

新增:
- src/models/nvidia.py: Pydantic Schema
- src/services/nvidia_provider.py: NvidiaProvider 類別
- tests/test_nvidia_provider.py: 15 項單元測試 (全部通過)

修改:
- ai_router.py: AIProvider.NVIDIA + route_tool_calling()
- ai_rate_limiter.py: NVIDIA 限制 (5 RPM, 100/day)
- models.json: NVIDIA 配置
- cd.yaml: Secrets 注入 NVIDIA_API_KEY

路由策略:
- Tool Calling: Nemotron → Gemini → Claude
- 一般對話: Ollama → Gemini → Claude (不變)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-29 00:00:08 +08:00
parent dc7daf5d81
commit b77e151387
11 changed files with 1083 additions and 16 deletions

View File

@@ -0,0 +1,119 @@
"""
NVIDIA Nemotron API Models - ADR-036
====================================
2026-03-29 ogt: Nemotron Tool Calling 整合 (83.3% 精準度)
OpenAI 相容格式 - 用於 Tool Calling 任務
"""
from typing import Any
from pydantic import BaseModel, Field
class ToolFunction(BaseModel):
"""Tool Function 定義"""
name: str = Field(..., description="Tool 函數名稱")
arguments: str = Field(..., description="Tool 參數 (JSON 字串)")
class ToolCall(BaseModel):
"""Tool Call 結構"""
id: str = Field(..., description="Tool Call ID")
type: str = Field(default="function", description="Tool 類型")
function: ToolFunction = Field(..., description="Tool 函數")
class NvidiaMessage(BaseModel):
"""NVIDIA API Message 結構"""
role: str = Field(..., description="訊息角色 (assistant/user/system)")
content: str | None = Field(default=None, description="訊息內容")
tool_calls: list[ToolCall] | None = Field(
default=None, description="Tool Calls (僅 assistant)"
)
class NvidiaChoice(BaseModel):
"""NVIDIA API Choice 結構"""
index: int = Field(default=0, description="選項索引")
message: NvidiaMessage = Field(..., description="回應訊息")
finish_reason: str | None = Field(
default=None, description="結束原因 (stop/tool_calls)"
)
class NvidiaUsage(BaseModel):
"""NVIDIA API Token 使用統計"""
prompt_tokens: int = Field(default=0, description="輸入 Token 數")
completion_tokens: int = Field(default=0, description="輸出 Token 數")
total_tokens: int = Field(default=0, description="總 Token 數")
class NvidiaResponse(BaseModel):
"""NVIDIA Nemotron API 完整回應"""
id: str = Field(..., description="回應 ID")
object: str = Field(default="chat.completion", description="物件類型")
created: int = Field(..., description="建立時間戳")
model: str = Field(..., description="模型名稱")
choices: list[NvidiaChoice] = Field(..., description="回應選項")
usage: NvidiaUsage | None = Field(default=None, description="Token 使用統計")
# === Tool Calling 請求結構 ===
class ToolDefinition(BaseModel):
"""Tool 定義 (發送給 API)"""
type: str = Field(default="function", description="Tool 類型")
function: dict[str, Any] = Field(..., description="函數定義 (JSON Schema)")
class NvidiaToolCallRequest(BaseModel):
"""NVIDIA Tool Calling 請求"""
model: str = Field(
default="nvidia/llama-3.1-nemotron-70b-instruct",
description="模型名稱",
)
messages: list[dict[str, Any]] = Field(..., description="對話訊息")
tools: list[ToolDefinition] = Field(..., description="可用 Tools")
tool_choice: str | dict[str, Any] = Field(
default="auto", description="Tool 選擇策略"
)
temperature: float = Field(default=0.0, description="溫度 (0.0 最確定性)")
max_tokens: int = Field(default=1024, description="最大輸出 Token")
# === 驗證結果結構 ===
class ToolCallValidationResult(BaseModel):
"""Tool Call 驗證結果"""
valid: bool = Field(..., description="是否有效")
tool_name: str | None = Field(default=None, description="Tool 名稱")
arguments: dict[str, Any] | None = Field(default=None, description="解析後參數")
error: str | None = Field(default=None, description="錯誤訊息")
raw_response: str | None = Field(default=None, description="原始回應 (debug)")
class NvidiaProviderResult(BaseModel):
"""NvidiaProvider 回傳結果"""
success: bool = Field(..., description="是否成功")
tool_calls: list[ToolCallValidationResult] = Field(
default_factory=list, description="驗證後的 Tool Calls"
)
usage: NvidiaUsage | None = Field(default=None, description="Token 使用統計")
latency_ms: float = Field(default=0.0, description="延遲 (毫秒)")
error: str | None = Field(default=None, description="錯誤訊息")
fallback_triggered: bool = Field(
default=False, description="是否觸發 Fallback"
)