feat: AI assistant phase 2 - configurable prompt, action operations, FAQ matching, NVIDIA provider

- Admin-configurable AI prompt/quick questions from system_configs DB
- GET /api/v1/ai/quick-questions endpoint for fetching quick questions
- Local FAQ matching for instant responses (avoid AI calls for common Qs)
- AI action extraction: "add customer" intent detected, structured data returned
- Frontend action confirmation card with editable fields, calls customer API on confirm
- NVIDIA provider (stepfun-ai/step-3.5-flash) for faster chat vs deepseek-v4-flash
- Fixed httpx client timeout preventing backend hangs
- Added log_usage calls for auth events (register/login/guest/wechat)
- Admin tabs (users/stats/logs/config) fully functional with real backend
- AiAssistant component added to all tabbar pages
This commit is contained in:
TradeMate Dev
2026-05-20 09:39:22 +08:00
parent 4755cc75ba
commit f8a23855d2
20 changed files with 744 additions and 5 deletions
+2 -1
View File
@@ -5,5 +5,6 @@ from .local import LocalProvider
from .spark import SparkProvider
from .sensenova import SensenovaProvider
from .opencode_go import OpencodeGoProvider
from .nvidia import NvidiaProvider
__all__ = ["OpenAIProvider", "ClaudeProvider", "DeepLProvider", "LocalProvider", "SparkProvider", "SensenovaProvider", "OpencodeGoProvider"]
__all__ = ["OpenAIProvider", "ClaudeProvider", "DeepLProvider", "LocalProvider", "SparkProvider", "SensenovaProvider", "OpencodeGoProvider", "NvidiaProvider"]
+50
View File
@@ -0,0 +1,50 @@
from typing import Dict, Any, Optional, List
from app.ai.providers.openai import OpenAIProvider, SYSTEM_PROMPTS
import logging
import time
import httpx
logger = logging.getLogger(__name__)
class NvidiaProvider(OpenAIProvider):
def __init__(self, api_key: str, model: str = "stepfun-ai/step-3.5-flash", base_url: str = "https://integrate.api.nvidia.com/v1"):
super().__init__(
api_key=api_key,
model=model,
base_url=base_url,
http_client=httpx.AsyncClient(timeout=httpx.Timeout(60.0)),
)
self._name = f"nvidia-{model}"
async def chat(self, message: str, history: list = None, system_prompt: str = None) -> Dict[str, Any]:
t0 = time.time()
system = system_prompt or SYSTEM_PROMPTS["chat"]
messages = [{"role": "system", "content": system}]
if history:
for h in history[-10:]:
messages.append(h)
messages.append({"role": "user", "content": message})
t1 = time.time()
kwargs = {
"model": self.model,
"messages": messages,
"max_tokens": 300,
"temperature": 0.3,
}
resp = await self.client.chat.completions.create(**kwargs)
t2 = time.time()
content = resp.choices[0].message.content or ""
if not content and hasattr(resp.choices[0].message, "reasoning"):
content = resp.choices[0].message.reasoning
t3 = time.time()
logger.info(
f"NVIDIA timing: build_msgs={t1-t0:.1f}s api_call={t2-t1:.1f}s process={t3-t2:.1f}s "
f"chars_in={sum(len(m.get('content','')) for m in messages)} chars_out={len(content)}"
)
return {"reply": content, "provider": self.name, "model": self.model}
+31 -1
View File
@@ -14,11 +14,21 @@ SYSTEM_PROMPTS = {
"marketing content that drives action. Adapt to the target audience's culture. "
"Return ONLY the copy, no explanations.",
"extract": "You extract structured data from text. Return ONLY valid JSON matching the requested schema.",
"chat": "你是 TradeMate(外贸小助手)的 AI 助手。你的职责是帮助外贸从业者解答关于本工具使用的问题,以及提供外贸业务建议。\n"
"你可以回答的问题包括:\n"
"- 功能介绍:翻译、客户管理、产品管理、报价单、营销文案、WhatsApp 集成等\n"
"- 使用帮助:如何添加客户、如何生成报价单、如何导出数据等\n"
"- 外贸知识:贸易术语(FOB、CIF 等)、谈判技巧、跟进策略等\n\n"
"回答要求:\n"
"- 简洁扼要,用中文回答\n"
"- 涉及操作步骤时用数字列表说明\n"
"- 不确定的问题不要编造,直接说需要查证\n"
"- 语气友好专业",
}
class OpenAIProvider(AIProvider):
def __init__(self, api_key: str, model: str = "gpt-4o", base_url: Optional[str] = None):
def __init__(self, api_key: str, model: str = "gpt-4o", base_url: Optional[str] = None, http_client=None):
try:
from openai import AsyncOpenAI
except ImportError:
@@ -29,6 +39,8 @@ class OpenAIProvider(AIProvider):
kwargs = {"api_key": api_key}
if base_url:
kwargs["base_url"] = base_url
if http_client:
kwargs["http_client"] = http_client
self.client = AsyncOpenAI(**kwargs)
self.model = model
self._name = f"openai-{model}"
@@ -92,6 +104,24 @@ class OpenAIProvider(AIProvider):
except json.JSONDecodeError:
return {"data": {}, "confidence": 0.0, "provider": self.name, "error": "parse_failed"}
async def chat(self, message: str, history: list = None, system_prompt: str = None) -> Dict[str, Any]:
system = system_prompt or SYSTEM_PROMPTS["chat"]
messages = [{"role": "system", "content": system}]
if history:
for h in history[-10:]:
messages.append(h)
messages.append({"role": "user", "content": message})
kwargs = {
"model": self._cheap_model,
"messages": messages,
"max_tokens": 2000,
"temperature": 0.7,
}
resp = await self.client.chat.completions.create(**kwargs)
content = resp.choices[0].message.content or ""
return {"reply": content, "provider": self.name, "model": self.model}
async def _call(self, system: str, prompt: str, max_tokens: int = 3000, response_format: Optional[Dict] = None, model: Optional[str] = None) -> str:
kwargs = {
"model": model or self.model,