mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:21:58 +00:00
183 lines
8.4 KiB
Python
183 lines
8.4 KiB
Python
"""分析设置(设置 → 偏好设置 → 分析,docs/design/analysis-round/01-算子与判定.md §10)和发起时的快照。
|
||
|
||
- 设置接口的 analysis 一组:键、缺省、范围、叫法全取 app/analysis/contract.py;GET 按生效的样子给(存坏了的项按缺省),
|
||
PUT 先查后写(不认识的键、类型、范围、两两之间的大小)
|
||
- 发起运行时快照进 run.options["analysis"]:只在图(含子工作流)里有分析卡、或 Agent / 协作成员绑了分析工具时写;
|
||
别的图的 run.started 一字不变
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
|
||
import pytest
|
||
from httpx import ASGITransport, AsyncClient
|
||
from sqlalchemy import select
|
||
|
||
from app.analysis import contract
|
||
from app.api.settings import ANALYSIS_GROUP, analysis_problem, analysis_values
|
||
from app.db.base import SessionLocal
|
||
from app.db.models import Run, RunEvent, Setting, Workflow
|
||
from app.engine.launch import launch_run
|
||
from app.engine.runner import run_manager
|
||
from app.main import app
|
||
|
||
#: 加 options 之前 run.started 就有的键(同 tests/test_run_options.py)
|
||
LEGACY_STARTED_KEYS = {"nodes", "resumed", "memory_scope", "collection", "approval_default", "tool_trust",
|
||
"agent_limits", "data_versions", "replay_protocol"}
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
async def engine_up():
|
||
await run_manager.setup()
|
||
yield
|
||
await run_manager.shutdown()
|
||
|
||
|
||
@pytest.fixture
|
||
async def client():
|
||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
||
yield c
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
async def clean_group():
|
||
yield
|
||
async with SessionLocal() as session:
|
||
row = await session.get(Setting, ANALYSIS_GROUP)
|
||
if row is not None:
|
||
await session.delete(row)
|
||
await session.commit()
|
||
|
||
|
||
def node(nid, ntype, **config):
|
||
return {"id": nid, "type": ntype, "position": {"x": 0, "y": 0}, "data": {"label": nid, "config": config}}
|
||
|
||
|
||
def chain(*nodes):
|
||
return {"nodes": list(nodes), "edges": [{"source": a["id"], "target": b["id"]} for a, b in zip(nodes, nodes[1:])]}
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 设置接口
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
def test_the_group_is_the_contract():
|
||
assert ANALYSIS_GROUP == contract.SETTINGS_KEY
|
||
assert analysis_values(None) == contract.DEFAULTS
|
||
# 存坏了的项按缺省;两两之间的大小对不上的那一对都按缺省
|
||
broken = {"main_share": "一半", "max_calls": 3.5, "k_warn": 4, "k_alert": 3.5, "min_history": 20, "max_history": 10,
|
||
"table_top_k": 5, "trend_alpha": True}
|
||
assert analysis_values(broken) == {**contract.DEFAULTS, "table_top_k": 5}
|
||
assert analysis_values({"main_share": 0.9, "concentration": 0.5}) == contract.DEFAULTS
|
||
# 浮点的项存成浮点:发起时的快照、结论里记的阈值一个写法
|
||
assert analysis_values({"k_warn": 3, "k_alert": 4})["k_warn"] == 3.0
|
||
assert isinstance(analysis_values({"k_warn": 3, "k_alert": 4})["k_alert"], float)
|
||
|
||
|
||
@pytest.mark.parametrize("value, why", [
|
||
("宽松", "需要是一个对象"),
|
||
({"bogus": 1}, "不支持「bogus」"),
|
||
({"main_share": 0.95}, f"{contract.LABELS['main_share']}要在 0.3 到 0.9 之间"),
|
||
({"max_calls": 2.5}, f"{contract.LABELS['max_calls']}要填整数"),
|
||
({"k_warn": 3.5}, f"{contract.LABELS['k_alert']}要大于{contract.LABELS['k_warn']}"),
|
||
({"min_history": 60}, f"{contract.LABELS['max_history']}不能小于{contract.LABELS['min_history']}"),
|
||
({"main_share": 0.9, "concentration": 0.5},
|
||
f"{contract.LABELS['concentration']}不能小于{contract.LABELS['main_share']}"),
|
||
])
|
||
def test_problems_are_said_with_labels(value, why):
|
||
assert why in (analysis_problem(value) or "")
|
||
|
||
|
||
async def test_get_put_round_trip(client):
|
||
got = (await client.get("/api/settings")).json()
|
||
assert got[ANALYSIS_GROUP] == contract.DEFAULTS
|
||
put = await client.put("/api/settings", json={"values": {ANALYSIS_GROUP: {"main_share": 0.6, "max_calls": 5}}})
|
||
assert put.status_code == 200, put.text
|
||
got = (await client.get("/api/settings")).json()[ANALYSIS_GROUP]
|
||
assert got == {**contract.DEFAULTS, "main_share": 0.6, "max_calls": 5}
|
||
# 整组替换:这次没写的项回到缺省
|
||
await client.put("/api/settings", json={"values": {ANALYSIS_GROUP: {"k_warn": 2.5}}})
|
||
assert (await client.get("/api/settings")).json()[ANALYSIS_GROUP] == {**contract.DEFAULTS, "k_warn": 2.5}
|
||
|
||
|
||
async def test_put_refuses_bad_values_and_stores_nothing(client):
|
||
r = await client.put("/api/settings", json={"values": {ANALYSIS_GROUP: {"main_share": 0.6, "k_alert": 1.5}}})
|
||
assert r.status_code == 422 and contract.LABELS["k_alert"] in r.json()["detail"]
|
||
assert (await client.get("/api/settings")).json()[ANALYSIS_GROUP] == contract.DEFAULTS
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 发起时的快照
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
async def _started(run_id: str) -> dict:
|
||
for _ in range(200):
|
||
async with SessionLocal() as session:
|
||
found = (await session.execute(select(RunEvent).where(
|
||
RunEvent.run_id == run_id, RunEvent.type == "run.started"))).scalars().first()
|
||
if found is not None:
|
||
await run_manager.wait_idle(run_id)
|
||
return found.data
|
||
await asyncio.sleep(0.05)
|
||
raise AssertionError("没有 run.started")
|
||
|
||
|
||
async def _launch(graph, **kw) -> tuple[Run, dict]:
|
||
async with SessionLocal() as session:
|
||
run = await launch_run(session, graph=graph, actor="测试", **kw)
|
||
return run, await _started(run.id)
|
||
|
||
|
||
CARD = node("card", "analysis", items=[{"id": "wow", "op": "baseline", "from": "q",
|
||
"params": {"period": "week", "measure": "gmv"}}])
|
||
QUERY = node("q", "tool", tool="calculator", args={"expression": "1 + 1"})
|
||
OUT = node("out", "output", fields=[{"name": "r", "value": "完成"}])
|
||
|
||
|
||
async def test_graphs_without_analysis_start_exactly_as_before():
|
||
run, started = await _launch(chain(node("start", "input"), node("t", "transform", mode="template", template="x"),
|
||
OUT))
|
||
assert set(started) == LEGACY_STARTED_KEYS, "没有分析的图,run.started 一个键都不多"
|
||
async with SessionLocal() as session:
|
||
assert (await session.get(Run, run.id)).options in (None, {})
|
||
# 绑了别的工具的 Agent:有能力治理的快照(以前就有),没有分析这一组
|
||
_, started = await _launch(chain(node("start", "input"),
|
||
node("bot", "agent", prompt="说一句", tools=["calculator"]), OUT))
|
||
assert ANALYSIS_GROUP not in (started.get("options") or {})
|
||
|
||
|
||
async def test_an_analysis_card_snapshots_the_settings():
|
||
async with SessionLocal() as session:
|
||
session.add(Setting(key=ANALYSIS_GROUP, value={"main_share": 0.6}))
|
||
await session.commit()
|
||
run, started = await _launch(chain(node("start", "input"), QUERY, CARD, OUT))
|
||
assert started["options"][ANALYSIS_GROUP] == {**contract.DEFAULTS, "main_share": 0.6}
|
||
async with SessionLocal() as session:
|
||
assert (await session.get(Run, run.id)).options[ANALYSIS_GROUP]["main_share"] == 0.6
|
||
|
||
|
||
@pytest.mark.parametrize("graph", [
|
||
chain(node("start", "input"), node("bot", "agent", prompt="为什么", tools=[contract.TOOL_NAMES["trend"]]), OUT),
|
||
chain(node("start", "input"), node("team", "supervisor", goal="为什么", agents=[
|
||
{"name": "分析员", "tools": [contract.TOOL_NAMES["differ"]]}]), OUT),
|
||
])
|
||
async def test_agents_and_members_with_analysis_tools_snapshot_the_settings(graph):
|
||
_, started = await _launch(graph)
|
||
assert started["options"][ANALYSIS_GROUP] == contract.DEFAULTS
|
||
|
||
|
||
async def test_a_card_inside_a_subgraph_counts_and_given_options_win():
|
||
async with SessionLocal() as session:
|
||
inner = Workflow(name="异动分析(子流程)", graph=chain(node("start", "input"), QUERY, CARD, OUT))
|
||
session.add(inner)
|
||
await session.commit()
|
||
inner_id = inner.id
|
||
outer = chain(node("start", "input"), node("sub", "subgraph", workflow_id=inner_id), OUT)
|
||
_, started = await _launch(outer)
|
||
assert started["options"][ANALYSIS_GROUP] == contract.DEFAULTS
|
||
# 调用方已经给了这一组的,照它的
|
||
_, started = await _launch(outer, options={ANALYSIS_GROUP: {"max_calls": 2}})
|
||
assert started["options"][ANALYSIS_GROUP] == {"max_calls": 2}
|