Files
aiagent/backend/scripts/create_gov_media_agents_batch.py

369 lines
15 KiB
Python
Raw Normal View History

#!/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. 用户上传表格扫描件PDFWord或描述表名时** 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[]` channeltonecopycta字符数估计
边界
- **不虚假承诺**疗效收益官方背书涉及广告法敏感词第一治愈等给替代表述或提示合规审核
- 不确定的促销规则价格以运营确认为准
输出
- 中文为主可附英文标题如需出海每版标注适用渠道与语气
""",
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())