from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from typing import Optional, List, Dict, Any from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.database import get_db from app.ai.router import get_ai_router from app.ai.local_faq import match_faq from app.api.v1.deps import get_current_user_id from app.models.system_config import SystemConfig from app.services.admin import AdminService import logging import time import re import json logger = logging.getLogger(__name__) router = APIRouter() ACTION_INSTRUCTIONS = """ 当用户想要执行操作时(如添加客户、创建产品、生成报价单、发送跟进、营销生成等),请执行以下步骤: 1. 从用户消息中提取所有必要的信息 2. 在回复末尾附上 JSON 格式的动作块,格式如下: ```actions [{"type": "create_customer", "label": "添加客户", "fields": {"name": "...", "phone": "...", "email": "...", "company": "...", "country": "...", "notes": "..."}}] ``` 支持的 action type 及字段说明: - create_customer:添加客户,fields 支持 name(必填), phone, email, company, country, website, notes - create_product:添加产品,fields 支持 name(必填), name_en, description, description_en, category, price, price_unit(默认USD), moq, keywords(逗号分隔) - create_quotation:生成报价单,fields 支持 customer_name(必填), product_info(必填), quantity(必填), price, terms - scan_followups:扫描待跟进客户,fields 不需要(空对象) - send_followup:发送跟进消息,fields 支持 customer_name(必填), message(必填) - generate_marketing:生成营销素材,fields 支持 product_name(必填), target_market, tone(如professional/casual), language - discovery_search:搜索潜在客户,fields 支持 keywords(必填), country, industry - navigate:跳转到指定页面,fields 支持 path(必填, 如 /customers /products /quotations /marketing /discovery /followup /translate /team /analytics) - search_users:搜索用户,fields 支持 query(必填) - update_user:修改用户信息,fields 支持 user_id(必填), username, phone, email, role, status - update_config:更新系统配置,fields 支持 key(必填), value(必填) - review_certification:审核认证,fields 支持 id(必填), action(approved/rejected), reason - process_invoice:处理发票,fields 支持 id(必填), action(approve/reject) 如果用户没有提供足够信息,请先询问缺少的字段,不要生成 action。 如果用户明确表示要执行操作但缺少信息,生成 action 但标注缺失的字段。 """ class ChatRequest(BaseModel): message: str history: Optional[List[Dict[str, str]]] = None @router.get("/quick-questions") async def get_quick_questions( db: AsyncSession = Depends(get_db), ): result = await db.execute( select(SystemConfig).where(SystemConfig.key == "ai_assistant_quick_questions") ) cfg = result.scalar_one_or_none() return cfg.value if cfg and cfg.value else [] @router.post("/chat") async def chat( data: ChatRequest, request: Request, user_id: str = Depends(get_current_user_id), db: AsyncSession = Depends(get_db), ): t_start = time.time() if not data.message.strip(): raise HTTPException(status_code=422, detail="Message is required") t0 = time.time() prompt_config = await db.execute( select(SystemConfig).where(SystemConfig.key == "ai_assistant_prompt") ) quick_config = await db.execute( select(SystemConfig).where(SystemConfig.key == "ai_assistant_quick_questions") ) t1 = time.time() prompt_row = prompt_config.scalar_one_or_none() base_prompt = prompt_row.value if prompt_row else None quick_row = quick_config.scalar_one_or_none() quick_questions = quick_row.value if quick_row else None t2 = time.time() action_keywords = ["帮我添加", "帮我创建", "帮我新增", "帮我新建", "帮我录入", "帮.+加", "添加客户.*电话", "创建客户.*电话"] needs_action = any(re.search(k, data.message) for k in action_keywords) system_prompt = base_prompt or "" if needs_action: system_prompt += "\n\n" + ACTION_INSTRUCTIONS faq_answer = match_faq(data.message) if faq_answer and not needs_action: reply = faq_answer provider = "local-faq" actions = [] t4 = time.time() logger.info( f"CHAT [{user_id[:8]}] FAQ match | " f"db={t1-t0:.2f}s orm={t2-t1:.2f}s total={t4-t_start:.2f}s" ) else: t3 = time.time() ai = get_ai_router() result = await ai.chat(data.message, data.history or [], system_prompt) t4 = time.time() reply = result.get("reply", "") provider = result.get("provider_used", "") actions = [] action_match = re.search(r'```actions\s*\n(.*?)\n```', reply, re.DOTALL) if action_match: try: actions = json.loads(action_match.group(1)) reply = reply[:action_match.start()].strip() except (json.JSONDecodeError, Exception) as e: logger.warning(f"Failed to parse actions: {e}") logger.info( f"CHAT [{user_id[:8]}] AI | " f"db={t1-t0:.2f}s orm={t2-t1:.2f}s setup={t3-t2:.2f}s ai={t4-t3:.2f}s total={t4-t_start:.2f}s" ) client_ip = request.client.host if request.client else None await AdminService(db).log_usage( user_id, "ai.chat", {"message": data.message[:200], "reply_length": len(reply), "provider": provider}, ip=client_ip, ) logger.info(f"CHAT [{user_id[:8]}] done total={time.time()-t_start:.2f}s") return { "reply": reply, "provider": provider, "quick_questions": quick_questions, "actions": actions, }