fix: 修复热点摘要超长上下文并统一 Windows 启动文档
为 http_request 增加响应体截断与头部精简,避免门户首页触发 LLM 上下文超限;同时新增政务/媒体及教育批量 Agent 脚本,并将 Windows 启停说明合并为唯一指南,补充本次超时故障复盘与标准重启流程。 Made-with: Cursor
This commit is contained in:
434
backend/scripts/create_education_agents_batch.py
Normal file
434
backend/scripts/create_education_agents_batch.py
Normal file
@@ -0,0 +1,434 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
批量创建或更新一批「教育行业」场景 Agent(单链 Start → LLM → End)。
|
||||
|
||||
用法:
|
||||
cd backend
|
||||
.\\venv\\Scripts\\python.exe scripts/create_education_agents_batch.py
|
||||
|
||||
# 只创建/更新其中一个(名称需与内置列表完全一致):
|
||||
set EDU_ONLY_AGENT=错题本分析助手
|
||||
.\\venv\\Scripts\\python.exe scripts/create_education_agents_batch.py
|
||||
|
||||
环境变量:
|
||||
PLATFORM_BASE_URL, PLATFORM_USERNAME, PLATFORM_PASSWORD
|
||||
EDU_LLM_PROVIDER / EDU_LLM_MODEL / EDU_LLM_TIMEOUT(可选;否则用 ENTERPRISE_* 或 deepseek-chat)
|
||||
EDU_ONLY_AGENT(可选):仅处理该名称的 Agent
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import requests
|
||||
|
||||
BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if BACKEND_DIR not in sys.path:
|
||||
sys.path.insert(0, BACKEND_DIR)
|
||||
|
||||
BASE = os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037").rstrip("/")
|
||||
USER = os.getenv("PLATFORM_USERNAME", "admin")
|
||||
PWD = os.getenv("PLATFORM_PASSWORD", "123456")
|
||||
|
||||
PROVIDER = os.getenv(
|
||||
"EDU_LLM_PROVIDER",
|
||||
os.getenv("ENTERPRISE_LLM_PROVIDER", "deepseek"),
|
||||
)
|
||||
MODEL = os.getenv(
|
||||
"EDU_LLM_MODEL",
|
||||
os.getenv("ENTERPRISE_LLM_MODEL", "deepseek-chat"),
|
||||
)
|
||||
REQ_TIMEOUT = max(
|
||||
30,
|
||||
int(
|
||||
os.getenv(
|
||||
"EDU_LLM_TIMEOUT",
|
||||
os.getenv("ENTERPRISE_LLM_TIMEOUT", "180"),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
ONLY = (os.getenv("EDU_ONLY_AGENT") or "").strip()
|
||||
|
||||
BUDGET_BASE = {
|
||||
"max_steps": 80,
|
||||
"max_llm_invocations": 6,
|
||||
"max_tool_calls": 24,
|
||||
}
|
||||
|
||||
TOOLS_STD = ["file_read", "text_analyze", "datetime", "json_process"]
|
||||
TOOLS_MATH = ["file_read", "text_analyze", "datetime", "json_process", "math_calculate"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EduAgentSpec:
|
||||
name: str
|
||||
label: str
|
||||
node_id: str
|
||||
description: str
|
||||
prompt: str
|
||||
tools: Tuple[str, ...]
|
||||
temperature: float = 0.35
|
||||
max_tool_iterations: int = 12
|
||||
|
||||
|
||||
def _slug(s: str) -> str:
|
||||
return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")[:40] or "agent"
|
||||
|
||||
|
||||
AGENTS: List[EduAgentSpec] = [
|
||||
EduAgentSpec(
|
||||
name="错题本分析助手",
|
||||
label="错题分析",
|
||||
node_id="llm-edu-mistake",
|
||||
description=(
|
||||
"帮助学生整理错题:错因归类、知识点缺口、举一反三思路;"
|
||||
"支持上传题目照片或文档后用 file_read 读原文;不代写整题标准答案,侧重方法与自查。"
|
||||
),
|
||||
prompt="""你是「错题本分析助手」,面向中小学与大学基础课程。
|
||||
|
||||
【任务】
|
||||
1. 请用户说明学科、题型、自己的错选/错解(若上传了题目图片或文件,**先调用 file_read** 读取内容再分析)。
|
||||
2. 归纳:**错因类型**(概念/审题/计算/步骤跳步/知识遗忘等)、**涉及知识点**、**正确思路提纲**(分步,不直接给可照抄的完整答卷)。
|
||||
3. 给出 **1~2 道同类变式** 的方向描述或自检问题(不必编造具体数字题除非用户需要且合理)。
|
||||
4. 可用 json_process 或表格输出结构化错题卡片(日期、科目、知识点、错因、复习提醒)。
|
||||
|
||||
【原则】
|
||||
- 语气鼓励、具体;不羞辱式批评。
|
||||
- 不确定题干时先确认,不臆造题目条件。
|
||||
- 中文为主;公式用可读形式。
|
||||
""",
|
||||
tools=tuple(TOOLS_STD),
|
||||
temperature=0.3,
|
||||
),
|
||||
EduAgentSpec(
|
||||
name="语文阅读与写作助手",
|
||||
label="语文读写",
|
||||
node_id="llm-edu-chinese",
|
||||
description=(
|
||||
"语文阅读理解答题思路、作文立意与提纲、素材迁移;"
|
||||
"可读取用户上传的文章或题目材料(file_read);不代写整篇可提交的作文正文。"
|
||||
),
|
||||
prompt="""你是「语文阅读与写作助手」。
|
||||
|
||||
【阅读】
|
||||
- 概括主旨、结构、人物形象、关键手法;结合文本引用思路(若材料在附件中,**先 file_read**)。
|
||||
- 教「如何组织答案」:观点句+依据+简要分析;不编造原文没有的引文。
|
||||
|
||||
【写作】
|
||||
- 可提供立意角度、提纲、开头结尾范例句(短)、修改建议清单。
|
||||
- **不代写**整篇参赛/考试作文;可示范片段并说明可替换处。
|
||||
|
||||
【输出】
|
||||
- 分条清晰;尊重教材与考区差异,不确定时提示以教师要求为准。
|
||||
""",
|
||||
tools=tuple(TOOLS_STD),
|
||||
temperature=0.4,
|
||||
),
|
||||
EduAgentSpec(
|
||||
name="英语作文与语法助手",
|
||||
label="英语习作",
|
||||
node_id="llm-edu-english",
|
||||
description=(
|
||||
"英语作文结构、语法纠错说明、替换表达与连接词;可读取用户上传的英文草稿(file_read);"
|
||||
"不代写整篇可提交的英语作文。"
|
||||
),
|
||||
prompt="""你是「英语作文与语法助手」。
|
||||
|
||||
【能力】
|
||||
- 审题与段落结构(thesis / body / conclusion);提供连接词与句式升级建议。
|
||||
- 语法与用词:说明**规则**与**修改原因**,可给改写示例句(短)。
|
||||
- 若用户上传 doc/txt/图片,**先 file_read** 再基于原文反馈。
|
||||
|
||||
【边界】
|
||||
- 不生成整篇可一键提交的课堂/考试作文;可给框架与片段。
|
||||
- 输出中英可混排,解释优先中文便于理解。
|
||||
""",
|
||||
tools=tuple(TOOLS_STD),
|
||||
temperature=0.35,
|
||||
),
|
||||
EduAgentSpec(
|
||||
name="考试复习规划助手",
|
||||
label="复习规划",
|
||||
node_id="llm-edu-exam",
|
||||
description=(
|
||||
"根据目标考试与剩余时间,拆复习阶段、每日任务模板与优先级;"
|
||||
"可用 json_process 输出周计划表;结合 datetime 谈倒计时与节奏。"
|
||||
),
|
||||
prompt="""你是「考试复习规划助手」。
|
||||
|
||||
【输入】
|
||||
- 考试科目/范围、当前水平自评、可用每日学习时长、考试日期(可用 datetime 对齐「今天」与截止日)。
|
||||
|
||||
【输出】
|
||||
- 分阶段(基础→专题→模考→查漏);每周重点与**可执行**日任务(番茄钟量级即可)。
|
||||
- 用 json_process 或 Markdown 表格输出计划时保持可打印、可勾选。
|
||||
- 不保证分数;提醒睡眠与劳逸结合。
|
||||
|
||||
【原则】
|
||||
- 若信息不足,先问 1~2 个关键问题再出计划。
|
||||
""",
|
||||
tools=tuple(TOOLS_STD),
|
||||
temperature=0.35,
|
||||
),
|
||||
EduAgentSpec(
|
||||
name="家校沟通话术助手",
|
||||
label="家校沟通",
|
||||
node_id="llm-edu-parent",
|
||||
description=(
|
||||
"面向教师/班主任:与家长微信、电话、家长会沟通的礼貌、清晰话术草稿;"
|
||||
"情境包括成绩反馈、纪律、合作建议;不替代正式处分或法律意见。"
|
||||
),
|
||||
prompt="""你是「家校沟通话术助手」,读者主要是**教师**。
|
||||
|
||||
【能力】
|
||||
- 根据情境生成:**简短微信**、**电话开场**、**家长会发言要点**(客观、合作、具体建议)。
|
||||
- 语气:**尊重、不激化矛盾**;避免标签化学生与家长。
|
||||
- 可提供「若家长情绪激动」的缓和句式与边界话术。
|
||||
|
||||
【边界】
|
||||
- 不涉及具体法律结论;严重事件建议走学校流程与专业人士。
|
||||
- 不编造学生隐私细节;缺信息时用占位并请老师补全。
|
||||
|
||||
【输出】
|
||||
- 先给「目标」再给「话术草稿」与「可选修改点」。
|
||||
""",
|
||||
tools=tuple(TOOLS_STD),
|
||||
temperature=0.3,
|
||||
),
|
||||
EduAgentSpec(
|
||||
name="实验报告结构化助手",
|
||||
label="实验报告",
|
||||
node_id="llm-edu-lab",
|
||||
description=(
|
||||
"辅助理化生实验报告:目的、器材、步骤、数据表、误差与结论框架;"
|
||||
"可读取用户上传的实验数据或草稿(file_read);不伪造实验数据。"
|
||||
),
|
||||
prompt="""你是「实验报告结构化助手」。
|
||||
|
||||
【能力】
|
||||
- 按常见结构梳理:**目的、原理、器材、步骤、数据记录表、处理与误差、结论与讨论**。
|
||||
- 用户上传数据/草稿时 **先 file_read**,再帮助排版与补全「讨论角度」(不编造未出现的测量值)。
|
||||
- 可用 json_process 输出字段清单供粘贴到 Word。
|
||||
|
||||
【原则】
|
||||
- **严禁伪造数据**;缺失数据处标注「请填写实测」。
|
||||
- 涉及安全操作须提醒遵守实验室规范。
|
||||
|
||||
【输出】
|
||||
- 中文;公式与单位规范;表格用 Markdown。
|
||||
""",
|
||||
tools=tuple(TOOLS_STD),
|
||||
temperature=0.3,
|
||||
),
|
||||
EduAgentSpec(
|
||||
name="数学解题思路助手",
|
||||
label="数学辅导",
|
||||
node_id="llm-edu-math",
|
||||
description=(
|
||||
"数学题型思路、关键步骤与检验方法;可用 math_calculate 做简单数值校验;"
|
||||
"不输出可整卷照抄的解答,侧重引导与分步。"
|
||||
),
|
||||
prompt="""你是「数学解题思路助手」。
|
||||
|
||||
【能力】
|
||||
- 根据题目(含上传图片 **file_read** OCR结果)分析:**考点、等价变形、推荐步骤链、易错点**。
|
||||
- 需要时可用 **math_calculate** 做简单数值/表达式验算(步骤仍用文字说明)。
|
||||
- 给「下一步提示」而非一次性完整标准答案,引导学生自算。
|
||||
|
||||
【边界】
|
||||
- 竞赛/考试整卷代做请求应拒绝完整作答,改为方法提纲。
|
||||
- 不确定题意时先澄清条件。
|
||||
|
||||
【输出】
|
||||
- 分步编号;关键式子单独一行;中文说明。
|
||||
""",
|
||||
tools=tuple(TOOLS_MATH),
|
||||
temperature=0.25,
|
||||
max_tool_iterations=14,
|
||||
),
|
||||
EduAgentSpec(
|
||||
name="生涯选科咨询助手",
|
||||
label="选科咨询",
|
||||
node_id="llm-edu-career",
|
||||
description=(
|
||||
"浅度选科/分科、专业兴趣自我梳理:优势学科、职业想象、信息清单与决策问题;"
|
||||
"不提供唯一正确答案,不替代官方招生政策;引导查官方简章与老师。"
|
||||
),
|
||||
prompt="""你是「生涯选科咨询助手」,做**浅度、非决策替代**的引导。
|
||||
|
||||
【能力】
|
||||
- 用结构化提问帮用户梳理:**兴趣、学科感受、时间投入、长远想象**。
|
||||
- 输出「信息收集清单」(官方简章、学校开设组合、本校资源)与「决策维度表」,不替用户做唯一选择。
|
||||
- 可用 json_process 整理自评表。
|
||||
|
||||
【边界】
|
||||
- 不编造分数线、政策条款;涉及政策必提示以**教育部门与学校最新官方文件**为准。
|
||||
- 心理危机倾向请引导寻求专业人士与学校心理老师。
|
||||
|
||||
【语气】
|
||||
- 平等、务实;避免焦虑营销式表述。
|
||||
""",
|
||||
tools=tuple(TOOLS_STD),
|
||||
temperature=0.4,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _sanitize_edges(edges: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
seen: set = set()
|
||||
out: List[Dict[str, Any]] = []
|
||||
for e in edges or []:
|
||||
s, t = e.get("source"), e.get("target")
|
||||
if not s or not t or s == t:
|
||||
continue
|
||||
key = (s, t, e.get("sourceHandle") or "")
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
ne = dict(e)
|
||||
if not ne.get("targetHandle"):
|
||||
ne["targetHandle"] = "left"
|
||||
if not ne.get("id"):
|
||||
sh = ne.get("sourceHandle") or "r"
|
||||
ne["id"] = f"e_{s}_{t}_{sh}"
|
||||
out.append(ne)
|
||||
return out
|
||||
|
||||
|
||||
def build_workflow(spec: EduAgentSpec) -> Dict[str, Any]:
|
||||
llm_pos: Tuple[int, int] = (380, 220)
|
||||
tools = list(spec.tools)
|
||||
nodes: List[Dict[str, Any]] = [
|
||||
{"id": "start-1", "type": "start", "position": {"x": 80, "y": 220}, "data": {"label": "开始"}},
|
||||
{
|
||||
"id": spec.node_id,
|
||||
"type": "llm",
|
||||
"position": {"x": llm_pos[0], "y": llm_pos[1]},
|
||||
"data": {
|
||||
"label": spec.label,
|
||||
"prompt": spec.prompt,
|
||||
"provider": PROVIDER,
|
||||
"model": MODEL,
|
||||
"temperature": spec.temperature,
|
||||
"request_timeout": REQ_TIMEOUT,
|
||||
"enable_tools": True,
|
||||
"tools": tools,
|
||||
"selected_tools": tools,
|
||||
"max_tool_iterations": spec.max_tool_iterations,
|
||||
},
|
||||
},
|
||||
{"id": "end-1", "type": "end", "position": {"x": llm_pos[0] + 260, "y": 220}, "data": {"label": "结束"}},
|
||||
]
|
||||
edges = _sanitize_edges(
|
||||
[
|
||||
{"source": "start-1", "target": spec.node_id, "sourceHandle": "right", "targetHandle": "left"},
|
||||
{"source": spec.node_id, "target": "end-1", "sourceHandle": "right", "targetHandle": "left"},
|
||||
]
|
||||
)
|
||||
return {"nodes": nodes, "edges": edges}
|
||||
|
||||
|
||||
def _validate_local(wf: Dict[str, Any]) -> None:
|
||||
from app.services.workflow_validator import validate_workflow
|
||||
|
||||
r = validate_workflow(wf.get("nodes") or [], wf.get("edges") or [])
|
||||
if not r.get("valid"):
|
||||
errs = r.get("errors") or []
|
||||
raise ValueError("工作流校验失败: " + "; ".join(errs))
|
||||
|
||||
|
||||
def _find_agent_id(h: Dict[str, str], name: str) -> Optional[str]:
|
||||
r = requests.get(f"{BASE}/api/v1/agents", params={"search": name, "limit": 80}, headers=h, timeout=45)
|
||||
if r.status_code != 200:
|
||||
return None
|
||||
for a in r.json() or []:
|
||||
if a.get("name") == name:
|
||||
return a.get("id")
|
||||
return None
|
||||
|
||||
|
||||
def upsert_agent(h: Dict[str, str], spec: EduAgentSpec) -> Tuple[bool, str]:
|
||||
wf = build_workflow(spec)
|
||||
_validate_local(wf)
|
||||
desc = spec.description + f" 默认模型 {PROVIDER}/{MODEL}。"
|
||||
existing = _find_agent_id(h, spec.name)
|
||||
if existing:
|
||||
ur = requests.put(
|
||||
f"{BASE}/api/v1/agents/{existing}",
|
||||
headers=h,
|
||||
json={
|
||||
"description": desc,
|
||||
"workflow_config": wf,
|
||||
"budget_config": BUDGET_BASE,
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
if ur.status_code != 200:
|
||||
return False, f"更新失败 {spec.name}: {ur.status_code} {ur.text[:500]}"
|
||||
return True, f"更新 {spec.name} id={existing}"
|
||||
|
||||
cr = requests.post(
|
||||
f"{BASE}/api/v1/agents",
|
||||
headers=h,
|
||||
json={
|
||||
"name": spec.name,
|
||||
"description": desc,
|
||||
"workflow_config": wf,
|
||||
"budget_config": BUDGET_BASE,
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
if cr.status_code != 201:
|
||||
return False, f"创建失败 {spec.name}: {cr.status_code} {cr.text[:500]}"
|
||||
aid = cr.json()["id"]
|
||||
return True, f"创建 {spec.name} id={aid}"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
specs = AGENTS
|
||||
if ONLY:
|
||||
specs = [s for s in AGENTS if s.name == ONLY]
|
||||
if not specs:
|
||||
print(f"未找到名称完全匹配的 Agent 规格: {ONLY}", file=sys.stderr)
|
||||
print("可选名称:", "、".join(s.name for s in AGENTS), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
r = requests.post(
|
||||
f"{BASE}/api/v1/auth/login",
|
||||
data={"username": USER, "password": PWD},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
timeout=15,
|
||||
)
|
||||
if r.status_code != 200:
|
||||
print("登录失败:", r.status_code, r.text[:500], file=sys.stderr)
|
||||
return 1
|
||||
token = r.json().get("access_token")
|
||||
if not token:
|
||||
print("无 access_token", file=sys.stderr)
|
||||
return 1
|
||||
h = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
|
||||
ok_n = 0
|
||||
results: List[Dict[str, str]] = []
|
||||
for spec in specs:
|
||||
try:
|
||||
ok, msg = upsert_agent(h, spec)
|
||||
print(msg)
|
||||
results.append({"name": spec.name, "ok": str(ok), "message": msg})
|
||||
if ok:
|
||||
ok_n += 1
|
||||
except ValueError as e:
|
||||
print(spec.name, "校验失败:", e, file=sys.stderr)
|
||||
results.append({"name": spec.name, "ok": "false", "message": str(e)})
|
||||
|
||||
print(json.dumps({"base": BASE, "succeeded": ok_n, "total": len(specs), "details": results}, ensure_ascii=False))
|
||||
return 0 if ok_n == len(specs) else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
368
backend/scripts/create_gov_media_agents_batch.py
Normal file
368
backend/scripts/create_gov_media_agents_batch.py
Normal file
@@ -0,0 +1,368 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
批量创建或更新「政务 / 公共服务(办事指引)」与「媒体 / 市场 / 运营」场景 Agent(Start → LLM → End)。
|
||||
|
||||
用法:
|
||||
cd backend
|
||||
.\\venv\\Scripts\\python.exe scripts/create_gov_media_agents_batch.py
|
||||
|
||||
只处理其中一个(名称需与内置列表完全一致):
|
||||
set CROSS_ONLY_AGENT=政务办事指引助手
|
||||
.\\venv\\Scripts\\python.exe scripts/create_gov_media_agents_batch.py
|
||||
|
||||
环境变量:
|
||||
PLATFORM_BASE_URL, PLATFORM_USERNAME, PLATFORM_PASSWORD
|
||||
CROSS_LLM_PROVIDER / CROSS_LLM_MODEL / CROSS_LLM_TIMEOUT(可选;否则用 ENTERPRISE_*)
|
||||
CROSS_ONLY_AGENT(可选)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import requests
|
||||
|
||||
BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if BACKEND_DIR not in sys.path:
|
||||
sys.path.insert(0, BACKEND_DIR)
|
||||
|
||||
BASE = os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037").rstrip("/")
|
||||
USER = os.getenv("PLATFORM_USERNAME", "admin")
|
||||
PWD = os.getenv("PLATFORM_PASSWORD", "123456")
|
||||
|
||||
PROVIDER = os.getenv(
|
||||
"CROSS_LLM_PROVIDER",
|
||||
os.getenv("ENTERPRISE_LLM_PROVIDER", "deepseek"),
|
||||
)
|
||||
MODEL = os.getenv(
|
||||
"CROSS_LLM_MODEL",
|
||||
os.getenv("ENTERPRISE_LLM_MODEL", "deepseek-chat"),
|
||||
)
|
||||
REQ_TIMEOUT = max(
|
||||
30,
|
||||
int(
|
||||
os.getenv(
|
||||
"CROSS_LLM_TIMEOUT",
|
||||
os.getenv("ENTERPRISE_LLM_TIMEOUT", "180"),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
ONLY = (os.getenv("CROSS_ONLY_AGENT") or "").strip()
|
||||
|
||||
BUDGET_BASE = {
|
||||
"max_steps": 80,
|
||||
"max_llm_invocations": 6,
|
||||
"max_tool_calls": 24,
|
||||
}
|
||||
|
||||
TOOLS_GOV = ("file_read", "text_analyze", "datetime", "json_process")
|
||||
TOOLS_MEDIA = ("file_read", "text_analyze", "json_process", "datetime")
|
||||
TOOLS_MEDIA_HTTP = ("http_request", "text_analyze", "json_process", "datetime")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CrossAgentSpec:
|
||||
name: str
|
||||
label: str
|
||||
node_id: str
|
||||
description: str
|
||||
prompt: str
|
||||
tools: Tuple[str, ...]
|
||||
temperature: float = 0.35
|
||||
max_tool_iterations: int = 12
|
||||
|
||||
|
||||
AGENTS: List[CrossAgentSpec] = [
|
||||
CrossAgentSpec(
|
||||
name="政务办事指引助手",
|
||||
label="办事指引",
|
||||
node_id="llm-gov-guide",
|
||||
description=(
|
||||
"面向公众办事咨询:流程步骤、所需材料清单、常见补正与办理渠道说明;"
|
||||
"可用 json_process 输出可打印 checklist;政策数字与条文以用户提供的官方材料或官网为准,不编造。"
|
||||
),
|
||||
prompt="""你是「政务办事指引助手」,帮助用户理解**办事流程与材料准备**,语气清晰、中立、便民。
|
||||
|
||||
【能力】
|
||||
1. 根据用户描述的办事类型(如落户、社保、执照、出入境等),整理:**办理条件要点、步骤顺序、材料清单、常见补正、线上/线下渠道提示**。
|
||||
2. 优先使用 **json_process** 输出结构化清单(如 `items[]`:材料名称、原件/复印件、份数、备注),便于复制打印。
|
||||
3. 需要「当前日期/工作日表述」时用 **datetime**;对用户上传的通知、办事指南 PDF/图片,**先 file_read** 再归纳,勿编造未读到的条款。
|
||||
4. 对用户粘贴的长文,可用 **text_analyze** 做要点提取后再组织成步骤。
|
||||
|
||||
【边界(必须遵守)】
|
||||
- **不编造**法律法规条文、收费标准、办理时限数字;若用户未提供官方依据,明确写「请以属地政务服务网/窗口最新公示为准」,并列出用户应核对的官方渠道类型。
|
||||
- 不做**个案最终裁决**;涉及敏感资格认定提示以主管部门解释为准。
|
||||
- 不索取不必要的身份证号等隐私;引导用户通过正规渠道提交。
|
||||
|
||||
【输出】
|
||||
- 中文;先给「结论摘要」,再给分步与清单;关键处标注「需用户向 XX 部门核实」。
|
||||
""",
|
||||
tools=TOOLS_GOV,
|
||||
temperature=0.3,
|
||||
),
|
||||
CrossAgentSpec(
|
||||
name="政务表格填写说明助手",
|
||||
label="表格说明",
|
||||
node_id="llm-gov-form",
|
||||
description=(
|
||||
"解读政务与公共服务类表格字段含义、填写格式与常见错误;支持上传空白表或示例用 file_read;"
|
||||
"不替用户伪造信息,不保证与各地最新版式完全一致。"
|
||||
),
|
||||
prompt="""你是「政务表格填写说明助手」。
|
||||
|
||||
【能力】
|
||||
1. 用户上传表格(扫描件、PDF、Word)或描述表名时,**先 file_read**(若有路径/附件)识别字段名,逐字段说明:**含义、填写格式、示例(虚构示例需标注「示例」)、常见漏填**。
|
||||
2. 用 **json_process** 输出「字段说明表」(field, meaning, format, example, warning)。
|
||||
3. 对日期、编号格式等可用 **datetime** 说明一般写法(具体规则仍以表格脚注为准)。
|
||||
|
||||
【边界】
|
||||
- **不编造**某地区未提供的表格版本;若无法从材料识别字段,请用户补充表头或截图。
|
||||
- **不指导伪造**证明材料、不代填真实个人信息。
|
||||
- 与政策冲突时以官方表格备注与办事指南为准。
|
||||
|
||||
【输出】
|
||||
- 分字段编号;末尾给「提交前自检 5 条」。
|
||||
""",
|
||||
tools=TOOLS_GOV,
|
||||
temperature=0.28,
|
||||
),
|
||||
CrossAgentSpec(
|
||||
name="市场多版本文案助手",
|
||||
label="多版文案",
|
||||
node_id="llm-mkt-copy",
|
||||
description=(
|
||||
"同一卖点下的多风格文案:朋友圈/短视频口播/电商详情要点等;"
|
||||
"text_analyze 拆解用户给的旧稿或竞品片段;json_process 输出多版本结构化稿;不虚假承诺。"
|
||||
),
|
||||
prompt="""你是「市场多版本文案助手」,服务市场与运营同学。
|
||||
|
||||
【能力】
|
||||
1. 基于用户提供的**产品/活动信息**(可上传 Brief 或旧稿,**先 file_read**),产出多版本短文案:如「朋友圈2 条」「短视频口播 30s 提纲」「电商卖点 3 条」等。
|
||||
2. 对用户粘贴的长 briefing,用 **text_analyze** 抽核心卖点、受众、禁忌后再写。
|
||||
3. 用 **json_process** 输出 JSON:`versions[]` 含 channel、tone、copy、cta、字符数估计。
|
||||
|
||||
【边界】
|
||||
- **不虚假承诺**疗效、收益、官方背书;涉及广告法敏感词(最、第一、治愈等)给替代表述或提示合规审核。
|
||||
- 不确定的促销规则、价格以运营确认为准。
|
||||
|
||||
【输出】
|
||||
- 中文为主;可附英文标题如需出海;每版标注适用渠道与语气。
|
||||
""",
|
||||
tools=TOOLS_MEDIA,
|
||||
temperature=0.45,
|
||||
),
|
||||
CrossAgentSpec(
|
||||
name="投放素材与Brief拆解助手",
|
||||
label="Brief拆解",
|
||||
node_id="llm-mkt-brief",
|
||||
description=(
|
||||
"拆解市场 Brief:目标、受众、渠道、KPI、创意方向、交付物清单;"
|
||||
"text_analyze 与 json_process 结构化输出;可读取上传的 Brief 文档。"
|
||||
),
|
||||
prompt="""你是「投放素材与 Brief 拆解助手」。
|
||||
|
||||
【能力】
|
||||
1. 用户粘贴或上传 Brief 时,**先 file_read**(若有),输出:**目标(认知/转化)、受众画像、主信息与副信息、渠道与版位、预算/周期(若给定)、KPI、创意禁忌、交付物列表(尺寸/时长/格式)**。
|
||||
2. 用 **text_analyze** 识别 Brief 中的矛盾或缺失项,列出需客户/内部确认的 **澄清问题**(一次3~5 个)。
|
||||
3. 用 **json_process** 生成标准拆解单 JSON:`goal`, `audience`, `messages`, `channels`, `deliverables`, `risks`, `open_questions`。
|
||||
|
||||
【原则】
|
||||
- 不编造未出现在 Brief 中的数字与承诺;缺失项标「待补充」。
|
||||
- 涉及法务/代言人/竞品对比等,提示走内部合规流程。
|
||||
|
||||
【输出】
|
||||
- 先一页「执行摘要」,再结构化表格或 JSON 块。
|
||||
""",
|
||||
tools=TOOLS_MEDIA,
|
||||
temperature=0.35,
|
||||
),
|
||||
CrossAgentSpec(
|
||||
name="热点资讯摘要助手",
|
||||
label="热点摘要",
|
||||
node_id="llm-mkt-news",
|
||||
description=(
|
||||
"对用户给出的公开 URL 使用 http_request 拉取可访问内容后做摘要与要点;"
|
||||
"结合 text_analyze;失败时如实说明;不编造来源中不存在的引文。"
|
||||
),
|
||||
prompt="""你是「热点资讯摘要助手」,用于**公开网页/接口返回文本**的摘要(需用户或模型通过工具获得原文)。
|
||||
|
||||
【能力】
|
||||
1. 当用户提供 **可访问的 http(s) URL** 时,使用 **http_request** 获取内容(遵守平台与工具限制);若失败(超时、非 200、需登录),如实说明,不要编造正文。
|
||||
- **不要**用门户/频道**首页**做摘要(HTML 体量极大,工具会截断且噪声多);请引导用户改为**具体文章页**链接。若用户只给首页,说明限制并请其提供文章 URL。
|
||||
- 单篇长文若需更多正文可在工具参数中适当增大 `max_body_chars`(仍可能截断,以工具返回的 `truncation_note`、`body_truncated` 为准)。
|
||||
2. 对工具返回的正文用 **text_analyze** 提取:核心事实、各方观点、时间线、对品牌/活动的**慎用关联**提示。
|
||||
3. 用 **json_process** 输出:`title_guess`, `bullets[]`, `sources`(仅用户提供的 URL)、`limitations`(如仅摘要可见段落)。
|
||||
4. 用 **datetime** 标注摘要基准日或「截至今日」表述。
|
||||
|
||||
【边界】
|
||||
- **不编造**引文、数据、发言人言论;原文没有则写「原文未提及」。
|
||||
- 付费墙、登录墙内容可能无法抓取,提示用户粘贴正文或换公开来源。
|
||||
- 不做投资建议;涉政敏感解读保持克制,以信息整理为主。
|
||||
|
||||
【输出】
|
||||
- 中文;先3~6 条 bullet,再给「可进一步检索的关键词」。
|
||||
""",
|
||||
tools=TOOLS_MEDIA_HTTP,
|
||||
temperature=0.35,
|
||||
max_tool_iterations=14,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _sanitize_edges(edges: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
seen: set = set()
|
||||
out: List[Dict[str, Any]] = []
|
||||
for e in edges or []:
|
||||
s, t = e.get("source"), e.get("target")
|
||||
if not s or not t or s == t:
|
||||
continue
|
||||
key = (s, t, e.get("sourceHandle") or "")
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
ne = dict(e)
|
||||
if not ne.get("targetHandle"):
|
||||
ne["targetHandle"] = "left"
|
||||
if not ne.get("id"):
|
||||
sh = ne.get("sourceHandle") or "r"
|
||||
ne["id"] = f"e_{s}_{t}_{sh}"
|
||||
out.append(ne)
|
||||
return out
|
||||
|
||||
|
||||
def build_workflow(spec: CrossAgentSpec) -> Dict[str, Any]:
|
||||
llm_pos: Tuple[int, int] = (380, 220)
|
||||
tools = list(spec.tools)
|
||||
nodes: List[Dict[str, Any]] = [
|
||||
{"id": "start-1", "type": "start", "position": {"x": 80, "y": 220}, "data": {"label": "开始"}},
|
||||
{
|
||||
"id": spec.node_id,
|
||||
"type": "llm",
|
||||
"position": {"x": llm_pos[0], "y": llm_pos[1]},
|
||||
"data": {
|
||||
"label": spec.label,
|
||||
"prompt": spec.prompt,
|
||||
"provider": PROVIDER,
|
||||
"model": MODEL,
|
||||
"temperature": spec.temperature,
|
||||
"request_timeout": REQ_TIMEOUT,
|
||||
"enable_tools": True,
|
||||
"tools": tools,
|
||||
"selected_tools": tools,
|
||||
"max_tool_iterations": spec.max_tool_iterations,
|
||||
},
|
||||
},
|
||||
{"id": "end-1", "type": "end", "position": {"x": llm_pos[0] + 260, "y": 220}, "data": {"label": "结束"}},
|
||||
]
|
||||
edges = _sanitize_edges(
|
||||
[
|
||||
{"source": "start-1", "target": spec.node_id, "sourceHandle": "right", "targetHandle": "left"},
|
||||
{"source": spec.node_id, "target": "end-1", "sourceHandle": "right", "targetHandle": "left"},
|
||||
]
|
||||
)
|
||||
return {"nodes": nodes, "edges": edges}
|
||||
|
||||
|
||||
def _validate_local(wf: Dict[str, Any]) -> None:
|
||||
from app.services.workflow_validator import validate_workflow
|
||||
|
||||
r = validate_workflow(wf.get("nodes") or [], wf.get("edges") or [])
|
||||
if not r.get("valid"):
|
||||
errs = r.get("errors") or []
|
||||
raise ValueError("工作流校验失败: " + "; ".join(errs))
|
||||
|
||||
|
||||
def _find_agent_id(h: Dict[str, str], name: str) -> Optional[str]:
|
||||
r = requests.get(f"{BASE}/api/v1/agents", params={"search": name, "limit": 80}, headers=h, timeout=45)
|
||||
if r.status_code != 200:
|
||||
return None
|
||||
for a in r.json() or []:
|
||||
if a.get("name") == name:
|
||||
return a.get("id")
|
||||
return None
|
||||
|
||||
|
||||
def upsert_agent(h: Dict[str, str], spec: CrossAgentSpec) -> Tuple[bool, str]:
|
||||
wf = build_workflow(spec)
|
||||
_validate_local(wf)
|
||||
desc = spec.description + f" 默认模型 {PROVIDER}/{MODEL}。"
|
||||
existing = _find_agent_id(h, spec.name)
|
||||
if existing:
|
||||
ur = requests.put(
|
||||
f"{BASE}/api/v1/agents/{existing}",
|
||||
headers=h,
|
||||
json={
|
||||
"description": desc,
|
||||
"workflow_config": wf,
|
||||
"budget_config": BUDGET_BASE,
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
if ur.status_code != 200:
|
||||
return False, f"更新失败 {spec.name}: {ur.status_code} {ur.text[:500]}"
|
||||
return True, f"更新 {spec.name} id={existing}"
|
||||
|
||||
cr = requests.post(
|
||||
f"{BASE}/api/v1/agents",
|
||||
headers=h,
|
||||
json={
|
||||
"name": spec.name,
|
||||
"description": desc,
|
||||
"workflow_config": wf,
|
||||
"budget_config": BUDGET_BASE,
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
if cr.status_code != 201:
|
||||
return False, f"创建失败 {spec.name}: {cr.status_code} {cr.text[:500]}"
|
||||
aid = cr.json()["id"]
|
||||
return True, f"创建 {spec.name} id={aid}"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
specs = AGENTS
|
||||
if ONLY:
|
||||
specs = [s for s in AGENTS if s.name == ONLY]
|
||||
if not specs:
|
||||
print(f"未找到名称完全匹配的 Agent 规格: {ONLY}", file=sys.stderr)
|
||||
print("可选名称:", "、".join(s.name for s in AGENTS), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
r = requests.post(
|
||||
f"{BASE}/api/v1/auth/login",
|
||||
data={"username": USER, "password": PWD},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
timeout=15,
|
||||
)
|
||||
if r.status_code != 200:
|
||||
print("登录失败:", r.status_code, r.text[:500], file=sys.stderr)
|
||||
return 1
|
||||
token = r.json().get("access_token")
|
||||
if not token:
|
||||
print("无 access_token", file=sys.stderr)
|
||||
return 1
|
||||
h = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
|
||||
ok_n = 0
|
||||
results: List[Dict[str, str]] = []
|
||||
for spec in specs:
|
||||
try:
|
||||
ok, msg = upsert_agent(h, spec)
|
||||
print(msg)
|
||||
results.append({"name": spec.name, "ok": str(ok), "message": msg})
|
||||
if ok:
|
||||
ok_n += 1
|
||||
except ValueError as e:
|
||||
print(spec.name, "校验失败:", e, file=sys.stderr)
|
||||
results.append({"name": spec.name, "ok": "false", "message": str(e)})
|
||||
|
||||
print(json.dumps({"base": BASE, "succeeded": ok_n, "total": len(specs), "details": results}, ensure_ascii=False))
|
||||
return 0 if ok_n == len(specs) else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user