docs: update project docs and clean up redundant files

- PROGRESS.md: update to 2026-05-29 with security hardening (T-005),
  4-frontend architecture, AI provider refactoring, discovery features,
  landing page/referral/quota, desktop layout, admin AI management
- AGENTS.md: add AI provider list (Alibaba/NVIDIA, removed Claude/DeepL/Local),
  DB-driven config, CSRF/rate-limit/CORS notes, admin_ai reload quirk
- .env.example: sync with actual config, replace deprecated providers
  with current Sensenova/OpencodeGo/NVIDIA/Spark/Alibaba
- docs/PROJECT_STATUS.md: archive (fully superseded by PROGRESS.md)
- Remove generated JS files (_bing_search.js, _batch_search.js)
- Remove empty directories (data/corpus, data/models)
- Remove backend/.coverage (test artifact)
- Fix services/.gitignore to cover _bing_search.js
- Include pending AI provider DB admin feature (admin_ai, AIProvider model,
  AIProviders.vue, migration) and T-008 test report
This commit is contained in:
TradeMate Dev
2026-05-29 11:15:33 +08:00
parent c04fa2c19f
commit 5d2bced39f
31 changed files with 1933 additions and 816 deletions
+1 -4
View File
@@ -1,11 +1,8 @@
from .openai import OpenAIProvider
from .claude import ClaudeProvider
from .deepl import DeepLProvider
from .local import LocalProvider
from .spark import SparkProvider
from .sensenova import SensenovaProvider
from .opencode_go import OpencodeGoProvider
from .nvidia import NvidiaProvider
from .alibaba import AlibabaMTProvider
__all__ = ["OpenAIProvider", "ClaudeProvider", "DeepLProvider", "LocalProvider", "SparkProvider", "SensenovaProvider", "OpencodeGoProvider", "NvidiaProvider", "AlibabaMTProvider"]
__all__ = ["OpenAIProvider", "SparkProvider", "SensenovaProvider", "OpencodeGoProvider", "NvidiaProvider", "AlibabaMTProvider"]
-93
View File
@@ -1,93 +0,0 @@
from typing import Dict, Any, Optional
import json
from app.ai.base import AIProvider
SYSTEM_PROMPTS = {
"marketing": "You are a world-class copywriter for international trade. Write persuasive, "
"culturally-adapted marketing content that converts. You excel at storytelling "
"and emotional appeal in business contexts.",
"reply": "You are a senior international sales representative with 20 years of experience. "
"Your replies are warm, professional, and strategically move the conversation "
"toward closing the deal.",
"translate": "You are a professional translator specializing in trade documents. "
"Preserve all numbers, terms, and formatting. Translate meaning, not words.",
"extract": "Extract structured data from text. Return ONLY valid JSON.",
}
class ClaudeProvider(AIProvider):
def __init__(self, api_key: str, model: str = "claude-sonnet-4-20250514"):
try:
from anthropic import AsyncAnthropic
except ImportError:
raise ImportError(
"anthropic SDK is required for ClaudeProvider. "
"Install it with: pip install anthropic"
)
self.client = AsyncAnthropic(api_key=api_key)
self.model = model
self._name = f"claude-sonnet"
self._pricing = {"input": 0.003, "output": 0.015}
async def translate(self, text: str, source_lang: Optional[str], target_lang: str, context: Optional[str] = None) -> Dict[str, Any]:
system = SYSTEM_PROMPTS["translate"]
if context:
system += f"\nContext: {context}"
prompt = f"Translate to {target_lang}:\n\n{text}"
content = await self._call(system, prompt)
return {"translated_text": content, "provider": self.name}
async def reply(self, inquiry: str, context: Optional[Dict[str, Any]] = None, tone: str = "professional", preference_context: Optional[str] = None) -> Dict[str, Any]:
system = SYSTEM_PROMPTS["reply"]
if preference_context:
system += f"\nUser writing preference: {preference_context}"
context_str = ""
if context:
for k, v in context.items():
if v:
context_str += f"{k}: {v}\n"
prompt = f"{context_str}\nCustomer says:\n{inquiry}\n\nYour reply ({tone} tone):"
content = await self._call(system, prompt)
return {"reply": content, "provider": self.name}
async def generate_marketing(self, product_info: Dict[str, Any], target: str, style: str = "professional", language: str = "en", preference_context: Optional[str] = None) -> Dict[str, Any]:
system = SYSTEM_PROMPTS["marketing"]
if preference_context:
system += f"\nUser preference: {preference_context}"
info = json.dumps(product_info, ensure_ascii=False, indent=2)
prompt = f"Product:\n{info}\n\nTarget: {target}\nStyle: {style}\nLanguage: {language}\n\nWrite marketing copy:"
content = await self._call(system, prompt, max_tokens=1500)
return {"content": content, "provider": self.name}
async def extract_info(self, text: str, schema: Dict[str, Any]) -> Dict[str, Any]:
system = SYSTEM_PROMPTS["extract"]
prompt = f"Schema:\n{json.dumps(schema, indent=2)}\n\nText:\n{text}\n\nJSON:"
content = await self._call(system, prompt, max_tokens=1000)
try:
data = json.loads(content)
return {"data": data, "confidence": 0.9, "provider": self.name}
except json.JSONDecodeError:
return {"data": {}, "confidence": 0.0, "provider": self.name, "error": "parse_failed"}
async def _call(self, system: str, prompt: str, max_tokens: int = 1000) -> str:
resp = await self.client.messages.create(
model=self.model,
system=system,
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens,
temperature=0.7,
)
return resp.content[0].text
@property
def name(self) -> str:
return self._name
@property
def cost_per_1k_tokens(self) -> float:
return (self._pricing["input"] + self._pricing["output"]) / 2
@property
def supports_streaming(self) -> bool:
return True
-51
View File
@@ -1,51 +0,0 @@
from typing import Dict, Any, Optional
import httpx
from app.ai.base import AIProvider
class DeepLProvider(AIProvider):
def __init__(self, api_key: str, endpoint: str = "https://api.deepl.com/v2"):
self.api_key = api_key
self.endpoint = endpoint
self._name = "deepl"
self._cost_per_char = 0.000006
async def translate(self, text: str, source_lang: Optional[str], target_lang: str, context: Optional[str] = None) -> Dict[str, Any]:
params = {
"auth_key": self.api_key,
"text": text,
"target_lang": target_lang.upper()[:2],
}
if source_lang and source_lang != "auto":
params["source_lang"] = source_lang.upper()[:2]
async with httpx.AsyncClient() as client:
resp = await client.post(f"{self.endpoint}/translate", data=params, timeout=15)
resp.raise_for_status()
data = resp.json()
t = data["translations"][0]
return {
"translated_text": t["text"],
"provider": self.name,
"detected_source_lang": t.get("detected_source_language", source_lang),
"char_count": len(text),
"cost": len(text) * self._cost_per_char,
}
async def reply(self, inquiry: str, context: Optional[Dict[str, Any]] = None, tone: str = "professional") -> Dict[str, Any]:
raise NotImplementedError("DeepL does not support reply generation")
async def generate_marketing(self, product_info: Dict[str, Any], target: str, style: str = "professional", language: str = "en") -> Dict[str, Any]:
raise NotImplementedError("DeepL does not support marketing generation")
async def extract_info(self, text: str, schema: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError("DeepL does not support info extraction")
@property
def name(self) -> str:
return self._name
@property
def cost_per_1k_tokens(self) -> float:
return self._cost_per_char * 1000
-60
View File
@@ -1,60 +0,0 @@
from typing import Dict, Any, Optional
import json, httpx
from app.ai.base import AIProvider
class LocalProvider(AIProvider):
def __init__(self, model_url: str = "http://localhost:8001", model_name: str = "gemma-3-8b"):
self.model_url = model_url.rstrip("/")
self.model_name = model_name
self._name = f"local-{model_name}"
async def translate(self, text: str, source_lang: Optional[str], target_lang: str, context: Optional[str] = None) -> Dict[str, Any]:
prompt = f"Translate{ f' from {source_lang}' if source_lang else ''} to {target_lang}:\n{text}\n\nTranslation:"
result = await self._generate(prompt)
return {"translated_text": result, "provider": self.name, "cost": 0.0}
async def reply(self, inquiry: str, context: Optional[Dict[str, Any]] = None, tone: str = "professional", preference_context: Optional[str] = None) -> Dict[str, Any]:
prompt = ""
if preference_context:
prompt += f"[User prefers: {preference_context}]\n"
if context:
prompt += "\n".join(f"{k}: {v}" for k, v in context.items() if v) + "\n"
prompt += f"Customer: {inquiry}\n\nWrite a {tone} reply:"
result = await self._generate(prompt)
return {"reply": result, "provider": self.name, "cost": 0.0}
async def generate_marketing(self, product_info: Dict[str, Any], target: str, style: str = "professional", language: str = "en", preference_context: Optional[str] = None) -> Dict[str, Any]:
info = json.dumps(product_info, ensure_ascii=False)
prompt = ""
if preference_context:
prompt += f"[User prefers: {preference_context}]\n"
prompt += f"Product: {info}\nTarget: {target}\nStyle: {style}\nLanguage: {language}\n\nMarketing copy:"
result = await self._generate(prompt, max_tokens=800)
return {"content": result, "provider": self.name, "cost": 0.0}
async def extract_info(self, text: str, schema: Dict[str, Any]) -> Dict[str, Any]:
prompt = f"Extract JSON from text matching schema:\nSchema: {json.dumps(schema)}\n\nText: {text}\n\nJSON:"
result = await self._generate(prompt, max_tokens=500)
try:
return {"data": json.loads(result), "confidence": 0.7, "provider": self.name, "cost": 0.0}
except json.JSONDecodeError:
return {"data": {}, "confidence": 0.0, "provider": self.name, "cost": 0.0, "error": "parse_failed"}
async def _generate(self, prompt: str, max_tokens: int = 500) -> str:
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{self.model_url}/v1/completions",
json={"model": self.model_name, "prompt": prompt, "max_tokens": max_tokens, "temperature": 0.7, "stream": False},
timeout=60,
)
resp.raise_for_status()
return resp.json()["choices"][0]["text"].strip()
@property
def name(self) -> str:
return self._name
@property
def cost_per_1k_tokens(self) -> float:
return 0.0