Files
aiagent/backend/scripts/create_education_agents_batch.py
renjianbo cadeb2dc32 fix: 修复热点摘要超长上下文并统一 Windows 启动文档
为 http_request 增加响应体截断与头部精简,避免门户首页触发 LLM 上下文超限;同时新增政务/媒体及教育批量 Agent 脚本,并将 Windows 启停说明合并为唯一指南,补充本次超时故障复盘与标准重启流程。

Made-with: Cursor
2026-04-30 00:10:19 +08:00

435 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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. 给出 **12 道同类变式** 的方向描述或自检问题(不必编造具体数字题除非用户需要且合理)。
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 表格输出计划时保持可打印、可勾选。
- 不保证分数;提醒睡眠与劳逸结合。
【原则】
- 若信息不足,先问 12 个关键问题再出计划。
""",
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())