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