前台8项体验修复: 知识库上传自动刷新; 资产PDF归类; Token驾驶舱媒体统计; AI模糊意图澄清选择; ChatPage增加MCP/附件上传/语音; 技能市场安装态修复+清理演示数据; MCP协议应用改名MCP应用; Agent单应用接入(名称/介绍/API Key)
This commit is contained in:
1 parent
463824480b
commit
bc2899ed09
11 files changed
+945
-154
No files matched your search
@@ -66,6 +66,50 @@ def detect_media_intent(text: str) -> str | None:
|
||||
return "audio"
|
||||
return None
|
||||
|
||||
|
||||
def detect_ambiguous_creative_intent(text: str):
|
||||
"""检测「模糊创作意图」:用户使用了创作类动词(制作/创作/做/生成/画/写/设计…),
|
||||
但未明确产出形式(文案/图片/视频/其他)。此时应与用户确认方向,而不是直接调用大模型
|
||||
(某些模型会对这类开放式创作请求返回 "high risk" 拒答)。
|
||||
|
||||
返回 None 表示无需确认;否则返回 {"options": [...]}。
|
||||
"""
|
||||
if not text:
|
||||
return None
|
||||
t = text.strip()
|
||||
tl = t.lower()
|
||||
# 已有明确媒体意图(图像/视频/语音)则无需确认
|
||||
if detect_media_intent(t):
|
||||
return None
|
||||
# 疑问句不触发
|
||||
if tl.endswith(("?", "?")) or any(q in tl for q in ("吗", "呢", "怎么", "如何", "能否", "是否可以", "可以吗", "会不会", "why", "how", "什么", "哪些")):
|
||||
return None
|
||||
# 已明确给出产出形式(文字类/其他类)则无需确认
|
||||
explicit_medium = (
|
||||
"文案", "文章", "诗歌", "诗", "小说", "故事", "代码", "程序", "脚本", "总结", "翻译",
|
||||
"邮件", "报告", "段落", "正文", "文字", "写作", "ppt", "演示", "思维导图", "表格",
|
||||
"图表", "数据",
|
||||
)
|
||||
if any(m in tl for m in explicit_medium):
|
||||
return None
|
||||
# 创作类动词
|
||||
creative_verbs = (
|
||||
"制作", "创作", "做个", "做一个", "搞一个", "产出", "构思", "设想",
|
||||
"打造", "塑造", "生成", "帮我做", "帮我生成", "帮我画", "帮我写", "请制作",
|
||||
"请创作", "请生成", "请画", "请写", "来一个", "来一张", "来一幅", "做一张",
|
||||
"做张", "写一首", "写一段", "写个",
|
||||
)
|
||||
if not any(v in t for v in creative_verbs):
|
||||
return None
|
||||
# 模糊:有创作动词但无明确产出形式 → 提供方向选择
|
||||
options = [
|
||||
{"key": "text", "label": "文案", "desc": "文章 / 故事 / 诗歌等文字内容", "medium": "文案"},
|
||||
{"key": "image", "label": "图片", "desc": "插画 / 海报 / 照片等图像", "medium": "图片"},
|
||||
{"key": "video", "label": "视频", "desc": "短片 / 动画等视频", "medium": "视频"},
|
||||
{"key": "other", "label": "其他", "desc": "语音 / 代码 / 其他形式", "medium": "其他"},
|
||||
]
|
||||
return {"options": options}
|
||||
|
||||
# CST timezone (UTC+8) for consistent time display
|
||||
CST_TZ = timezone(timedelta(hours=8))
|
||||
|
||||
@@ -243,6 +287,8 @@ class SendMessageRequest(BaseModel):
|
||||
skill_ids: Optional[list] = None
|
||||
agent_ids: Optional[list] = None
|
||||
knowledge_base_ids: Optional[list] = None
|
||||
mcp_ids: Optional[list] = None
|
||||
attachments: Optional[list] = None
|
||||
use_tools: Optional[bool] = False
|
||||
|
||||
|
||||
@@ -374,7 +420,36 @@ def _resolve_market_agents(db: Session, agent_ids: list, user_id: str) -> list:
|
||||
return results
|
||||
|
||||
|
||||
def _parse_skill_content(content: str) -> dict:
|
||||
def _resolve_market_mcp(db: Session, mcp_ids: list, user_id: str) -> list:
|
||||
"""
|
||||
加载用户选择的 MCP 应用配置(MarketApp.type == 'mcp')。
|
||||
返回: [{id, name, transport, endpoint, description}]
|
||||
"""
|
||||
if not mcp_ids:
|
||||
return []
|
||||
|
||||
mcps = db.query(MarketApp).filter(
|
||||
MarketApp.id.in_(mcp_ids),
|
||||
MarketApp.type == "mcp"
|
||||
).all()
|
||||
|
||||
results = []
|
||||
for m in mcps:
|
||||
config = {}
|
||||
try:
|
||||
if m.skill_content:
|
||||
config = json.loads(m.skill_content)
|
||||
except Exception:
|
||||
config = {}
|
||||
results.append({
|
||||
"id": m.id,
|
||||
"name": m.name,
|
||||
"transport": m.mcp_transport or config.get("transport", "stdio"),
|
||||
"endpoint": m.agent_endpoint or config.get("endpoint", "") or config.get("url", ""),
|
||||
"description": m.description or config.get("description", ""),
|
||||
})
|
||||
|
||||
return results
|
||||
"""
|
||||
解析SKILL.md格式内容, 提取结构化信息。
|
||||
SKILL.md格式:
|
||||
@@ -651,6 +726,7 @@ def _build_system_prompt(
|
||||
agents: list,
|
||||
kb_context: str,
|
||||
user_content: str,
|
||||
mcp: list = None,
|
||||
) -> tuple:
|
||||
"""
|
||||
动态组装system prompt + function definitions。
|
||||
@@ -736,6 +812,12 @@ def _build_system_prompt(
|
||||
if kb_context:
|
||||
system_parts.append(f"参考以下知识库内容回答用户问题:\n{kb_context}")
|
||||
|
||||
# MCP 应用注入
|
||||
if mcp:
|
||||
mcp_descs = ", ".join([f"{s['name']}({s.get('transport', 'stdio')})" for s in mcp])
|
||||
system_parts.append(f"已连接的 MCP 应用(外部工具服务): {mcp_descs}")
|
||||
system_parts.append("当用户请求需要调用外部系统/工具(如查询数据库、调用API、操作第三方服务)时,可参考上述 MCP 应用提供的能力;若平台已接入对应 MCP 工具,优先通过工具调用完成。")
|
||||
|
||||
system_prompt = "\n".join(system_parts)
|
||||
return system_prompt, function_definitions
|
||||
|
||||
@@ -882,6 +964,8 @@ def _get_conversation_context(db: Session, conv_id: str) -> dict:
|
||||
context["agent_ids"] = first_tool_msg.agent_ids
|
||||
if first_tool_msg.knowledge_ids:
|
||||
context["knowledge_base_ids"] = first_tool_msg.knowledge_ids
|
||||
if getattr(first_tool_msg, "mcp_ids", None):
|
||||
context["mcp_ids"] = first_tool_msg.mcp_ids
|
||||
|
||||
return context
|
||||
|
||||
@@ -1068,33 +1152,88 @@ async def send_message(
|
||||
effective_skill_ids = req.skill_ids or []
|
||||
effective_agent_ids = req.agent_ids or []
|
||||
effective_kb_ids = req.knowledge_base_ids or []
|
||||
effective_mcp_ids = req.mcp_ids or []
|
||||
effective_use_tools = req.use_tools or False
|
||||
|
||||
if not effective_skill_ids and not effective_agent_ids and not effective_kb_ids:
|
||||
if not effective_skill_ids and not effective_agent_ids and not effective_kb_ids and not effective_mcp_ids:
|
||||
# 从对话上下文继承
|
||||
prev_context = _get_conversation_context(db, conv_id)
|
||||
if prev_context:
|
||||
effective_skill_ids = prev_context.get("skill_ids", [])
|
||||
effective_agent_ids = prev_context.get("agent_ids", [])
|
||||
effective_kb_ids = prev_context.get("knowledge_base_ids", [])
|
||||
if effective_skill_ids or effective_agent_ids:
|
||||
effective_mcp_ids = prev_context.get("mcp_ids", [])
|
||||
if effective_skill_ids or effective_agent_ids or effective_mcp_ids:
|
||||
effective_use_tools = True
|
||||
|
||||
# 保存用户消息(含skill/agent/kb选择用于上下文保持)
|
||||
# 附件内容拼接到用户消息中(文本类文件附带内容, 供模型参考)
|
||||
attachment_block = ""
|
||||
if req.attachments:
|
||||
parts = []
|
||||
for att in req.attachments:
|
||||
name = att.get("name", "附件") if isinstance(att, dict) else str(att)
|
||||
ftype = att.get("file_type", "") if isinstance(att, dict) else ""
|
||||
content = att.get("content", "") if isinstance(att, dict) else ""
|
||||
if content:
|
||||
parts.append(f"【附件: {name}】\n{content}")
|
||||
else:
|
||||
parts.append(f"【附件(二进制, 名称: {name}, 类型: {ftype})】")
|
||||
if parts:
|
||||
attachment_block = "\n\n" + "\n\n".join(parts)
|
||||
|
||||
# 保存用户消息(含skill/agent/kb/mcp选择用于上下文保持)
|
||||
user_msg = Message(
|
||||
id=gen_uuid(),
|
||||
conversation_id=conv_id,
|
||||
role="user",
|
||||
content=req.content,
|
||||
content=req.content + attachment_block,
|
||||
token_used=len(req.content) // 2,
|
||||
model_id=req.model_id or conv.model_id,
|
||||
skill_ids=req.skill_ids if req.skill_ids else None,
|
||||
agent_ids=req.agent_ids if req.agent_ids else None,
|
||||
knowledge_ids=req.knowledge_base_ids if req.knowledge_base_ids else None,
|
||||
mcp_ids=req.mcp_ids if req.mcp_ids else None,
|
||||
attachments=req.attachments if req.attachments else None,
|
||||
created_at=cst_now(),
|
||||
)
|
||||
db.add(user_msg)
|
||||
|
||||
# ===== 模糊创作意图拦截 =====
|
||||
# 用户使用创作类动词(制作/创作/做/生成…)但未明确产出形式时,先与用户确认方向,
|
||||
# 避免直接调用大模型被内容安全策略拒答(如返回 "high risk")。
|
||||
clarify = detect_ambiguous_creative_intent(req.content)
|
||||
if clarify:
|
||||
clarify_content = "<<CLARIFY>>" + json.dumps(
|
||||
{"prompt": req.content, "options": clarify["options"]}, ensure_ascii=False
|
||||
) + "<<CLARIFY>>"
|
||||
ai_msg = Message(
|
||||
id=gen_uuid(),
|
||||
conversation_id=conv_id,
|
||||
role="assistant",
|
||||
content=clarify_content,
|
||||
token_used=0,
|
||||
model_id=req.model_id or conv.model_id,
|
||||
created_at=cst_now(),
|
||||
)
|
||||
db.add(ai_msg)
|
||||
db.commit()
|
||||
db.refresh(user_msg)
|
||||
db.refresh(ai_msg)
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"user_message": {
|
||||
"id": user_msg.id, "role": user_msg.role, "content": user_msg.content,
|
||||
"token_used": user_msg.token_used, "created_at": str(user_msg.created_at),
|
||||
},
|
||||
"assistant_message": {
|
||||
"id": ai_msg.id, "role": ai_msg.role, "content": clarify_content,
|
||||
"created_at": str(ai_msg.created_at), "clarify": clarify,
|
||||
},
|
||||
},
|
||||
"message": "请选择创作方向",
|
||||
}
|
||||
|
||||
# ===== 核心聊天引擎 =====
|
||||
user_id = current_user.id
|
||||
model_id = req.model_id or conv.model_id
|
||||
@@ -1140,6 +1279,7 @@ async def send_message(
|
||||
# Stage A+B: 解析Skills和Agents
|
||||
skills = _resolve_market_skills(db, effective_skill_ids, user_id)
|
||||
agents = _resolve_market_agents(db, effective_agent_ids, user_id)
|
||||
mcps = _resolve_market_mcp(db, effective_mcp_ids, user_id)
|
||||
|
||||
# Stage B.1: Agent可能自带skill_ids和kb_ids
|
||||
agent_extra_skill_ids = []
|
||||
@@ -1171,6 +1311,7 @@ async def send_message(
|
||||
agents=agents,
|
||||
kb_context=kb_context,
|
||||
user_content=req.content,
|
||||
mcp=mcps,
|
||||
)
|
||||
|
||||
# 构建消息列表
|
||||
|
||||
Reference in new issue
Block a user