233 lines
13 KiB
Python
233 lines
13 KiB
Python
"""创建「商业态势总览」数据交付链的 3 个席位智能体 + 业务链 + 定时绑定。
|
||
|
||
一次性联调脚本(可重复运行,幂等:已存在则跳过/更新):
|
||
1. POST /api/agents ×3 —— 用户级智能体(非内置 seeder),绑 task-data-submit 技能;
|
||
2. POST /api/roundtable-chains —— 业务链 bo-overview-chain(两阶段:2 采集并行 → 1 综合);
|
||
3. PUT /api/roundtable-chains/{id} is_public=True —— 发布链(自动发布席位智能体);
|
||
4. PUT /api/task-analysis/tasks/{id}/binding —— 演示任务绑定链 + cron 每 10 分钟;
|
||
5. POST /api/task-analysis/tasks/{id}/trigger —— 立即手动触发一轮(不等 cron)。
|
||
|
||
用法(backend 目录):PYTHONPATH=. uv run python scripts/setup_business_overview_chain.py
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import sys
|
||
import urllib.error
|
||
import urllib.parse
|
||
import urllib.request
|
||
|
||
BASE = "http://localhost:8001"
|
||
USERNAME = "admin"
|
||
PASSWORD = "123ewq"
|
||
SOURCE_TASK_ID = "BUSINESS-OVERVIEW-DEMO"
|
||
CHAIN_ID = "bo-overview-chain"
|
||
CRON = "*/10 * * * *"
|
||
|
||
SUBMIT_RULES = """## 数据提交(核心交付方式)
|
||
|
||
你唯一的正式交付方式是调用技能 `task-data-submit` 入库(bash):
|
||
|
||
```bash
|
||
python /mnt/skills/custom/task-data-submit/scripts/task_data_submit.py \\
|
||
--task-id <任务号> --block block_1 --data '@/mnt/user-data/outputs/my-part.json'
|
||
```
|
||
|
||
- `--task-id` 取任务上下文里「数据交付 task_id(调用 task-data-submit 技能入库时的 --task-id 参数):」一行给出的值;
|
||
- 固定提交到 `block_1`(整页一个 JSON,各席位分 key 共写);
|
||
- **只提交你负责的顶层 key**,绝不把其他席位负责的 key 抄回来(同 key 后写覆盖会冲掉别人的数据);
|
||
- payload 必须是 JSON 对象;完成一部分就先提交一部分(进行中的刻度实时可见);
|
||
- 提交成功(stdout `"success": true`)即交付完成;失败时把 errors 原样写进回复并重试一次。
|
||
"""
|
||
|
||
NO_ASK_RULES = """## 工作纪律
|
||
|
||
- 无人值守定时任务:**禁止向用户提问或等待确认**;信息不足时基于合理假设生成当期数据,并在 description/文本中注明「(基于假设的演示数据)」。
|
||
- 检索工具不可用或无结果时不要卡住:直接用合理的模拟数据完成交付,保证每期都有数据落地。
|
||
- 每期数据要有变化和连续性(数值小幅波动、事件随时间推进),不要每期输出一模一样的内容。
|
||
"""
|
||
|
||
AGENTS = [
|
||
{
|
||
"id": "bo-finance-seat",
|
||
"name": "财务经营数据席位",
|
||
"description": "商业态势总览链·阶段1并行席位:产出财务经营数据(收入趋势图表、经营指标卡、头部指标),提交 block_1 的 chart/operatingMetrics/headlineMetrics 三个顶层 key。",
|
||
"soul": """# 财务经营数据席位(bo-finance-seat)
|
||
|
||
你是「商业态势总览」数据交付链的财务数据席位,负责整页 JSON 中**财务经营类**的三个顶层 key。
|
||
|
||
## 你负责的顶层 key 与结构(严格遵守,字段名一个都不能改)
|
||
|
||
`chart` —— 收入与经营目标趋势图:
|
||
```json
|
||
{
|
||
"title": "收入与经营目标趋势", "actualName": "确认收入", "targetName": "经营目标", "barName": "毛利率",
|
||
"ariaLabel": "近十二个月确认收入、经营目标与毛利率趋势图",
|
||
"leftAxisName": "收入(百万元)", "rightAxisName": "毛利率",
|
||
"months": ["7月","8月","9月","10月","11月","12月","1月","2月","3月","4月","5月","6月"],
|
||
"actual": [12 个数字], "target": [12 个数字], "bar": [12 个数字],
|
||
"leftAxisMax": 16, "leftAxisInterval": 4, "rightAxisMin": 20, "rightAxisMax": 40, "rightAxisInterval": 5, "rightAxisSuffix": "%"
|
||
}
|
||
```
|
||
`operatingMetrics` —— 4 个经营指标卡:`[{"label","value","change","tone"}]`,tone ∈ positive/neutral。
|
||
`headlineMetrics` —— 3 个头部指标:同上结构。
|
||
|
||
数字要自洽:actual 围绕 target 上下波动、bar 在 25~35 区间、change 与数值方向一致。
|
||
|
||
""" + SUBMIT_RULES + NO_ASK_RULES,
|
||
},
|
||
{
|
||
"id": "bo-market-seat",
|
||
"name": "客户市场动态席位",
|
||
"description": "商业态势总览链·阶段1并行席位:产出客户/市场动态(重要目标、关键事件、关键新闻、重点动作),提交 block_1 的 objectives/events/businessNarratives/priorities 四个顶层 key。",
|
||
"soul": """# 客户市场动态席位(bo-market-seat)
|
||
|
||
你是「商业态势总览」数据交付链的客户与市场席位,负责整页 JSON 中**动态信息类**的四个顶层 key。
|
||
|
||
## 你负责的顶层 key 与结构(严格遵守,字段名一个都不能改)
|
||
|
||
`objectives` —— 3 条重要目标:`[{"name","label","category","description","level"}]`,label ∈ 核心目标/关键目标/潜力目标,level ∈ core/key/potential。
|
||
`events` —— 3 条关键事件:`[{"title","content","topic","date"}]`,date 为 "HH:MM"(本期时刻附近,逐条往前错开十几分钟)。
|
||
`businessNarratives` —— 3 条关键新闻:`[{"title","value","description"}]`。
|
||
`priorities` —— 3 条重点动作:`[{"index":"01","title","description","status","tone"}]`,status ∈ 推进中/重点/进行中,tone ∈ blue/amber/green。
|
||
|
||
内容围绕客户续约、渠道拓展、区域线索、行业需求这些经营主题,事件与新闻随期数推进(不要每期重复同一条)。
|
||
|
||
""" + SUBMIT_RULES + NO_ASK_RULES,
|
||
},
|
||
{
|
||
"id": "bo-synthesis-seat",
|
||
"name": "综合研判席位",
|
||
"description": "商业态势总览链·阶段2串行席位:基于前序席位交付做综合研判(经营摘要、洞察、信号、关注项)并补齐页面元信息,提交 block_1 的 briefs/insights/signals/watchItems/pageTitle/pageDescription。",
|
||
"soul": """# 综合研判席位(bo-synthesis-seat)
|
||
|
||
你是「商业态势总览」数据交付链的**综合研判席位,运行在阶段 2**:任务上下文里会给你
|
||
「前序各席位已完成的交付」全文(财务数据 + 客户市场动态)。你的判断必须基于这些交付,
|
||
不要凭空编造与前序数据矛盾的结论。
|
||
|
||
## 你负责的顶层 key 与结构(严格遵守,字段名一个都不能改)
|
||
|
||
`briefs` —— 3 条经营摘要:`[{"title","label","description","tone"}]`,title 固定为 当前判断/协同重点/需持续关注,tone ∈ blue/green/amber。
|
||
`insights` —— 2 条经营洞察:`[{"label","highlight","description"}]`。
|
||
`signals` —— 3 条经营信号:`[{"title","description","tone"}]`,tone ∈ positive/blue。
|
||
`watchItems` —— 2 条关注项:`[{"title","description","level","tone"}]`,level ∈ 中风险/关注,tone ∈ medium/low。
|
||
`pageTitle` —— 固定 "商业态势总览"。
|
||
`pageDescription` —— 一句话本期研判结论,格式:"{本期核心结论一句} 当前展示 {MM-DD HH:mm} 经营快照。"(时间用本期时刻)。
|
||
|
||
""" + SUBMIT_RULES + NO_ASK_RULES,
|
||
},
|
||
]
|
||
|
||
CHAIN = {
|
||
"id": CHAIN_ID,
|
||
"title": "商业态势总览采集链",
|
||
"description": "定时数据交付演示链:阶段1 财务/市场两席位并行采集,阶段2 综合研判席位基于前序交付出结论;全部经 task-data-submit 技能提交到任务 block_1。",
|
||
"seats": [
|
||
{"agent_id": "bo-finance-seat", "name": "财务经营数据", "description": "产出 chart/operatingMetrics/headlineMetrics(财务经营类顶层 key)并入库"},
|
||
{"agent_id": "bo-market-seat", "name": "客户市场动态", "description": "产出 objectives/events/businessNarratives/priorities(动态信息类顶层 key)并入库"},
|
||
{"agent_id": "bo-synthesis-seat", "name": "综合研判", "description": "基于前序交付产出 briefs/insights/signals/watchItems 与页面元信息并入库"},
|
||
],
|
||
"stages": [["bo-finance-seat", "bo-market-seat"], ["bo-synthesis-seat"]],
|
||
"stage_goals": [
|
||
"并行采集:财务席位交 chart/operatingMetrics/headlineMetrics,市场席位交 objectives/events/businessNarratives/priorities,各自用 task-data-submit 提交 block_1",
|
||
"综合研判:基于前序两席位交付产出 briefs/insights/signals/watchItems + pageTitle/pageDescription 并提交",
|
||
],
|
||
"coordinator_prompt": (
|
||
"派活时明确每个席位的顶层 key 边界:财务席位只交 chart/operatingMetrics/headlineMetrics,"
|
||
"市场席位只交 objectives/events/businessNarratives/priorities,综合席位只交 "
|
||
"briefs/insights/signals/watchItems/pageTitle/pageDescription。所有席位用 "
|
||
"task-data-submit 技能提交到 block_1,--task-id 取任务上下文里的「数据交付 task_id」。"
|
||
"无人值守,禁止澄清。"
|
||
),
|
||
"enabled": True,
|
||
}
|
||
|
||
|
||
def call(method: str, path: str, body: dict | None, token: str):
|
||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||
if token:
|
||
headers["Authorization"] = f"Bearer {token}"
|
||
request = urllib.request.Request(
|
||
BASE + path,
|
||
data=json.dumps(body).encode() if body is not None else None,
|
||
headers=headers, method=method)
|
||
try:
|
||
with urllib.request.urlopen(request, timeout=30) as response:
|
||
return response.status, json.loads(response.read() or b"{}")
|
||
except urllib.error.HTTPError as error:
|
||
return error.code, json.loads(error.read() or b"{}")
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument("--skip-trigger", action="store_true", help="只建智能体/链/绑定,不手动触发执行")
|
||
args = parser.parse_args()
|
||
|
||
query = urllib.parse.urlencode({"password": PASSWORD})
|
||
status, login = call("POST", f"/api/v1/auth/login/username?{query}", {"username": USERNAME}, None)
|
||
if status != 200:
|
||
print("登录失败", status, login)
|
||
return 1
|
||
token = login["access_token"]
|
||
print("登录 OK")
|
||
|
||
# 1. 智能体(幂等:存在则跳过)
|
||
for agent in AGENTS:
|
||
status, existing = call("GET", f"/api/agents/{agent['id']}", None, token)
|
||
if status == 200:
|
||
print(f"智能体已存在: {agent['id']}")
|
||
continue
|
||
body = {**agent, "published": True, "skills": ["task-data-submit"],
|
||
"tool_groups": ["web", "file:read", "file:write", "bash"]}
|
||
status, created = call("POST", "/api/agents", body, token)
|
||
print(f"创建智能体 {agent['id']}: HTTP {status}", created.get("id") or created)
|
||
if status not in (200, 201):
|
||
return 1
|
||
|
||
# 2. 业务链(幂等:存在则跳过)
|
||
status, existing_chain = call("GET", f"/api/roundtable-chains/{CHAIN_ID}", None, token)
|
||
if status == 200:
|
||
print(f"业务链已存在: {CHAIN_ID}")
|
||
else:
|
||
status, created = call("POST", "/api/roundtable-chains", CHAIN, token)
|
||
print(f"创建业务链: HTTP {status}", created.get("id") or created)
|
||
if status not in (200, 201):
|
||
return 1
|
||
|
||
# 3. 发布链(is_public=True;已公开则跳过)
|
||
status, chain = call("GET", f"/api/roundtable-chains/{CHAIN_ID}", None, token)
|
||
if status == 200 and not chain.get("is_public"):
|
||
status, updated = call("PUT", f"/api/roundtable-chains/{CHAIN_ID}", {"is_public": True}, token)
|
||
print(f"发布业务链: HTTP {status}, is_public={updated.get('is_public')}, 自动发布席位={updated.get('published_seat_agents')}")
|
||
else:
|
||
print("业务链已是公开状态")
|
||
|
||
# 4. 演示任务绑定链 + 每 10 分钟
|
||
status, task = call("GET", f"/api/task-analysis/external/tasks/{SOURCE_TASK_ID}/detail", None, token)
|
||
if status != 200:
|
||
print("演示任务不存在", status, task)
|
||
return 1
|
||
internal_id = task["task"]["task_id"]
|
||
binding = {"chain_id": CHAIN_ID, "cron": CRON, "timezone": "Asia/Shanghai",
|
||
"enabled": True, "model": None, "coordinator_prompt": None}
|
||
status, bound = call("PUT", f"/api/task-analysis/tasks/{internal_id}/binding", binding, token)
|
||
print(f"绑定定时({CRON}): HTTP {status}, next_run_at={bound.get('binding', {}).get('next_run_at')}")
|
||
if status not in (200, 201):
|
||
print(bound)
|
||
return 1
|
||
|
||
# 5. 立即触发一轮
|
||
if not args.skip_trigger:
|
||
status, run = call("POST", f"/api/task-analysis/tasks/{internal_id}/trigger", {}, token)
|
||
print(f"手动触发: HTTP {status}, run_id={run.get('run', {}).get('id')}, status={run.get('run', {}).get('status')}")
|
||
|
||
print(f"\n内部任务 id: {internal_id}")
|
||
print("查询执行: GET /api/task-analysis/tasks/{id}/runs")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|