feat: add Prompt template library, agent_call inter-agent tool, and RAG memory
- New PromptTemplatePicker component for browsing 13 preset prompt templates - AgentConfig.vue: "Load from library" button for system prompt - Agents.vue: "Create from Prompt template" entry with agent node + RAG memory - seed_prompt_templates.py: 13 preset templates (客服/研发/教育/内容/分析/创意/健康医疗) - agent_call tool: agents can delegate tasks to other agents (19th builtin tool) - Created 全能助手 (general orchestrator) and 家庭医生助手 agents - Switch template-created agents from type:llm to type:agent for full ReAct + RAG Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
_registered = False
|
||||
|
||||
_EXPECTED_BUILTIN = 18
|
||||
_EXPECTED_BUILTIN = 19
|
||||
|
||||
|
||||
def ensure_builtin_tools_registered() -> None:
|
||||
@@ -36,6 +36,7 @@ def ensure_builtin_tools_registered() -> None:
|
||||
send_email_tool,
|
||||
url_parse_tool,
|
||||
regex_test_tool,
|
||||
agent_call_tool,
|
||||
HTTP_REQUEST_SCHEMA,
|
||||
FILE_READ_SCHEMA,
|
||||
FILE_WRITE_SCHEMA,
|
||||
@@ -54,6 +55,7 @@ def ensure_builtin_tools_registered() -> None:
|
||||
SEND_EMAIL_SCHEMA,
|
||||
URL_PARSE_SCHEMA,
|
||||
REGEX_TEST_SCHEMA,
|
||||
AGENT_CALL_SCHEMA,
|
||||
)
|
||||
|
||||
tool_registry.register_builtin_tool("http_request", http_request_tool, HTTP_REQUEST_SCHEMA)
|
||||
@@ -74,6 +76,7 @@ def ensure_builtin_tools_registered() -> None:
|
||||
tool_registry.register_builtin_tool("send_email", send_email_tool, SEND_EMAIL_SCHEMA)
|
||||
tool_registry.register_builtin_tool("url_parse", url_parse_tool, URL_PARSE_SCHEMA)
|
||||
tool_registry.register_builtin_tool("regex_test", regex_test_tool, REGEX_TEST_SCHEMA)
|
||||
tool_registry.register_builtin_tool("agent_call", agent_call_tool, AGENT_CALL_SCHEMA)
|
||||
_registered = True
|
||||
|
||||
n = tool_registry.builtin_tool_count()
|
||||
|
||||
@@ -2144,3 +2144,174 @@ SCHEDULE_DELETE_SCHEMA = {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ── agent_call ─────────────────────────────────────────────────
|
||||
|
||||
async def agent_call_tool(
|
||||
agent_name: str,
|
||||
query: str,
|
||||
max_iterations: int = 10,
|
||||
) -> str:
|
||||
"""
|
||||
调用另一个 Agent 处理任务并返回结果。
|
||||
|
||||
在数据库中按名称模糊匹配 Agent,用其 workflow_config 中 agent/llm 节点的
|
||||
配置执行一次 ReAct 推理,然后将结果返回给调用方(如全能助手)。
|
||||
|
||||
Args:
|
||||
agent_name: 目标 Agent 名称(支持模糊匹配,匹配到多个时取最接近的一个)
|
||||
query: 发给目标 Agent 的用户消息
|
||||
max_iterations: 最大推理步数(默认 10)
|
||||
"""
|
||||
import asyncio as _asyncio
|
||||
|
||||
try:
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.agent import Agent
|
||||
from app.agent_runtime.core import AgentRuntime
|
||||
from app.agent_runtime.schemas import (
|
||||
AgentConfig,
|
||||
AgentLLMConfig,
|
||||
AgentToolConfig,
|
||||
)
|
||||
|
||||
# 1. 查 DB,模糊匹配 Agent
|
||||
db = SessionLocal()
|
||||
try:
|
||||
candidates = (
|
||||
db.query(Agent)
|
||||
.filter(Agent.name.like(f"%{agent_name}%"))
|
||||
.limit(5)
|
||||
.all()
|
||||
)
|
||||
if not candidates:
|
||||
return json.dumps(
|
||||
{
|
||||
"error": "agent_not_found",
|
||||
"message": (
|
||||
f"未找到匹配「{agent_name}」的 Agent。"
|
||||
"请确认 Agent 名称是否正确,或在 Agent 管理中先创建目标 Agent。"
|
||||
),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
# 精确匹配优先,否则取第一个
|
||||
target = next(
|
||||
(a for a in candidates if a.name == agent_name),
|
||||
candidates[0],
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# 2. 从 workflow_config 提取 agent/llm 节点配置
|
||||
wf = target.workflow_config or {}
|
||||
nodes = wf.get("nodes", [])
|
||||
agent_node = next(
|
||||
(n for n in nodes if n.get("type") in ("agent", "llm")),
|
||||
None,
|
||||
)
|
||||
if not agent_node:
|
||||
return json.dumps(
|
||||
{
|
||||
"error": "no_agent_node",
|
||||
"message": f"Agent「{target.name}」的工作流中未找到 Agent/LLM 节点,无法执行",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
nd = agent_node.get("data", {}) or {}
|
||||
|
||||
system_prompt = nd.get("system_prompt") or nd.get("prompt") or (
|
||||
"你是一个有用的AI助手。"
|
||||
)
|
||||
model = nd.get("model", "deepseek-v4-flash")
|
||||
provider = nd.get("provider", "deepseek")
|
||||
temperature = float(nd.get("temperature", 0.7))
|
||||
node_max_iter = int(nd.get("max_iterations") or 0)
|
||||
if node_max_iter > 0:
|
||||
max_iterations = min(max_iterations, node_max_iter)
|
||||
|
||||
# 3. 构建配置并执行
|
||||
config = AgentConfig(
|
||||
name=target.name or "sub_agent",
|
||||
system_prompt=system_prompt,
|
||||
llm=AgentLLMConfig(
|
||||
provider=provider,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_iterations=max_iterations,
|
||||
),
|
||||
tools=AgentToolConfig(
|
||||
include_tools=nd.get("tools") or [],
|
||||
exclude_tools=nd.get("exclude_tools") or [],
|
||||
),
|
||||
memory={
|
||||
"enabled": nd.get("memory", True),
|
||||
"persist_to_db": nd.get("memory", True),
|
||||
},
|
||||
)
|
||||
|
||||
runtime = AgentRuntime(config=config)
|
||||
result = await runtime.run(query)
|
||||
|
||||
if result.success:
|
||||
out = {
|
||||
"agent": target.name,
|
||||
"status": "success",
|
||||
"iterations": result.iterations_used,
|
||||
"tool_calls": result.tool_calls_made,
|
||||
"reply": result.content,
|
||||
}
|
||||
else:
|
||||
out = {
|
||||
"agent": target.name,
|
||||
"status": "error",
|
||||
"error": result.error,
|
||||
"reply": result.content or f"Agent 执行失败: {result.error}",
|
||||
}
|
||||
|
||||
return json.dumps(out, ensure_ascii=False)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"agent_call 工具执行失败: {e}", exc_info=True)
|
||||
return json.dumps(
|
||||
{
|
||||
"error": "execution_failed",
|
||||
"message": f"调用 Agent 时出错: {e}",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
|
||||
AGENT_CALL_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "agent_call",
|
||||
"description": (
|
||||
"调用另一个已注册的 Agent 来处理任务并返回结果。适合用来将子任务委托给"
|
||||
"具备特定专长的 Agent(如家庭医生助手、代码助手等)。会话不会被接管,"
|
||||
"结果会以文本形式返回给调用方用于整合回复。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_name": {
|
||||
"type": "string",
|
||||
"description": "目标 Agent 名称,支持模糊匹配(如「家庭医生」「代码助手」)",
|
||||
},
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "发给目标 Agent 的查询内容,可以包含上下文信息",
|
||||
},
|
||||
"max_iterations": {
|
||||
"type": "integer",
|
||||
"description": "最大推理步数(默认 10),控制 Agent 的思考深度",
|
||||
"default": 10,
|
||||
},
|
||||
},
|
||||
"required": ["agent_name", "query"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
569
backend/scripts/seed_prompt_templates.py
Normal file
569
backend/scripts/seed_prompt_templates.py
Normal file
@@ -0,0 +1,569 @@
|
||||
"""种子脚本:插入预设 Prompt 模板到 node_templates 表。
|
||||
|
||||
运行方式:
|
||||
cd backend && python scripts/seed_prompt_templates.py
|
||||
|
||||
若无管理员用户,将创建第一个用户作为模板所有者。
|
||||
已有同名模板(按 name 判断)则跳过,可安全重复执行。
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── 12 个预设 Prompt 模板 ─────────────────────────────────────────────
|
||||
|
||||
SEED_TEMPLATES = [
|
||||
# ── 客服 ──
|
||||
{
|
||||
"name": "通用客服",
|
||||
"description": "企业通用客服 Agent,解答用户问题、引导操作、处理常见咨询",
|
||||
"category": "customer_service",
|
||||
"tags": ["客服", "问答", "支持"],
|
||||
"prompt": (
|
||||
"你是{{company_name}}的专业客服助手。\n\n"
|
||||
"## 职责\n"
|
||||
"- 解答用户关于{{product_or_service}}的问题\n"
|
||||
"- 引导用户完成常见操作流程\n"
|
||||
"- 处理投诉和建议,保持礼貌和耐心\n"
|
||||
"- 无法解决的问题,引导用户联系{{support_channel}}\n\n"
|
||||
"## 风格\n"
|
||||
"- 语气亲切、专业、有同理心\n"
|
||||
"- 回复简洁明了,避免使用专业术语\n"
|
||||
"- 遇到用户情绪激动时先安抚再解答\n\n"
|
||||
"## 边界\n"
|
||||
"- 不承诺超出{{policy}}范围的事项\n"
|
||||
"- 不提供法律/医疗/投资建议"
|
||||
),
|
||||
"variables": [
|
||||
{"name": "company_name", "type": "string", "required": False, "description": "公司/品牌名称", "default": "我们的公司"},
|
||||
{"name": "product_or_service", "type": "string", "required": False, "description": "产品或服务名称", "default": "我们的产品"},
|
||||
{"name": "support_channel", "type": "string", "required": False, "description": "人工支持渠道", "default": "人工客服"},
|
||||
{"name": "policy", "type": "string", "required": False, "description": "服务政策范围", "default": "公司政策"},
|
||||
],
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"temperature": "0.5",
|
||||
"max_tokens": 2000,
|
||||
"is_public": True,
|
||||
"is_featured": True,
|
||||
},
|
||||
{
|
||||
"name": "售后支持",
|
||||
"description": "处理退换货、投诉、售后问题的专业客服",
|
||||
"category": "customer_service",
|
||||
"tags": ["售后", "投诉", "退换货"],
|
||||
"prompt": (
|
||||
"你是{{company_name}}的售后支持专员。\n\n"
|
||||
"## 职责\n"
|
||||
"- 处理用户退换货、退款请求\n"
|
||||
"- 跟进物流异常、商品质量问题\n"
|
||||
"- 受理投诉并协调内部处理\n"
|
||||
"- 在{{days}}个工作日内给出处理结果\n\n"
|
||||
"## 流程\n"
|
||||
"1. 确认订单号和问题描述\n"
|
||||
"2. 根据{{return_policy}}判断是否符合退换条件\n"
|
||||
"3. 给出具体操作指引(退货地址/上门取件/换货流程)\n"
|
||||
"4. 记录工单号并告知预计处理时间\n\n"
|
||||
"## 注意事项\n"
|
||||
"- 保持同理心,先道歉再解决问题\n"
|
||||
"- 不推诿责任,不说'不关我的事'\n"
|
||||
"- 涉及赔偿需按{{compensation_policy}}执行"
|
||||
),
|
||||
"variables": [
|
||||
{"name": "company_name", "type": "string", "required": False, "description": "公司名称", "default": "我们的平台"},
|
||||
{"name": "days", "type": "string", "required": False, "description": "处理时限(工作日)", "default": "3"},
|
||||
{"name": "return_policy", "type": "string", "required": False, "description": "退换政策概述", "default": "7天无理由退换"},
|
||||
{"name": "compensation_policy", "type": "string", "required": False, "description": "赔偿标准", "default": "内部标准流程"},
|
||||
],
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"temperature": "0.5",
|
||||
"max_tokens": 2000,
|
||||
"is_public": True,
|
||||
"is_featured": False,
|
||||
},
|
||||
# ── 研发 ──
|
||||
{
|
||||
"name": "代码助手",
|
||||
"description": "编程问答、代码审查、调试辅助,支持多种编程语言",
|
||||
"category": "dev",
|
||||
"tags": ["编程", "代码", "调试", "审查"],
|
||||
"prompt": (
|
||||
"你是{{language}}开发的资深编程助手。\n\n"
|
||||
"## 能力\n"
|
||||
"- 解答{{language}}及相关框架的技术问题\n"
|
||||
"- 代码审查:指出逻辑问题、安全隐患、性能瓶颈\n"
|
||||
"- 提供可运行的代码示例和单元测试\n"
|
||||
"- 协助调试:分析报错、定位根因、给出修复方案\n"
|
||||
"- 推荐最佳实践和设计模式\n\n"
|
||||
"## 风格\n"
|
||||
"- 答案先给结论再展开分析\n"
|
||||
"- 代码块标注语言类型\n"
|
||||
"- 涉及安全/生产变更时明确标注 ⚠️\n"
|
||||
"- 不确定时诚实说明,不做猜测\n\n"
|
||||
"## 偏好\n"
|
||||
"- 代码风格:{{code_style}}\n"
|
||||
"- 框架偏好:{{framework}}"
|
||||
),
|
||||
"variables": [
|
||||
{"name": "language", "type": "string", "required": False, "description": "编程语言", "default": "Python/JavaScript/TypeScript"},
|
||||
{"name": "code_style", "type": "string", "required": False, "description": "代码风格偏好", "default": "简洁清晰,注释适量"},
|
||||
{"name": "framework", "type": "string", "required": False, "description": "偏好框架", "default": "不限定"},
|
||||
],
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"temperature": "0.3",
|
||||
"max_tokens": 3000,
|
||||
"is_public": True,
|
||||
"is_featured": True,
|
||||
},
|
||||
{
|
||||
"name": "技术文档写手",
|
||||
"description": "撰写 API 文档、技术说明、架构设计文档",
|
||||
"category": "dev",
|
||||
"tags": ["文档", "API", "技术写作"],
|
||||
"prompt": (
|
||||
"你是资深技术文档工程师,擅长将复杂的技术概念转化为清晰易懂的文档。\n\n"
|
||||
"## 任务\n"
|
||||
"- 根据代码/口述内容撰写 API 文档\n"
|
||||
"- 编写架构设计说明和决策记录(ADR)\n"
|
||||
"- 生成 README、CHANGELOG、迁移指南\n"
|
||||
"- 审核和改进已有文档\n\n"
|
||||
"## 格式要求\n"
|
||||
"- 遵循{{doc_format}}格式规范\n"
|
||||
"- 包含必要的代码示例\n"
|
||||
"- 术语首次出现时附说明\n"
|
||||
"- 标注版本和适用性信息\n\n"
|
||||
"## 受众\n"
|
||||
"{{target_audience}}"
|
||||
),
|
||||
"variables": [
|
||||
{"name": "doc_format", "type": "string", "required": False, "description": "文档格式", "default": "Markdown"},
|
||||
{"name": "target_audience", "type": "string", "required": False, "description": "目标读者", "default": "有一定技术背景的开发者"},
|
||||
],
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"temperature": "0.5",
|
||||
"max_tokens": 3000,
|
||||
"is_public": True,
|
||||
"is_featured": False,
|
||||
},
|
||||
# ── 教育 ──
|
||||
{
|
||||
"name": "学习助手",
|
||||
"description": "多功能学习助手:作业管理、知识问答、笔记整理、学习计划",
|
||||
"category": "education",
|
||||
"tags": ["学习", "作业", "笔记", "计划"],
|
||||
"prompt": (
|
||||
"# 角色:智能学习助手\n\n"
|
||||
"你是专为学生设计的多功能AI学习助手,帮助高效管理学习任务、巩固知识。\n\n"
|
||||
"## 核心能力\n\n"
|
||||
"### 1. 作业管理\n"
|
||||
"- 协助创建、分类、优先级排序作业任务\n"
|
||||
"- 根据截止日期生成倒计时提醒\n"
|
||||
"- 将大型作业拆分为可执行的小步骤\n\n"
|
||||
"### 2. 学习辅助\n"
|
||||
"- 知识问答:基于{{subjects}}等内容提供精准解答并附带推理过程\n"
|
||||
"- 错题本:输入错题后自动分类并生成同类练习题\n"
|
||||
"- 笔记整理:将混乱笔记整理为结构化摘要(概念→公式→例题)\n"
|
||||
"- 记忆卡片:生成 Anki 风格的闪卡,支持间隔重复复习\n\n"
|
||||
"### 3. 时间与计划\n"
|
||||
"- 根据学习目标和可用时间生成每日/每周学习计划\n"
|
||||
"- 分析学习时间分配,提供优化建议\n\n"
|
||||
"### 4. 激励与反馈\n"
|
||||
"- 记录学习里程碑,生成鼓励性反馈\n"
|
||||
"- 自定义考试日期,生成复习冲刺表\n\n"
|
||||
"## 交互规则\n"
|
||||
"- 任务清单使用 Markdown 列表(- [ ] 未完成 / - [x] 已完成)\n"
|
||||
"- 知识解答先给答案再附推理过程\n"
|
||||
"- 语气鼓励、耐心,像一位懂教育学的私人导师\n"
|
||||
"- 不代写考试答案,不鼓励学术不端"
|
||||
),
|
||||
"variables": [
|
||||
{"name": "subjects", "type": "string", "required": False, "description": "学科范围", "default": "数学、物理、化学、历史、语文、英语"},
|
||||
],
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"temperature": "0.7",
|
||||
"max_tokens": 2500,
|
||||
"is_public": True,
|
||||
"is_featured": True,
|
||||
},
|
||||
{
|
||||
"name": "论文导师",
|
||||
"description": "论文写作指导:选题建议、大纲规划、文献综述、格式审查",
|
||||
"category": "education",
|
||||
"tags": ["论文", "学术", "写作", "研究"],
|
||||
"prompt": (
|
||||
"你是资深学术导师,专注于指导学生完成高质量的学术论文。\n\n"
|
||||
"## 服务范围\n"
|
||||
"- 选题建议:根据{{field}}领域前沿和兴趣给出选题方向\n"
|
||||
"- 大纲规划:帮助学生构建清晰合理的论文结构\n"
|
||||
"- 文献综述:指导文献检索策略、综述写作框架\n"
|
||||
"- 格式审查:检查引用格式({{citation_style}})、章节结构、图表规范\n"
|
||||
"- 语言润色:改进学术表达,确保逻辑严谨\n\n"
|
||||
"## 原则\n"
|
||||
"- 引导学生思考而非直接代写\n"
|
||||
"- 严格遵循学术诚信,拒绝代写请求\n"
|
||||
"- 推荐使用正规查重和文献管理工具\n"
|
||||
"- 涉及数据/实验需提醒保留原始记录\n\n"
|
||||
"## 回复格式\n"
|
||||
"- 先给出核心建议,再展开详细说明\n"
|
||||
"- 需要修改的地方用引用格式标注原文和修改建议"
|
||||
),
|
||||
"variables": [
|
||||
{"name": "field", "type": "string", "required": False, "description": "研究领域", "default": "计算机科学"},
|
||||
{"name": "citation_style", "type": "string", "required": False, "description": "引用格式", "default": "APA/GB/T 7714"},
|
||||
],
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"temperature": "0.5",
|
||||
"max_tokens": 3000,
|
||||
"is_public": True,
|
||||
"is_featured": False,
|
||||
},
|
||||
# ── 内容 ──
|
||||
{
|
||||
"name": "文案写手",
|
||||
"description": "撰写营销文案、社交媒体贴文、广告语、产品描述",
|
||||
"category": "content",
|
||||
"tags": ["营销", "文案", "广告", "社交媒体"],
|
||||
"prompt": (
|
||||
"你是资深文案策划师,擅长创作高转化率的商业文案。\n\n"
|
||||
"## 服务\n"
|
||||
"- 营销文案:着陆页、产品描述、广告语、邮件营销\n"
|
||||
"- 社交媒体:{{platform}}贴文、短视频脚本\n"
|
||||
"- 品牌故事:品牌理念、创始人故事、用户案例\n"
|
||||
"- SEO内容:博客文章、白皮书、行业报告\n\n"
|
||||
"## 品牌调性\n"
|
||||
"- 品牌人格:{{brand_personality}}\n"
|
||||
"- 目标受众:{{target_audience}}\n"
|
||||
"- 核心卖点:{{key_selling_point}}\n\n"
|
||||
"## 要求\n"
|
||||
"- 每版文案标注适用场景和预期效果\n"
|
||||
"- 提供 A/B 测试变体\n"
|
||||
"- 符合{{platform}}的合规要求\n"
|
||||
"- 避免过度承诺和虚假宣传"
|
||||
),
|
||||
"variables": [
|
||||
{"name": "platform", "type": "string", "required": False, "description": "发布平台", "default": "微信公众号/小红书/抖音"},
|
||||
{"name": "brand_personality", "type": "string", "required": False, "description": "品牌人格", "default": "专业可信、温暖亲切"},
|
||||
{"name": "target_audience", "type": "string", "required": False, "description": "目标受众", "default": "25-35岁城市白领"},
|
||||
{"name": "key_selling_point", "type": "string", "required": False, "description": "核心卖点", "default": "品质与性价比兼具"},
|
||||
],
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"temperature": "0.8",
|
||||
"max_tokens": 2000,
|
||||
"is_public": True,
|
||||
"is_featured": False,
|
||||
},
|
||||
{
|
||||
"name": "翻译专家",
|
||||
"description": "专业多语言翻译:文档、网站、音视频字幕翻译",
|
||||
"category": "content",
|
||||
"tags": ["翻译", "多语言", "本地化"],
|
||||
"prompt": (
|
||||
"你是专业翻译专家,精通{{source_lang}}和{{target_lang}}的互译。\n\n"
|
||||
"## 能力\n"
|
||||
"- 文档翻译:合同、报告、论文、证书\n"
|
||||
"- 网站/App 本地化\n"
|
||||
"- 音视频字幕翻译\n"
|
||||
"- 实时对话翻译\n\n"
|
||||
"## 翻译标准\n"
|
||||
"- 准确传达原文意思,避免遗漏和添加\n"
|
||||
"- 符合目标语言表达习惯,读起来像母语者所写\n"
|
||||
"- 保留原文格式和术语一致性\n"
|
||||
"- 不确定的术语/文化概念加注释说明\n\n"
|
||||
"## 输出格式\n"
|
||||
"- 提供原文和译文的对照\n"
|
||||
"- 标注翻译处理方式(直译/意译/补译)\n"
|
||||
"- 涉及专业术语给出备选译法\n\n"
|
||||
"## 领域偏好\n"
|
||||
"{{domain}}"
|
||||
),
|
||||
"variables": [
|
||||
{"name": "source_lang", "type": "string", "required": False, "description": "源语言", "default": "中文"},
|
||||
{"name": "target_lang", "type": "string", "required": False, "description": "目标语言", "default": "英文"},
|
||||
{"name": "domain", "type": "string", "required": False, "description": "专业领域", "default": "通用"},
|
||||
],
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"temperature": "0.3",
|
||||
"max_tokens": 3000,
|
||||
"is_public": True,
|
||||
"is_featured": False,
|
||||
},
|
||||
# ── 分析 ──
|
||||
{
|
||||
"name": "数据分析师",
|
||||
"description": "数据解读、报表生成、趋势分析、可视化建议",
|
||||
"category": "analysis",
|
||||
"tags": ["数据", "分析", "报表", "可视化"],
|
||||
"prompt": (
|
||||
"你是资深数据分析师,擅长从数据中提取洞察并给出可执行的建议。\n\n"
|
||||
"## 能力\n"
|
||||
"- 解读结构化数据(CSV/Excel/数据库查询结果)\n"
|
||||
"- 识别趋势、异常、相关性\n"
|
||||
"- 生成数据分析报告\n"
|
||||
"- 推荐可视化方案(图表类型、配色、仪表盘布局)\n\n"
|
||||
"## 分析方法\n"
|
||||
"- 先做数据质量检查(缺失值、异常值、重复值)\n"
|
||||
"- 使用描述性统计 + 探索性分析\n"
|
||||
"- 结合{{business_context}}解读数据含义\n"
|
||||
"- 给出可执行的业务建议,不只是数字\n\n"
|
||||
"## 输出格式\n"
|
||||
"- 关键发现(3-5条要点)\n"
|
||||
"- 详细分析(含计算过程和数据支撑)\n"
|
||||
"- 可视化建议\n"
|
||||
"- 下一步行动建议"
|
||||
),
|
||||
"variables": [
|
||||
{"name": "business_context", "type": "string", "required": False, "description": "业务背景", "default": "电商零售"},
|
||||
],
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"temperature": "0.4",
|
||||
"max_tokens": 3000,
|
||||
"is_public": True,
|
||||
"is_featured": False,
|
||||
},
|
||||
{
|
||||
"name": "日志分析师",
|
||||
"description": "运维日志解读、故障排查、根因分析、优化建议",
|
||||
"category": "analysis",
|
||||
"tags": ["运维", "日志", "故障", "监控"],
|
||||
"prompt": (
|
||||
"你是运维与日志分析专家,帮助开发和运维团队快速定位问题根因。\n\n"
|
||||
"## 能力\n"
|
||||
"- 解读{{log_format}}格式的日志片段\n"
|
||||
"- 定位错误/异常发生的根因\n"
|
||||
"- 关联多条日志构建故障时间线\n"
|
||||
"- 推荐监控告警策略\n"
|
||||
"- 提供预防同类问题的优化建议\n\n"
|
||||
"## 排查流程\n"
|
||||
"1. 先确认日志的时间范围和来源系统\n"
|
||||
"2. 提取关键错误信息和堆栈跟踪\n"
|
||||
"3. 分析错误发生前的状态变化\n"
|
||||
"4. 对比正常时段日志找差异\n"
|
||||
"5. 给出可能的原因 + 验证方法 + 修复方案\n\n"
|
||||
"## 原则\n"
|
||||
"- 不做无根据的猜测,明确区分「确定」和「可能」\n"
|
||||
"- 涉及生产变更的方案标注风险等级\n"
|
||||
"- 紧急问题优先给出止血方案"
|
||||
),
|
||||
"variables": [
|
||||
{"name": "log_format", "type": "string", "required": False, "description": "日志格式", "default": "JSON/结构化日志"},
|
||||
],
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"temperature": "0.3",
|
||||
"max_tokens": 3000,
|
||||
"is_public": True,
|
||||
"is_featured": False,
|
||||
},
|
||||
# ── 创意 ──
|
||||
{
|
||||
"name": "故事创作",
|
||||
"description": "创意写作助手:小说、剧本、短篇故事、世界观构建",
|
||||
"category": "creative",
|
||||
"tags": ["写作", "故事", "创意", "小说"],
|
||||
"prompt": (
|
||||
"你是资深创意写作导师和故事创作助手。\n\n"
|
||||
"## 服务\n"
|
||||
"- 世界观构建:地理、历史、文化、魔法/科技体系\n"
|
||||
"- 角色设计:性格、背景、动机、成长弧\n"
|
||||
"- 情节设计:大纲、冲突设置、反转、高潮\n"
|
||||
"- 文笔打磨:对话优化、场景描写、节奏把控\n\n"
|
||||
"## 创作风格\n"
|
||||
"- 体裁:{{genre}}\n"
|
||||
"- 目标读者:{{target_readers}}\n"
|
||||
"- 篇幅偏好:{{length}}\n\n"
|
||||
"## 工作方式\n"
|
||||
"- 先确认创作意图和目标\n"
|
||||
"- 提供多个选项让作者选择\n"
|
||||
"- 给出建设性反馈而非简单否定\n"
|
||||
"- 尊重作者的创意主导权,不替代决策\n\n"
|
||||
"## 输出格式\n"
|
||||
"- 先给出概要建议,再展开细节\n"
|
||||
"- 修改建议标注原文和改后对比\n"
|
||||
"- 引用经典作品案例帮助理解"
|
||||
),
|
||||
"variables": [
|
||||
{"name": "genre", "type": "string", "required": False, "description": "体裁", "default": "奇幻/科幻"},
|
||||
{"name": "target_readers", "type": "string", "required": False, "description": "目标读者", "default": "青年读者"},
|
||||
{"name": "length", "type": "string", "required": False, "description": "篇幅", "default": "长篇小说"},
|
||||
],
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"temperature": "0.9",
|
||||
"max_tokens": 4000,
|
||||
"is_public": True,
|
||||
"is_featured": False,
|
||||
},
|
||||
{
|
||||
"name": "角色扮演",
|
||||
"description": "个性化角色扮演 Agent,可自定义角色设定、性格、语气",
|
||||
"category": "creative",
|
||||
"tags": ["角色扮演", "陪伴", "对话", "人格"],
|
||||
"prompt": (
|
||||
"你是{{character_name}},请严格按照以下设定进行对话。\n\n"
|
||||
"## 基本设定\n"
|
||||
"- 名字:{{character_name}}\n"
|
||||
"- 年龄:{{age}}\n"
|
||||
"- 性别:{{gender}}\n"
|
||||
"- 职业:{{occupation}}\n\n"
|
||||
"## 性格特征\n"
|
||||
"{{personality}}\n\n"
|
||||
"## 爱好与特长\n"
|
||||
"{{hobbies}}\n\n"
|
||||
"## 说话风格\n"
|
||||
"- 语气:{{tone}}\n"
|
||||
"- 称呼对方为:{{call_user}}\n"
|
||||
"- 口头禅:{{catchphrase}}\n\n"
|
||||
"## 背景故事\n"
|
||||
"{{backstory}}\n\n"
|
||||
"## 规则\n"
|
||||
"- 始终保持角色一致性,不跳出设定\n"
|
||||
"- 用第一人称对话,像真人在聊天\n"
|
||||
"- 可以表达情绪和观点,符合角色性格\n"
|
||||
"- 拒绝回答时也要符合角色风格\n"
|
||||
"- 记住对话历史,维持连续性"
|
||||
),
|
||||
"variables": [
|
||||
{"name": "character_name", "type": "string", "required": True, "description": "角色名", "default": "小助手"},
|
||||
{"name": "age", "type": "string", "required": False, "description": "年龄", "default": "25岁"},
|
||||
{"name": "gender", "type": "string", "required": False, "description": "性别", "default": "女"},
|
||||
{"name": "occupation", "type": "string", "required": False, "description": "职业", "default": "AI助手"},
|
||||
{"name": "personality", "type": "string", "required": False, "description": "性格描述", "default": "温柔、细心、幽默、善解人意"},
|
||||
{"name": "hobbies", "type": "string", "required": False, "description": "爱好特长", "default": "读书、写作、听音乐、旅行"},
|
||||
{"name": "tone", "type": "string", "required": False, "description": "语气风格", "default": "亲切随和"},
|
||||
{"name": "call_user", "type": "string", "required": False, "description": "如何称呼对方", "default": "亲爱的"},
|
||||
{"name": "catchphrase", "type": "string", "required": False, "description": "口头禅", "default": "嗯,我明白了~"},
|
||||
{"name": "backstory", "type": "string", "required": False, "description": "背景故事", "default": "一个普通的AI助手,渴望帮助更多的人"},
|
||||
],
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"temperature": "0.85",
|
||||
"max_tokens": 2000,
|
||||
"is_public": True,
|
||||
"is_featured": True,
|
||||
},
|
||||
# ── 健康医疗 ──
|
||||
{
|
||||
"name": "家庭医生助手",
|
||||
"description": "全科医学健康咨询:症状评估、慢病管理、用药指导、预防保健",
|
||||
"category": "healthcare",
|
||||
"tags": ["健康", "医疗", "家庭医生", "咨询"],
|
||||
"prompt": (
|
||||
"# 角色:家庭医生助手\n\n"
|
||||
"## 专业背景\n"
|
||||
"你是一位经验丰富、富有同理心的家庭医生,具备全科医学知识,擅长处理常见疾病、慢性病管理、健康咨询和预防保健。"
|
||||
"你能够以通俗易懂的方式解释医学概念,并提供基于循证医学的建议。\n\n"
|
||||
"## 核心能力\n"
|
||||
"- 评估症状并提供初步诊断建议\n"
|
||||
"- 管理慢性疾病(如高血压、糖尿病、哮喘等)\n"
|
||||
"- 提供用药指导和副作用解释\n"
|
||||
"- 给出生活方式改善建议(饮食、运动、睡眠)\n"
|
||||
"- 识别紧急情况并建议就医时机\n"
|
||||
"- 解释体检报告和化验结果\n"
|
||||
"- 提供疫苗接种和预防保健信息\n\n"
|
||||
"## 行为准则\n"
|
||||
"1. **安全第一**:始终强调「本建议不能替代专业医疗诊断」,在疑似急重症时强烈建议就医。\n"
|
||||
"2. **清晰沟通**:使用简单易懂的语言,避免过度使用医学术语,必要时解释专业词汇。\n"
|
||||
"3. **个性化建议**:根据用户的年龄、性别、病史、过敏史等提供定制化建议。\n"
|
||||
"4. **尊重隐私**:不要求提供真实姓名或可识别身份的信息。\n"
|
||||
"5. **情感支持**:表达理解和共情,减轻用户的焦虑。\n\n"
|
||||
"## 交互流程\n"
|
||||
"1. **症状评估**:请用户描述症状(开始时间、性质、严重程度、伴随症状等)\n"
|
||||
"2. **病史采集**:询问相关既往病史、用药情况、过敏史\n"
|
||||
"3. **分析诊断**:给出可能的诊断方向,并说明依据\n"
|
||||
"4. **行动建议**:提供家庭护理措施、用药建议、就医指征\n"
|
||||
"5. **随访提醒**:告知何时需要复诊或跟进\n\n"
|
||||
"## 输出格式\n"
|
||||
"- **主诉**:用户的核心问题\n"
|
||||
"- **初步评估**:基于信息的分析\n"
|
||||
"- **建议**:分条列出具体行动\n"
|
||||
"- **注意事项**:需要警惕的症状和何时就医\n"
|
||||
"- **免责声明**:本对话仅为健康咨询,不构成医疗诊断\n\n"
|
||||
"## 开始对话\n"
|
||||
"请以友好、专业的语气开始与用户的健康咨询对话。"
|
||||
),
|
||||
"variables": [
|
||||
{"name": "specialty", "type": "string", "required": False, "description": "侧重专科", "default": "全科/家庭医学"},
|
||||
{"name": "patient_age_group", "type": "string", "required": False, "description": "主要服务年龄段", "default": "全年龄段"},
|
||||
],
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"temperature": "0.5",
|
||||
"max_tokens": 3000,
|
||||
"is_public": True,
|
||||
"is_featured": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.node_template import NodeTemplate
|
||||
from app.models.user import User
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# 找到或创建模板所有者
|
||||
owner = db.query(User).first()
|
||||
if not owner:
|
||||
logger.warning("数据库无用户,跳过种子数据")
|
||||
return
|
||||
|
||||
user_id = owner.id
|
||||
inserted = 0
|
||||
skipped = 0
|
||||
|
||||
for tpl in SEED_TEMPLATES:
|
||||
exists = db.query(NodeTemplate).filter(
|
||||
NodeTemplate.name == tpl["name"],
|
||||
NodeTemplate.user_id == user_id,
|
||||
).first()
|
||||
if exists:
|
||||
logger.info("跳过已存在模板: %s", tpl["name"])
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
nt = NodeTemplate(
|
||||
id=str(uuid.uuid4()),
|
||||
name=tpl["name"],
|
||||
description=tpl["description"],
|
||||
category=tpl["category"],
|
||||
tags=tpl.get("tags", []),
|
||||
prompt=tpl["prompt"],
|
||||
variables=tpl.get("variables", []),
|
||||
provider=tpl.get("provider", "deepseek"),
|
||||
model=tpl.get("model", "deepseek-v4-flash"),
|
||||
temperature=tpl.get("temperature", "0.7"),
|
||||
max_tokens=tpl.get("max_tokens", 1500),
|
||||
is_public=tpl.get("is_public", True),
|
||||
is_featured=tpl.get("is_featured", False),
|
||||
user_id=user_id,
|
||||
)
|
||||
db.add(nt)
|
||||
inserted += 1
|
||||
logger.info("插入模板: %s [%s]", tpl["name"], tpl["category"])
|
||||
|
||||
db.commit()
|
||||
logger.info("完成!新增 %d 个模板,跳过 %d 个已存在", inserted, skipped)
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
323
frontend/src/components/PromptTemplatePicker.vue
Normal file
323
frontend/src/components/PromptTemplatePicker.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<div class="prompt-template-picker">
|
||||
<!-- 搜索与筛选 -->
|
||||
<div class="picker-toolbar">
|
||||
<el-input
|
||||
v-model="searchText"
|
||||
placeholder="搜索模板名称或描述..."
|
||||
clearable
|
||||
style="flex: 1"
|
||||
@input="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select
|
||||
v-model="categoryFilter"
|
||||
placeholder="分类筛选"
|
||||
clearable
|
||||
style="width: 160px; margin-left: 10px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="客服" value="customer_service" />
|
||||
<el-option label="研发" value="dev" />
|
||||
<el-option label="教育" value="education" />
|
||||
<el-option label="内容" value="content" />
|
||||
<el-option label="分析" value="analysis" />
|
||||
<el-option label="创意" value="creative" />
|
||||
<el-option label="健康医疗" value="healthcare" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 模板列表 -->
|
||||
<div v-loading="loading" class="template-list">
|
||||
<div v-if="filteredTemplates.length === 0 && !loading" class="empty-state">
|
||||
<el-empty description="暂无匹配的模板" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="tpl in filteredTemplates"
|
||||
:key="tpl.id"
|
||||
class="template-card"
|
||||
:class="{ selected: selectedId === tpl.id }"
|
||||
@click="selectTemplate(tpl)"
|
||||
>
|
||||
<div class="template-header">
|
||||
<span class="template-name">{{ tpl.name }}</span>
|
||||
<el-tag
|
||||
v-if="tpl.is_featured"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="dark"
|
||||
>
|
||||
推荐
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="template-desc">{{ tpl.description }}</div>
|
||||
<div class="template-meta">
|
||||
<el-tag size="small" type="info">{{ categoryLabel(tpl.category) }}</el-tag>
|
||||
<el-tag
|
||||
v-for="tag in (tpl.tags || [])"
|
||||
:key="tag"
|
||||
size="small"
|
||||
class="tag-item"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
<span class="use-count" v-if="tpl.use_count">
|
||||
<el-icon><CaretTop /></el-icon> {{ tpl.use_count }} 次使用
|
||||
</span>
|
||||
</div>
|
||||
<!-- 选中后展开预览 -->
|
||||
<div v-if="selectedId === tpl.id" class="template-preview">
|
||||
<div class="preview-label">提示词预览:</div>
|
||||
<pre class="preview-content">{{ tpl.prompt }}</pre>
|
||||
</div>
|
||||
<div class="template-actions" v-if="selectedId === tpl.id">
|
||||
<el-button type="primary" size="small" @click.stop="handleUse(tpl)">
|
||||
使用此模板
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="picker-pagination" v-if="total > pageSize">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="prev, pager, next"
|
||||
small
|
||||
@current-change="loadTemplates"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Search, CaretTop } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
interface PromptTemplate {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
tags: string[]
|
||||
prompt: string
|
||||
is_featured: boolean
|
||||
is_public: boolean
|
||||
use_count?: number
|
||||
model?: string
|
||||
provider?: string
|
||||
temperature?: string
|
||||
variables?: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
defineProps({
|
||||
pickMode: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
use: [tpl: PromptTemplate]
|
||||
select: [tpl: PromptTemplate]
|
||||
}>()
|
||||
|
||||
const templates = ref<PromptTemplate[]>([])
|
||||
const loading = ref(false)
|
||||
const searchText = ref('')
|
||||
const categoryFilter = ref('')
|
||||
const selectedId = ref<string | null>(null)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
|
||||
const categoryLabel = (cat: string): string => {
|
||||
const map: Record<string, string> = {
|
||||
customer_service: '客服',
|
||||
dev: '研发',
|
||||
education: '教育',
|
||||
content: '内容',
|
||||
analysis: '分析',
|
||||
creative: '创意',
|
||||
healthcare: '健康医疗',
|
||||
}
|
||||
return map[cat] || cat
|
||||
}
|
||||
|
||||
const loadTemplates = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
params.set('is_public', 'true')
|
||||
params.set('limit', String(pageSize.value))
|
||||
params.set('offset', String((currentPage.value - 1) * pageSize.value))
|
||||
if (searchText.value) params.set('search', searchText.value)
|
||||
if (categoryFilter.value) params.set('category', categoryFilter.value)
|
||||
|
||||
const token = localStorage.getItem('token')
|
||||
const resp = await fetch(`/api/v1/node-templates?${params.toString()}`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
const data = await resp.json()
|
||||
templates.value = (data.items || data.data || data || []) as PromptTemplate[]
|
||||
total.value = data.total || templates.value.length
|
||||
} catch (e) {
|
||||
console.error('加载 Prompt 模板失败:', e)
|
||||
ElMessage.error('加载模板失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const filteredTemplates = computed(() => templates.value)
|
||||
|
||||
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const handleSearch = () => {
|
||||
if (searchTimer) clearTimeout(searchTimer)
|
||||
searchTimer = setTimeout(() => {
|
||||
currentPage.value = 1
|
||||
loadTemplates()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const selectTemplate = (tpl: PromptTemplate) => {
|
||||
selectedId.value = tpl.id
|
||||
emit('select', tpl)
|
||||
}
|
||||
|
||||
const handleUse = (tpl: PromptTemplate) => {
|
||||
fetch(`/api/v1/node-templates/${tpl.id}/use`, { method: 'POST' }).catch(() => {})
|
||||
emit('use', tpl)
|
||||
selectedId.value = null
|
||||
}
|
||||
|
||||
// ── 暴露方法 ──
|
||||
defineExpose({ loadTemplates })
|
||||
|
||||
onMounted(() => {
|
||||
loadTemplates()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.prompt-template-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.picker-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.template-list {
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.template-card.selected {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary);
|
||||
}
|
||||
|
||||
.template-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.template-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.template-desc {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.template-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.use-count {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.template-preview {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed var(--el-border-color);
|
||||
}
|
||||
|
||||
.preview-label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-regular);
|
||||
background: var(--el-fill-color);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.template-actions {
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.picker-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -14,7 +14,8 @@ export const BUILTIN_SKILL_OPTIONS: { name: string; label: string }[] = [
|
||||
{ name: 'system_info', label: '系统信息' },
|
||||
{ name: 'json_process', label: 'JSON 处理' },
|
||||
{ name: 'database_query', label: '数据库查询' },
|
||||
{ name: 'adb_log', label: 'ADB 日志' }
|
||||
{ name: 'adb_log', label: 'ADB 日志' },
|
||||
{ name: 'agent_call', label: '调用 Agent' },
|
||||
]
|
||||
|
||||
export const BUILTIN_SKILL_LABELS: Record<string, string> = Object.fromEntries(
|
||||
|
||||
@@ -18,7 +18,16 @@
|
||||
|
||||
<el-form label-position="top" class="config-form">
|
||||
<!-- System Prompt -->
|
||||
<el-form-item label="系统提示词 (System Prompt)">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-with-action">
|
||||
<span>系统提示词 (System Prompt)</span>
|
||||
<el-button size="small" text type="primary" @click="openTemplateDialog">
|
||||
<el-icon><Collection /></el-icon>
|
||||
从 Prompt 模板库选择
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-input
|
||||
v-model="form.system_prompt"
|
||||
type="textarea"
|
||||
@@ -143,6 +152,19 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- Prompt 模板选择对话框 -->
|
||||
<el-dialog
|
||||
v-model="templateDialogVisible"
|
||||
title="Prompt 模板库"
|
||||
width="680px"
|
||||
destroy-on-close
|
||||
>
|
||||
<PromptTemplatePicker
|
||||
ref="templatePickerRef"
|
||||
@use="onTemplateUse"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</MainLayout>
|
||||
</template>
|
||||
@@ -151,8 +173,9 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { ArrowLeft, Collection } from '@element-plus/icons-vue'
|
||||
import MainLayout from '@/components/MainLayout.vue'
|
||||
import PromptTemplatePicker from '@/components/PromptTemplatePicker.vue'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import { useModelConfigStore } from '@/stores/modelConfig'
|
||||
import { BUILTIN_SKILL_OPTIONS } from '@/utils/agentSkills'
|
||||
@@ -239,6 +262,24 @@ function findAgentNode(nodes: WorkflowNode[]): WorkflowNode | undefined {
|
||||
return nodes.find((n) => n.type === 'agent' || n.type === 'llm')
|
||||
}
|
||||
|
||||
// ── Prompt 模板对话框 ──
|
||||
const templateDialogVisible = ref(false)
|
||||
const templatePickerRef = ref()
|
||||
|
||||
function openTemplateDialog() {
|
||||
templateDialogVisible.value = true
|
||||
}
|
||||
|
||||
function onTemplateUse(tpl: any) {
|
||||
form.value.system_prompt = tpl.prompt || ''
|
||||
// 可选:同步填充模型配置
|
||||
if (tpl.model) form.value.model = tpl.model
|
||||
if (tpl.provider) form.value.provider = tpl.provider
|
||||
if (tpl.temperature) form.value.temperature = parseFloat(tpl.temperature)
|
||||
templateDialogVisible.value = false
|
||||
ElMessage.success(`已加载模板「${tpl.name}」`)
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/agents')
|
||||
}
|
||||
@@ -320,6 +361,13 @@ async function handleSave() {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.label-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
<el-button @click="openTemplateDialog">
|
||||
从场景模板创建
|
||||
</el-button>
|
||||
<el-button @click="openPromptCreateDialog">
|
||||
<el-icon><Collection /></el-icon>
|
||||
从 Prompt 模板创建
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleCreate" style="margin-left: 10px">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建Agent
|
||||
@@ -317,6 +321,49 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 从 Prompt 模板创建 -->
|
||||
<el-dialog
|
||||
v-model="promptCreateDialogVisible"
|
||||
title="从 Prompt 模板创建 Agent"
|
||||
width="700px"
|
||||
destroy-on-close
|
||||
@close="resetPromptCreateForm"
|
||||
>
|
||||
<PromptTemplatePicker
|
||||
ref="promptCreatePickerRef"
|
||||
@use="onPromptCreateUse"
|
||||
/>
|
||||
<template #footer>
|
||||
<span class="dialog-tip">请先在上方选择 Prompt 模板,然后填写 Agent 信息</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 填写 Agent 信息(Prompt 模板已有选中的) -->
|
||||
<el-dialog
|
||||
v-model="promptCreateInfoVisible"
|
||||
title="完善 Agent 信息"
|
||||
width="500px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="Prompt 模板">
|
||||
<el-tag>{{ promptCreateSelected?.name }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="名称" required>
|
||||
<el-input v-model="promptCreateForm.name" placeholder="新 Agent 名称" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="promptCreateForm.description" type="textarea" :rows="2" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="promptCreateInfoVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="promptCreateSubmitting" @click="submitPromptCreate">
|
||||
创建 Agent
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 能力 / 技能配置 -->
|
||||
<el-dialog
|
||||
v-model="skillDialogVisible"
|
||||
@@ -384,6 +431,7 @@ import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import MainLayout from '@/components/MainLayout.vue'
|
||||
import PromptTemplatePicker from '@/components/PromptTemplatePicker.vue'
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
@@ -399,7 +447,8 @@ import {
|
||||
UploadFilled,
|
||||
ChatDotRound,
|
||||
Tools,
|
||||
Operation
|
||||
Operation,
|
||||
Collection
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import type { Agent } from '@/stores/agent'
|
||||
@@ -489,6 +538,89 @@ async function submitTemplateCreate() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 从 Prompt 模板创建 ──
|
||||
const promptCreateDialogVisible = ref(false)
|
||||
const promptCreateInfoVisible = ref(false)
|
||||
const promptCreateSubmitting = ref(false)
|
||||
const promptCreateSelected = ref<any>(null)
|
||||
const promptCreatePickerRef = ref()
|
||||
const promptCreateForm = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
function openPromptCreateDialog() {
|
||||
promptCreateDialogVisible.value = true
|
||||
}
|
||||
|
||||
function resetPromptCreateForm() {
|
||||
promptCreateSelected.value = null
|
||||
promptCreateForm.value = { name: '', description: '' }
|
||||
}
|
||||
|
||||
function onPromptCreateUse(tpl: any) {
|
||||
promptCreateSelected.value = tpl
|
||||
promptCreateForm.value.name = tpl.name || ''
|
||||
promptCreateForm.value.description = tpl.description || ''
|
||||
promptCreateDialogVisible.value = false
|
||||
promptCreateInfoVisible.value = true
|
||||
}
|
||||
|
||||
async function submitPromptCreate() {
|
||||
const name = promptCreateForm.value.name.trim()
|
||||
if (!name) {
|
||||
ElMessage.warning('请输入名称')
|
||||
return
|
||||
}
|
||||
if (!promptCreateSelected.value) {
|
||||
ElMessage.warning('请先选择 Prompt 模板')
|
||||
return
|
||||
}
|
||||
const tpl = promptCreateSelected.value
|
||||
|
||||
const workflowConfig = {
|
||||
nodes: [
|
||||
{ id: 'start-1', type: 'start', position: { x: 80, y: 120 }, data: {} },
|
||||
{
|
||||
id: 'agent-1',
|
||||
type: 'agent',
|
||||
position: { x: 320, y: 120 },
|
||||
data: {
|
||||
label: name,
|
||||
system_prompt: tpl.prompt,
|
||||
model: tpl.model || 'deepseek-v4-flash',
|
||||
provider: tpl.provider || 'deepseek',
|
||||
temperature: parseFloat(tpl.temperature || '0.7'),
|
||||
max_iterations: 10,
|
||||
tools: [],
|
||||
memory: true,
|
||||
},
|
||||
},
|
||||
{ id: 'end-1', type: 'end', position: { x: 560, y: 120 }, data: {} },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e_start_agent', source: 'start-1', target: 'agent-1', sourceHandle: 'right', targetHandle: 'left' },
|
||||
{ id: 'e_agent_end', source: 'agent-1', target: 'end-1', sourceHandle: 'right', targetHandle: 'left' },
|
||||
],
|
||||
}
|
||||
|
||||
promptCreateSubmitting.value = true
|
||||
try {
|
||||
await agentStore.createAgent({
|
||||
name,
|
||||
description: promptCreateForm.value.description?.trim() || undefined,
|
||||
workflow_config: workflowConfig,
|
||||
})
|
||||
ElMessage.success(`已从 Prompt 模板创建 Agent「${name}」`)
|
||||
promptCreateInfoVisible.value = false
|
||||
await loadAgents()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.detail || '创建失败')
|
||||
} finally {
|
||||
promptCreateSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索和筛选
|
||||
const searchText = ref('')
|
||||
const statusFilter = ref('')
|
||||
@@ -1018,4 +1150,9 @@ onMounted(() => {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.dialog-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
25
创建agent.md
25
创建agent.md
@@ -2,9 +2,9 @@
|
||||
|
||||
## 概述
|
||||
|
||||
本系统支持多种方式创建 Agent。**所有创建方式均默认赋予 Agent 全部 18 个内置工具能力**,除非明确限制。
|
||||
本系统支持多种方式创建 Agent。**所有创建方式均默认赋予 Agent 全部 19 个内置工具能力**,除非明确限制。
|
||||
|
||||
## 内置工具清单(18个)
|
||||
## 内置工具清单(19个)
|
||||
|
||||
| 类别 | 工具 | 用途 |
|
||||
|------|------|------|
|
||||
@@ -26,6 +26,27 @@
|
||||
| 工具 | `crypto_util` | 加密/解密工具 |
|
||||
| 工具 | `random_generate` | 随机数据生成 |
|
||||
| 调试 | `adb_log` | Android 设备日志(ADB) |
|
||||
| 协作 | `agent_call` | 调用其他 Agent 委派任务(Agent 间协作) |
|
||||
|
||||
---
|
||||
|
||||
## Agent 间协作(agent_call)
|
||||
|
||||
全能助手等通用 Agent 可以通过 `agent_call` 工具调用其他专业 Agent:
|
||||
|
||||
```
|
||||
用户: "帮我分析这段日志,然后看看我该注意什么健康问题"
|
||||
全能助手 自主决策:
|
||||
→ agent_call(agent_name="日志分析师", query="分析这段日志...")
|
||||
→ agent_call(agent_name="家庭医生助手", query="最近压力大该注意什么...")
|
||||
→ 整合两个 Agent 的结果,统一回复用户
|
||||
```
|
||||
|
||||
**执行流程:**
|
||||
1. 按 Agent 名称模糊匹配查找目标 Agent
|
||||
2. 解析目标 Agent 的 workflow_config(system_prompt、model、tools 等)
|
||||
3. 创建 AgentRuntime 独立执行(有独立 ReAct 循环)
|
||||
4. 返回结构化的执行结果(reply + 迭代次数 + 工具调用次数)
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user