mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 02:51:58 +00:00
7f66985dba
新增 caliber_drafts 表与 /api/ledger/drafts 一组接口:抽取在后台跑,文档缺失整单拒绝, 有未决问题时停在 questions_open,问题清完才到 path_pending。 接上运行沿用从运行固化的建图逻辑,定义按同名或同义词并入指标卡,expression 与 id 不动; 文档引用记进 origin.documents,复核能落回具体段落。接入只产出可试跑的草稿,不发布、不发起运行。
150 lines
7.8 KiB
Python
150 lines
7.8 KiB
Python
"""文档草稿接上一次真实运行(app/ledger/docattach.py)。
|
||
|
||
真的跑一次问数据(虚构店铺库),等后台比对做完,再把文档草稿接上这次运行:
|
||
- 取数路径来自运行,定义并进口径卡的同名指标,expression / id 一字不改;
|
||
- origin 记着用到的文档段落(document_id / chunk_id / ordinal / quote,引用去重)和 draft_id;
|
||
- 没成功、没比对、报告没引用查询的运行都接不上(409);
|
||
- 草稿里对不上的指标名进 warnings,不拦这一次接入;
|
||
- 只落草稿:不发布、不发起运行。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import uuid
|
||
|
||
import pytest
|
||
from sqlalchemy import select
|
||
|
||
from app.core.artifact_store import graph_hash
|
||
from app.db.base import SessionLocal
|
||
from app.db.models import Chunk, Conversation, ConversationTurn, Document, Run, Workflow, WorkflowVersion
|
||
from app.engine.runner import run_manager
|
||
from app.ledger import docattach
|
||
from app.ledger import match as ledger_match
|
||
from tests import ledger_samples as S
|
||
|
||
#: 每个用例一句话的问题:问句进全站唯一的聚类,和别的用例用同一句会在 certify 的队列里互相看见
|
||
QUESTION = f"文档接上运行后卖了多少({uuid.uuid4().hex[:8]})"
|
||
START, END = "2026-09-01", "2026-09-08"
|
||
SQL = (f"SELECT SUM(amount) AS gmv FROM orders WHERE status = 'paid' AND paid_at >= '{START}' "
|
||
f"AND paid_at < '{END}'")
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
async def engine_up(tmp_path):
|
||
await run_manager.setup()
|
||
await S.fresh_source(tmp_path / "shop.db")
|
||
yield
|
||
await ledger_match.drain()
|
||
await run_manager.shutdown()
|
||
|
||
|
||
async def ask(monkeypatch) -> Run:
|
||
"""问数据的一轮:建对话和轮次、跑一次探索运行、等比对做完。"""
|
||
async with SessionLocal() as session:
|
||
conv = Conversation(title="问销售", kind="chat")
|
||
session.add(conv)
|
||
await session.commit()
|
||
turn = ConversationTurn(conversation_id=conv.id, question=QUESTION)
|
||
session.add(turn)
|
||
await session.commit()
|
||
turn_id = turn.id
|
||
S.script(monkeypatch, "这一周销售额 [[v:Q1.r0.gmv]]。")
|
||
run = await run_manager.start(graph=S.explore_graph(SQL), input_payload={"question": QUESTION},
|
||
origin_kind="chat", origin={"conversation_id": conv.id, "turn_id": turn_id})
|
||
row = await S.wait(run.id)
|
||
assert row.status == "succeeded", row.error
|
||
async with SessionLocal() as session:
|
||
(await session.get(ConversationTurn, turn_id)).run_id = row.id
|
||
await session.commit()
|
||
await ledger_match.drain()
|
||
return row
|
||
|
||
|
||
async def document() -> tuple[str, str]:
|
||
"""一份业务文档和其中的一段原文(口径定义的事实来源)。"""
|
||
async with SessionLocal() as session:
|
||
doc = Document(title="销售口径说明", content="销售额按已支付订单统计,退款订单不计入。")
|
||
session.add(doc)
|
||
await session.flush()
|
||
chunk = Chunk(document_id=doc.id, ordinal=3, content="销售额按已支付订单统计,退款订单不计入。")
|
||
session.add(chunk)
|
||
await session.commit()
|
||
return doc.id, chunk.id
|
||
|
||
|
||
def payload(document_id: str, chunk_id: str, *, name: str = "gmv") -> dict:
|
||
citation = {"chunk": chunk_id, "document_id": document_id, "quote": "销售额按已支付订单统计"}
|
||
return {"question": QUESTION, "metrics": [{
|
||
"name": name, "synonyms": ["销售额"], "unit": "元",
|
||
"definition": {"text": "按已支付订单统计,退款不计入", "citations": [citation, dict(citation)]}}]}
|
||
|
||
|
||
async def test_attach_merges_the_definition_into_the_run_derived_draft(monkeypatch):
|
||
run = await ask(monkeypatch)
|
||
doc_id, chunk_id = await document()
|
||
async with SessionLocal() as session:
|
||
made = await docattach.attach(session, payload=payload(doc_id, chunk_id), name="周销售口径",
|
||
run_id=run.id, draft_id="draft-1")
|
||
assert made["version"] == 1 and made["warnings"] == []
|
||
[metric] = made["metrics"]
|
||
assert metric["id"] == "m1" and metric["expression"] == "cell(nodes.fetch, 0, 'gmv')"
|
||
assert metric["definition"]["text"] == "按已支付订单统计,退款不计入"
|
||
|
||
async with SessionLocal() as session:
|
||
wf = await session.get(Workflow, made["workflow_id"])
|
||
version = (await session.execute(
|
||
select(WorkflowVersion).where(WorkflowVersion.workflow_id == wf.id))).scalar_one()
|
||
assert wf.status == "draft" and wf.tags == ["doc_draft"] and wf.published_version is None
|
||
assert version.version == 1 and version.graph_hash == graph_hash(wf.graph)
|
||
assert version.note == f"从文档草稿接上运行 {run.id}"
|
||
card = next(n for n in wf.graph["nodes"] if n["type"] == "metrics")["data"]["config"]
|
||
[card_metric] = card["metrics"]
|
||
assert card_metric["id"] == "m1" and card_metric["expression"] == "cell(nodes.fetch, 0, 'gmv')", \
|
||
"草稿只并定义,不改取数路径"
|
||
assert card_metric["definition"]["text"] == "按已支付订单统计,退款不计入"
|
||
assert card["caliber"] == "周销售口径"
|
||
# 取数路径照抄运行:字面量都是常量、没有入口参数,SQL 原样
|
||
fetch = next(n for n in wf.graph["nodes"] if n["type"] == "tool")
|
||
assert fetch["data"]["config"]["args"]["sql"] == SQL and fetch["data"]["config"]["tool"] == S.tool()
|
||
|
||
# origin:来源运行、草稿 id、引用的文档段落(两条一样的引用去重,ordinal 从库里补)
|
||
assert wf.origin["run_id"] == run.id and wf.origin["freeze"] is True
|
||
assert wf.origin["sources"] == [{"run_id": run.id}] and wf.origin["draft_id"] == "draft-1"
|
||
assert wf.origin["documents"] == [{"document_id": doc_id, "chunk_id": chunk_id, "ordinal": 3,
|
||
"quote": "销售额按已支付订单统计"}]
|
||
assert "1 个指标" in wf.description and run.id in wf.description
|
||
|
||
|
||
async def test_attach_refuses_runs_that_are_missing_failed_or_not_matched(monkeypatch):
|
||
async with SessionLocal() as session:
|
||
session.add(Run(id="failed-run", status="failed", graph={}, input={}, output={}))
|
||
session.add(Run(id="unmatched-run", status="succeeded", graph={}, input={}, output={}))
|
||
await session.commit()
|
||
for run_id, message in (("nope", docattach.NOT_FOUND),
|
||
("failed-run", docattach.NOT_SUCCEEDED),
|
||
("unmatched-run", docattach.NOT_MATCHED)):
|
||
with pytest.raises(docattach.AttachRefused) as caught:
|
||
await docattach.attach(session, payload={}, name="周销售口径", run_id=run_id)
|
||
assert caught.value.status == 409 and caught.value.message == message
|
||
|
||
# 成功、也比对过,但报告没引用任何查询:没有可接上的取数
|
||
row = await S.explore(monkeypatch, S.ELSEWHERE, text="一共几家店,没有引用。")
|
||
async with SessionLocal() as session:
|
||
with pytest.raises(docattach.AttachRefused) as caught:
|
||
await docattach.attach(session, payload={}, name="周销售口径", run_id=row.id)
|
||
assert caught.value.status == 409 and caught.value.message == docattach.NO_QUERIES
|
||
|
||
|
||
async def test_a_metric_name_that_does_not_meet_the_card_goes_to_warnings(monkeypatch):
|
||
run = await ask(monkeypatch)
|
||
doc_id, chunk_id = await document()
|
||
async with SessionLocal() as session:
|
||
made = await docattach.attach(session, payload=payload(doc_id, chunk_id, name="毛利率"),
|
||
name="毛利口径", run_id=run.id)
|
||
assert len(made["warnings"]) == 1 and "毛利率" in made["warnings"][0]
|
||
[metric] = made["metrics"]
|
||
assert metric["id"] == "m1" and "definition" not in metric, "对不上的定义不往指标上写"
|
||
async with SessionLocal() as session:
|
||
wf = await session.get(Workflow, made["workflow_id"])
|
||
assert wf is not None and wf.status == "draft"
|