核查工具箱收尾:连接的方面补上另一侧(关联条件里出现的别的表,CROSS JOIN 写它之前的来源),双盲复算对照数据目录里的关系时用得上;只读 agent 收尾前先拿到收尾用的模型再预留金额,模型拿不到时不在每日账上白记一次调用;收尾消息的参数改名 limit_words,说清是哪个上限的说法

This commit is contained in:
ThearchyHelios
2026-10-05 00:42:02 +08:00
parent 1efa5f7b20
commit f1c068d6a9
4 changed files with 27 additions and 17 deletions
+10 -4
View File
@@ -217,12 +217,14 @@ class Part:
@dataclass(frozen=True)
class JoinFacet:
"""一次连接。kind:inner / left / right / full / cross / semi / anti / natural。on 是关联条件的合取式排好序后
用 AND 连起来的写法,没有条件是 None。"""
"""一次连接。kind:inner / left / right / full / cross / semi / anti / natural。table 是关联进来的那一侧,other
是另一侧:关联条件里出现的别的表(给人看的真实表名);没有条件的(CROSS JOIN)是在它之前出现的各个来源。on 是
关联条件的合取式排好序后用 AND 连起来的写法,没有条件是 None。"""
kind: str
table: Part
on: Part | None = None
other: tuple[str, ...] = ()
@dataclass(frozen=True)
@@ -688,7 +690,8 @@ class _Statement:
conds = [self.canonical(exp.EQ(this=exp.column(ident.name, table=first_alias),
expression=exp.column(ident.name, table=alias)), sources)
for ident in join.args["using"]]
drafts.append({"kind": _join_kind(join), "slot": slot, "source": sources.get(slot), "conds": conds})
drafts.append({"kind": _join_kind(join), "slot": slot, "source": sources.get(slot), "conds": conds,
"before": [s.name for key, s in list(sources.items())[:i + 1] if key != slot]})
return drafts
def _joins_on_where(self, cond: Any, sources: dict[str, _Source], drafts: list[dict[str, Any]]) -> bool:
@@ -720,7 +723,10 @@ class _Statement:
on = Part(key=" AND ".join(t[0] for t in triples), text=" AND ".join(t[2] for t in triples),
loose=" AND ".join(sorted(t[1] for t in triples)), full=" AND ".join(t[2] for t in triples)) \
if triples else None
return JoinFacet(kind=draft["kind"], table=table, on=on)
mentioned = {table for c in draft["conds"] for table, _ in self.part(c, sources).columns}
other = [name for name in dict.fromkeys(t for t in mentioned if t) if name != table.text] if draft["conds"] \
else draft["before"]
return JoinFacet(kind=draft["kind"], table=table, on=on, other=tuple(sorted(other)))
def _time(self, cond: Any, sources: dict[str, _Source]) -> TimePredicate | None:
"""时间条件:一边恰好一列(可以套着日期函数、CAST),另一边没有列;列名像日期,或者用了日期函数、像日期的字符串。"""
+11 -11
View File
@@ -323,12 +323,13 @@ def _text_of(messages: list[BaseMessage]) -> str:
for m in messages)
def _final_messages(system: str, task: str, queries: list[dict[str, Any]], said: str, stopped: str | None
def _final_messages(system: str, task: str, queries: list[dict[str, Any]], said: str, limit_words: str | None
) -> list[BaseMessage]:
"""收尾那次调用的消息:任务、查到的结果(每次查询列出行号)、模型最后说的话,以及交结果的规则。"""
"""收尾那次调用的消息:任务、查到的结果(每次查询列出行号)、模型最后说的话,以及交结果的规则。
limit_words:触顶时是哪个上限(「最多查询 6 次」),告诉模型为什么不能再查。"""
parts = [f"任务:\n{task.strip()}"]
if stopped:
parts.append(f"已达上限({stopped}),不能再查询,按已经查到的结果交结果。")
if limit_words:
parts.append(f"已达上限({limit_words}),不能再查询,按已经查到的结果交结果。")
parts.append(f"你查到的结果:\n{_digest(queries) if queries else '(没有成功的查询)'}")
parts.append(f"你最后的说明(只作参考,数以查询结果为准):\n{said.strip() or '(没有)'}")
parts.append(VERIFY_FINAL_PROMPT)
@@ -541,7 +542,12 @@ async def run_verify_agent(
await _announce(emit, EventType.LOG, level="warn", code="verify_limit", limit=stopped,
message=f"核查已达上限({words}),已停止查询,按已有结果收尾")
# ---- 收尾:按结构交结果
# ---- 收尾:按结构交结果。关掉 thinking:结构化输出靠强制工具调用,和思考模式不兼容
try:
async with SessionLocal() as session:
settler, _ = await get_chat_model(session, spec.model_copy(update={"thinking": "off"}))
except Exception as e: # noqa: BLE001
return outcome(model=model_id, limit=stopped, answered=answered, error=f"核查模型不可用({describe_exception(e)})")
schema = result_schema(output_schema)
final = _final_messages(head, task, queries, said, words)
estimate = budget.estimate(_text_of(final), _OUT_FINAL)
@@ -554,12 +560,6 @@ async def run_verify_agent(
remaining = left()
grace = None if remaining is None else max(remaining, SETTLE_GRACE_S)
finish = None if grace is None else time.monotonic() + grace
try:
async with SessionLocal() as session:
settler, _ = await get_chat_model(session, spec.model_copy(update={"thinking": "off"}))
except Exception as e: # noqa: BLE001
await spend.settle(day or "", -estimate)
return outcome(model=model_id, limit=stopped, answered=answered, error=f"核查模型不可用({describe_exception(e)})")
def until_finish() -> float | None:
return None if finish is None else finish - time.monotonic()
+4 -1
View File
@@ -116,7 +116,7 @@ def test_tables_are_the_real_tables_without_cte_names():
def test_joins_carry_kind_both_sides_and_a_normalized_condition():
f = facets("SELECT o.id FROM orders o LEFT JOIN refunds r ON r.order_id = o.id AND r.state = 'done'")
[join] = f.joins
assert join.kind == "left" and join.table.text == "refunds"
assert join.kind == "left" and join.table.text == "refunds" and join.other == ("orders",)
# 别名换成真实表名,两边排好顺序,几个条件按 key 排序后用 AND 连起来
assert join.on.text == "orders.id = refunds.order_id AND refunds.state = 'done'"
same = facets("SELECT orders.id FROM orders LEFT OUTER JOIN refunds ON refunds.state = 'done' "
@@ -128,6 +128,9 @@ def test_a_comma_join_is_the_same_as_an_inner_join():
comma = facets("SELECT a.x FROM orders a, stores b WHERE a.store_id = b.id AND b.city = '虚构市'")
inner = facets("SELECT orders.x FROM orders JOIN stores ON stores.id = orders.store_id WHERE stores.city = '虚构市'")
assert comma.joins == inner.joins and comma.joins[0].kind == "inner"
assert comma.joins[0].table.text == "stores" and comma.joins[0].other == ("orders",)
cross = facets("SELECT 1 FROM orders o CROSS JOIN stores s")
assert (cross.joins[0].kind, cross.joins[0].on, cross.joins[0].other) == ("cross", None, ("orders",))
assert [p.key for p in comma.where] == [p.key for p in inner.where] == ["stores.city = '虚构市'"]
+2 -1
View File
@@ -212,7 +212,8 @@ async def test_writes_are_refused_on_a_writable_source(monkeypatch, shop):
script = Script(monkeypatch, [q(name, "DELETE FROM orders WHERE is_deleted = 1", "w1"),
q(name, "WITH k AS (SELECT 1) UPDATE orders SET amount = 0", "w2"),
q(name, VALID_SUM, "c1")],
{"value": {"alias": "Q1", "row": 0, "column": "total"}, "verdict": "一致", "rationale": "见 [[see:Q1]]。"})
{"value": {"alias": "Q1", "row": 0, "column": "total"}, "verdict": "一致",
"rationale": "见 [[see:Q1]]。"})
out = await run(name)
seen = script.tool_results()
assert seen["w1"].startswith("SQL 被拒绝:") and seen["w2"].startswith("SQL 被拒绝:")