mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 10:31:59 +00:00
277982ead3
- 蓝图:bp_* 操作和逐卡白名单、蓝图检查、留痕、按蓝图判权限;五个场景模板加自由形(骨架、关节、应用骨架、定时任务),实例化与骨架比对;BlueprintApplier / TemplateApplier(只收模板声明过的关节)与自查;look 新类别口径卡;蓝图对话和填关节两个流式接口,不同组的关节并行填、出错的组串行重试;模板列表 - 试运行与发布单:四种起点(对话、模板、问数据的一轮、已发布版本);试运行是探索运行,草稿应用按这次运行解析、印章写试运行,流程类派给自己、不触发升级;发布单九步、预检只读、每步留痕、停下后接着做、核对卡片指纹和应用管理权、启动时整理中断的单;白话层覆盖全部问题编号 - 记录表与文件:平台托管只追加的记录表、按高水位固定数据、连接上的临时视图做行级可见、写入走动作(records_append)、表版本在发布时切换、草稿不影响线上、记录列表遮敏感列;文件按哈希存、两步上传、提交时绑定运行、下载按读权限、清理、file_text、按配方导入一期(table_import_period) - 个人应用:存成个人应用(问数据的约束)、刷新(遮罩、只读、每日上限)、只有拥有者看得见、提交审核与共享副本、开关下发到认人接口;页面编辑的候选;隐藏组件不下发;三条新检查
129 lines
6.5 KiB
Python
129 lines
6.5 KiB
Python
"""蓝图、试运行、发布单测试共用的样本(docs/design/normal-user-apps.md 包 B2)。数据全是虚构的。
|
||
|
||
- 一个小的 SQLite 数据源(门店订单,金额、支付日),登记成只读数据源;
|
||
- 看板图:统计期 → 查订单 → 口径卡 → 按指标出具(不调模型);
|
||
- 流程图:发起报销 → 财务会签(岗位 finance,1 小时超时升级给 director)→ 已通过 / 已退回;
|
||
- 存工作流并直接落一个发布版本(发布门禁另有测试,这里要的是一个已发布的不可变版本)、等运行结束、改蓝图卡片。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import sqlite3
|
||
import uuid
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
from app.core.artifact_store import graph_hash
|
||
from app.db.base import SessionLocal, utcnow
|
||
from app.db.models import DataSource, Run, Workflow, WorkflowVersion
|
||
from app.db.models_blueprints import Blueprint
|
||
|
||
GMV_SQL = ("SELECT ROUND(SUM(amount), 2) AS gmv, COUNT(*) AS orders FROM orders "
|
||
"WHERE paid_at BETWEEN '{{ input.period_start }}' AND '{{ input.period_end }}'")
|
||
PERIOD = {"period": "2026-W40", "period_start": "2026-09-28", "period_end": "2026-10-04"}
|
||
|
||
|
||
def node(nid: str, ntype: str, label: str | None = None, **config: Any) -> dict[str, Any]:
|
||
return {"id": nid, "type": ntype, "position": {"x": 0, "y": 0}, "data": {"label": label or nid, "config": config}}
|
||
|
||
|
||
def chain(*nodes: dict[str, Any]) -> dict[str, Any]:
|
||
return {"nodes": list(nodes), "edges": [{"source": a["id"], "target": b["id"]} for a, b in zip(nodes, nodes[1:])]}
|
||
|
||
|
||
def make_source(tmp_path: Path) -> str:
|
||
"""建库、登记数据源,交回数据源名(每个测试一个,名字不撞)。同步写库文件,登记在调用方的事件循环里做。"""
|
||
path = tmp_path / f"shop_{uuid.uuid4().hex[:6]}.db"
|
||
with sqlite3.connect(path) as db:
|
||
db.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, amount REAL, paid_at TEXT)")
|
||
db.executemany("INSERT INTO orders (amount, paid_at) VALUES (?, ?)",
|
||
[(120.5, "2026-09-29"), (80.0, "2026-10-01"), (999.0, "2026-10-09")])
|
||
return str(path)
|
||
|
||
|
||
async def register_source(path: str) -> str:
|
||
name = f"shop_{uuid.uuid4().hex[:6]}"
|
||
async with SessionLocal() as session:
|
||
session.add(DataSource(name=name, kind="sqlite", database=path, readonly=True, enabled=True))
|
||
await session.commit()
|
||
return name
|
||
|
||
|
||
def board_graph(source: str) -> dict[str, Any]:
|
||
"""统计期 → 查订单 → 口径卡 → 按指标出具。"""
|
||
return chain(
|
||
node("start", "input", "统计期", fields=[
|
||
{"name": "period", "default": PERIOD["period"]},
|
||
{"name": "period_start", "default": PERIOD["period_start"]},
|
||
{"name": "period_end", "default": PERIOD["period_end"]}]),
|
||
node("q", "tool", "查订单", tool=f"db_query__{source}", args={"sql": GMV_SQL}),
|
||
node("caliber", "metrics", "周销售口径卡", caliber="周销售口径", caliber_version="v1", metrics=[
|
||
{"id": "gmv", "name": "销售额", "unit": "元", "decimals": 2, "expression": "cell(nodes.q, 0, 'gmv')"}]),
|
||
node("done", "output", "出具", fields=[{"name": "销售额", "value": "{{ nodes.caliber.metrics[0].value }}"}],
|
||
contract={"metrics_from": ["caliber"], "required": ["gmv"], "strict": True}),
|
||
)
|
||
|
||
|
||
def expense_graph() -> dict[str, Any]:
|
||
nodes = [node("start", "input", "发起报销", fields=[{"name": "applicant"}, {"name": "amount", "type": "number"}]),
|
||
node("review", "human", "财务会签", mode="approve", title="报销审批:{{ input.applicant }}",
|
||
assignee={"kind": "slot", "slot": "finance"}, policy="all", due="PT1H",
|
||
on_timeout={"action": "escalate", "to_slot": "director"}),
|
||
node("ok", "output", "已通过", fields=[{"name": "结论", "value": "已通过"}]),
|
||
node("no", "output", "已退回", fields=[{"name": "结论", "value": "已退回"}])]
|
||
edges = [{"source": "start", "target": "review"},
|
||
{"source": "review", "target": "ok", "sourceHandle": "approved"},
|
||
{"source": "review", "target": "no", "sourceHandle": "rejected"}]
|
||
return {"nodes": nodes, "edges": edges}
|
||
|
||
|
||
async def published_workflow(graph: dict[str, Any], name: str, *, description: str = "",
|
||
level: str = "published") -> str:
|
||
"""存工作流并把 v1 立为发布版本(直接落库)。交回工作流 id。"""
|
||
async with SessionLocal() as session:
|
||
wf = Workflow(name=name, description=description, graph=graph, version=1, published_version=1,
|
||
status=level)
|
||
session.add(wf)
|
||
await session.flush()
|
||
session.add(WorkflowVersion(workflow_id=wf.id, version=1, graph=graph, graph_hash=graph_hash(graph),
|
||
level=level, published_at=utcnow(), published_by="样本"))
|
||
await session.commit()
|
||
return wf.id
|
||
|
||
|
||
async def wait(run_id: str, statuses: tuple[str, ...] = ("succeeded", "failed", "cancelled"),
|
||
timeout: float = 30.0) -> Run:
|
||
"""等运行走到这几种状态之一(终态的还要等封存完)。"""
|
||
from app.engine.runner import run_manager
|
||
|
||
deadline = asyncio.get_event_loop().time() + timeout
|
||
row = None
|
||
while asyncio.get_event_loop().time() < deadline:
|
||
async with SessionLocal() as session:
|
||
row = await session.get(Run, run_id)
|
||
if row is not None and row.status in statuses and not run_manager.is_active(run_id) and (
|
||
row.status == "interrupted" or row.manifest_seq is not None):
|
||
return row
|
||
await asyncio.sleep(0.05)
|
||
raise AssertionError(f"运行 {run_id} 没有走到 {statuses}:{row.status if row else None}")
|
||
|
||
|
||
def item(value: Any, source: str = "user", status: str = "confirmed") -> dict[str, Any]:
|
||
return {"value": value, "source": source, "status": status, "why": None}
|
||
|
||
|
||
async def edit_spec(blueprint_id: str, **cards: Any) -> None:
|
||
"""直接改蓝图的卡片(卡片上的修改另有测试,这里要的是一张填好的蓝图)。"""
|
||
async with SessionLocal() as session:
|
||
bp = await session.get(Blueprint, blueprint_id)
|
||
bp.spec = {**(bp.spec or {}), **cards}
|
||
await session.commit()
|
||
|
||
|
||
WEEKLY = {"preset": "weekly", "weekday": 1, "time": "09:00", "tz": "Asia/Shanghai", "period": "last_iso_week"}
|
||
|
||
|
||
async def blueprint(blueprint_id: str) -> Blueprint:
|
||
async with SessionLocal() as session:
|
||
return await session.get(Blueprint, blueprint_id)
|