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

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

369 lines
15 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
"""
批量创建或更新「政务 / 公共服务(办事指引)」与「媒体 / 市场 / 运营」场景 AgentStart → 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 中的矛盾或缺失项,列出需客户/内部确认的 **澄清问题**一次35 个)。
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** 标注摘要基准日或「截至今日」表述。
【边界】
- **不编造**引文、数据、发言人言论;原文没有则写「原文未提及」。
- 付费墙、登录墙内容可能无法抓取,提示用户粘贴正文或换公开来源。
- 不做投资建议;涉政敏感解读保持克制,以信息整理为主。
【输出】
- 中文先36 条 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())