Files
aiagent/backend/scripts/create_education_agents_batch.py

435 lines
16 KiB
Python
Raw Normal View History

#!/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())