9b9ee4964b
- 新增 assistant_actions.py 动作注册表(list_topics/get_system_stats/get_task_status/trigger_task/search_web 等) - assistant.py 增加规则意图识别优先执行,失败则走 LLM ACTION 格式 - 格式化返回结果,前端无感知 - 支持查看系统统计、选题、任务日志、手动触发定时任务
313 lines
14 KiB
Python
313 lines
14 KiB
Python
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} 完成。"
|