import re import json from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from typing import List, Optional, Dict, Any from ..core.nvidia_client import call_llm from .auth import get_current_user from .assistant_actions import ACTION_REGISTRY router = APIRouter(prefix="/api/assistant", tags=["assistant"]) PAGE_CONTEXTS = { "dashboard": "仪表盘页面:显示系统概览统计(选题总数、待处理、待审查、待发布、已发布、今日新增)、模块状态卡片(6个定时任务的运行状态)、近期计划列表", "topics": "选题管理页面:创建和管理内容选题,支持筛选、排序、评分、编辑、预览等功能", "metrics": "数据分析页面:展示各平台内容表现数据,包括浏览量、点赞、收藏、评论等指标的趋势图表", "calendar": "内容日历页面:以日历形式展示内容发布计划,支持创建、编辑、拖拽调整日期", "assets": "素材管理页面:管理图片、文件等媒体素材,支持上传、标签分类、关联选题", "tasks": "任务管理页面:查看内容创作任务的执行状态,包括自动采集、同步、生成、优化等任务", "platforms": "平台配置页面:管理各发布平台的配置,包括平台名称、API端点、发布格式等", "admin": "系统管理页面:管理用户、LLM配置、系统配置、采集类别、信息源、组织、查看运行日志等", } # 可用操作清单(用于系统提示词) from .assistant_actions import ACTION_REGISTRY ACTION_DESCRIPTIONS = "\n".join([f"- {name}: {info['description']}" for name, info in ACTION_REGISTRY.items()]) class ChatRequest(BaseModel): message: str page: str = "dashboard" history: List[dict] = [] def _execute_action(action_name: str, params: Dict[str, Any], current_user) -> Dict[str, Any]: """执行一个具体的操作""" if action_name not in ACTION_REGISTRY: return {"error": f"未知操作: {action_name}"} func = ACTION_REGISTRY[action_name]["func"] try: result = func(**params) return {"success": True, "result": result} except Exception as e: import traceback return {"error": str(e), "traceback": traceback.format_exc()} @router.post("/chat") def chat(request: ChatRequest, current_user=Depends(get_current_user)): user_message = request.message.strip() # 1. 意图识别(规则优先):用户问题涉及系统数据,直接调用工具,避免 LLM 漏掉 ACTION intent, params = _detect_intent(user_message) if intent: result = _execute_action(intent, params, current_user) reply = _format_action_result(intent, params, result) return {"reply": reply, "actions": [{"action": intent, "params": params, "result": result}]} # 2. 走 LLM 对话(让 LLM 生成 ACTION) try: from ..database import SessionLocal from ..models import SystemConfig, User db = SessionLocal() sc = db.query(SystemConfig).filter(SystemConfig.key == "assistant_system_prompt").first() base_prompt = sc.value if sc else None user_rec = db.query(User).filter(User.id == getattr(current_user, 'id', None)).first() username = user_rec.username if user_rec else (getattr(current_user, 'username', '用户')) db.close() except Exception: base_prompt = None username = getattr(current_user, 'username', '用户') page_name = request.page page_guide = PAGE_CONTEXTS.get(page_name, "未知页面") system_prompt = base_prompt or f"""你是一个智能内容创作平台助手,能够通过调用工具与系统实时交互,为用户提供准确的数据和操作。 当前用户 {username} 正在查看:{page_guide} **重要规则**:对于任何涉及系统数据(选题、任务、统计、配置等)的问题,你必须使用提供的工具查询最新数据,不要依赖你的训练数据或猜测。 可用工具: {ACTION_DESCRIPTIONS} **调用格式(必须严格遵守)**: 当需要执行工具时,在你的回复中包含单独一行(不要在行内有其他文字): **ACTION: action_name** {{"param1": "value1", ...}} 示例对话: 用户:系统现在有多少选题? 助手:稍等,我为你查询一下。 **ACTION: get_system_stats** {{}} 用户:我想看最近5条任务日志 助手:好的。 **ACTION: get_recent_task_logs** {{"limit":5}} 工具执行后,系统会返回结果,你再根据结果生成最终回复(如「当前选题总数是42个」)。 注意: - 必须严格使用上述格式,工具名和参数必须匹配 - 参数必须是合法JSON,缺失必要参数时应询问用户 - 如果用户问题模糊,先澄清再调用工具 - 回复内容简洁友好,不超过200字 """ messages = request.history[-20:] conversation = "\n".join([f"{'用户' if m.get('role') == 'user' else '助手'}: {m.get('content', '')}" for m in messages]) prompt_text = f"{conversation}\n用户: {request.message}\n助手:" try: reply = call_llm(prompt_text, system_prompt=system_prompt, temperature=0.5, max_tokens=1500) # 检测是否有操作指令(支持多行) action_pattern = re.compile(r'\*\*ACTION:\s*(\w+)\*\*\s*(\{.*\})', re.DOTALL) action_matches = list(action_pattern.finditer(reply)) action_results = [] processed_reply = reply for match in action_matches: action_name = match.group(1).strip() args_str = match.group(2).strip() try: args = json.loads(args_str) # 执行操作 exec_result = _execute_action(action_name, args, current_user) action_results.append({ "action": action_name, "params": args, "result": exec_result }) # 从回复中移除 ACTION 标记行 processed_reply = processed_reply.replace(match.group(0), '').strip() except json.JSONDecodeError: action_results.append({"action": action_name, "error": "参数JSON格式错误"}) # 如果有执行结果,追加简要说明到回复中 if action_results: summary_lines = [] for ar in action_results: if "error" in ar["result"]: summary_lines.append(f"❌ 操作 {ar['action']} 失败: {ar['result']['error']}") else: summary_lines.append(f"✅ 操作 {ar['action']} 已完成。") processed_reply = processed_reply + "\n\n" + "\n".join(summary_lines) return {"reply": processed_reply.strip(), "actions": action_results} except Exception as e: return {"reply": f"抱歉,AI助手暂时无法响应,请稍后重试。"} def _detect_intent(message: str) -> (str, Dict[str, Any]): """基于关键词的意图识别,返回 (action_name, params) 或 (None, {{}})""" msg = message.lower() # 查询系统统计(选题总数、各状态数量、案例数) if any(k in msg for k in ['统计', '总数', '多少选题', '选题数量', '系统数据', '数据总览']): return "get_system_stats", {} # 查看选题列表 if any(k in msg for k in ['选题列表', '查看选题', '选题有哪些', '列出选题', '选题']): # 默认返回第一页20条,可后续细化 page = 1 page_size = 10 # 检测是否指定状态 status = None if any(k in msg for k in ['待处理', '待审核', '待审查', '待发布', '已发布']): status_map = {'待处理': 'pending', '待审查': 'review', '待发布': 'ready', '已发布': 'published'} for k, v in status_map.items(): if k in msg: status = v break page_size = 20 if '全部' in msg else page_size return "list_topics", {"page": page, "page_size": page_size, "status": status, "field": None} # 查询特定选题 if any(k in msg for k in ['查看选题', '选题详情', '选题详情', '获取选题', '选题id']): # 尝试提取ID(大写字母+数字 或 LIVING/TECH/M0604 等格式) import re match = re.search(r'[A-Z0-9\-]{4,}', msg) if match: topic_id = match.group(0) return "get_topic", {"topic_id": topic_id} return None, {} # 最近任务日志 if any(k in msg for k in ['任务日志', '执行记录', '任务记录', '最近任务']): limit = 5 if '最近' in msg or '查看' in msg else 10 # 检查是否指定模块 module_id = None for mod in ['collect', 'generate', 'optimize', 'metrics', 'trends']: if mod in msg: module_id = f"scheduled_{mod}" break return "get_task_status", {"module_id": module_id} if module_id else {"module_id": None} # 手动触发任务 if any(k in msg for k in ['触发', '运行', '执行', '采集', '生成', '优化', '指标']): module_map = { 'collect': 'scheduled_collect', '采集': 'scheduled_collect', 'generate': 'scheduled_generate', '生成': 'scheduled_generate', 'optimize': 'scheduled_optimize', '优化': 'scheduled_optimize', 'optimize_sources': 'scheduled_optimize_sources', 'source': 'scheduled_optimize_sources', 'metrics': 'scheduled_metrics_sync', '指标': 'scheduled_metrics_sync', 'trend': 'scheduled_trends', '趋势': 'scheduled_trends' } for key, val in module_map.items(): if key in msg: return "trigger_task", {"module_id": val} # 如果无法识别具体模块,不触发 return None, {} # 搜索 if any(k in msg for k in ['搜索', '查询', '查找']): # 提取搜索词(简单:把问题其余部分作为查询) # 这里较难简单提取,暂不自动触发,留给 LLM return None, {} # 默认不触发 return None, {} def _format_action_result(action_name: str, params: Dict[str, Any], result: Dict[str, Any]) -> str: """将工具执行结果格式化为自然语言回复""" if "error" in result: return f"⚠️ 执行 {action_name} 失败:{result['error']}" res = result.get("result", result) if action_name == "get_system_stats": t = res.get("topics", {}) return (f"📊 系统统计:\n" f"• 选题总数:{t.get('total', 0)}\n" f"• 待处理:{t.get('pending', 0)}\n" f"• 待审查:{t.get('review', 0)}\n" f"• 草稿:{t.get('draft', 0)}\n" f"• 待发布:{t.get('ready', 0)}\n" f"• 已发布:{t.get('published', 0)}\n" f"• 本周新增:{t.get('new_this_week', 0)}\n" f"• 案例总数:{res.get('cases', 0)}") if action_name == "list_topics": items = res.get("items", []) total = res.get("total", 0) page = res.get("page", 1) if not items: return f"当前没有符合条件的选题(总计 {total} 条)。" lines = [f"📋 选题列表(第 {page} 页,显示 {len(items)} 条,共 {total} 条):"] for it in items: lines.append(f" • {it['id']}:{it['title'][:30]}... [{it['status']}]") return "\n".join(lines) if action_name == "get_topic": if "error" in res: return f"❌ {res['error']}" t = res return (f"📄 选题详情 [{t['id']}]:\n" f"标题:{t['title']}\n" f"领域:{t['field']}\n" f"状态:{t['status']}\n" f"优先级:{t['priority']} (分:{t['priority_score']})") if action_name == "get_recent_task_logs": logs = res[:5] if isinstance(res, list) else [] if not logs: return "暂无任务日志。" lines = ["📝 最近任务记录:"] for l in logs: status_icon = {"success":"✅","failed":"❌","running":"🔄"}.get(l['status'],"⬜") time_str = l['started_at'][:10] if l.get('started_at') else "?" lines.append(f" {status_icon} {l['module_id']} - {l['status']} ({time_str})") return "\n".join(lines) if action_name == "trigger_task": if "error" in res: return f"❌ 触发失败:{res['error']}" return f"⏳ 已触发任务:{params.get('module_id')},正在后台执行。" if action_name == "search_web": if "error" in res: return f"❌ 搜索失败:{res['error']}" results = res.get("results", []) count = res.get("count", len(results)) if not results: return "未找到相关搜索结果。" lines = [f"🔍 搜索到 {count} 条结果:"] for i, r in enumerate(results[:3], 1): lines.append(f" {i}. {r.get('title','无标题')}\n {r.get('url','')}") if count > 3: lines.append(f" ... 还有 {count-3} 条") return "\n".join(lines) if action_name == "get_task_status": logs = res[:5] if isinstance(res, list) else [] if not logs: return "未找到任务日志。" lines = ["📋 任务状态:"] for l in logs: status_icon = {"success":"✅","failed":"❌","running":"🔄"}.get(l['status'],"⬜") time_str = l['started_at'][:10] if l.get('started_at') else "?" msg = (l.get('message') or '') msg_snippet = (msg[:30] + '...') if len(msg) > 30 else msg[:30] lines.append(f" {status_icon} {l['module_id']} - {l['status']} ({time_str})") if msg_snippet: lines.append(f" {msg_snippet}") return "\n".join(lines) # 默认返回原始结果 return f"✅ 操作 {action_name} 完成。"