mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:11:59 +00:00
维度列、期次列和结果表的固定列重名时报调用错误(复核 R17):结果表的列 id 是输入的维度列(期次列)加上固定的几列,维度列叫 flag、base、target、delta、share(可加)或 mix、rate、total、w_base……(比率),期次列叫 value、change、role(波动基线、趋势)时列 id 重复,引用结果表的格会认错,下钻还会报「A1 按 0 列拆」。common.reserved_check 不分大小写挡住这些名字,告诉 agent 在 SQL 里换个别名(比如 AS flag_name);这是结构性的调用错误,分析卡里照旧让卡失败
This commit is contained in:
@@ -106,6 +106,14 @@ def need(raw: Mapping[str, Any], key: str, what: str) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
def reserved_check(column: str, reserved: Sequence[str], what: str) -> None:
|
||||
"""结果表的列 id 是输入的维度列(期次列)加上固定的几列:重名了列 id 就重复,引用结果表的格、下钻按的维度数都会认错。
|
||||
遇到这些名字(不分大小写)是调用错误,让 agent 在 SQL 里换个别名。"""
|
||||
if column.lower() in {r.lower() for r in reserved}:
|
||||
raise CallError("reserved_column", f"{what}「{column}」和结果表的列重名(结果表要用 {'、'.join(reserved)} 这几个名字):"
|
||||
f"在 SQL 里给它换个别名再分析,比如 AS {column}_name")
|
||||
|
||||
|
||||
def written_places(raw: Any) -> int:
|
||||
"""快照里一格写到第几位小数(headline 是取整过的,按它的书写精度容差):数字文本按字面,浮点按最短写法,整数为 0。是整数
|
||||
的浮点(SQLite 的 ROUND() 交回 2120.0)是取到个位的,也为 0——最短写法里的「.0」不是书写精度。"""
|
||||
@@ -152,4 +160,4 @@ def sign_word(value: Fraction | float | int) -> str | None:
|
||||
|
||||
|
||||
__all__ = ["DERIVED_PARAMS", "Env", "build", "closes", "column_label", "column_types", "label_for", "need", "refuse",
|
||||
"sign_word", "today_param", "written_places"]
|
||||
"reserved_check", "sign_word", "today_param", "written_places"]
|
||||
|
||||
@@ -26,8 +26,8 @@ from typing import Any
|
||||
|
||||
from app.analysis import comparable as cmp
|
||||
from app.analysis import phrases as ph
|
||||
from app.analysis.common import (Env, build, closes, column_label, column_types, label_for, need, refuse, sign_word,
|
||||
today_param)
|
||||
from app.analysis.common import (Env, build, closes, column_label, column_types, label_for, need, refuse,
|
||||
reserved_check, sign_word, today_param)
|
||||
from app.analysis.contract import CallError
|
||||
from app.analysis.inputs import Snap, as_list, hashable, same_value
|
||||
from app.analysis.numbers import canon, to_fraction
|
||||
@@ -81,9 +81,10 @@ def _key_strings(key: Sequence[Any]) -> list[str]:
|
||||
return [ph.key_text([k]) for k in key]
|
||||
|
||||
|
||||
def _sides(raw: Mapping[str, Any], env: Env, roles: Sequence[tuple[str, str]], *, by_needed: bool = True
|
||||
) -> tuple[dict[str, Any], Snap, Snap, dict[str, str], list[str]]:
|
||||
"""一份或两份快照:(规范化的参数, 基期快照, 本期快照, {角色: 列名}, 维度列)。roles 是 [(参数名, 角色的叫法)]。"""
|
||||
def _sides(raw: Mapping[str, Any], env: Env, roles: Sequence[tuple[str, str]], *, by_needed: bool = True,
|
||||
reserved: Sequence[str] = ()) -> tuple[dict[str, Any], Snap, Snap, dict[str, str], list[str]]:
|
||||
"""一份或两份快照:(规范化的参数, 基期快照, 本期快照, {角色: 列名}, 维度列)。roles 是 [(参数名, 角色的叫法)];reserved 是
|
||||
结果表固定的列名,维度列不能叫这些。"""
|
||||
params: dict[str, Any] = {}
|
||||
two = raw.get("base_query") is not None or raw.get("target_query") is not None
|
||||
if two:
|
||||
@@ -105,6 +106,8 @@ def _sides(raw: Mapping[str, Any], env: Env, roles: Sequence[tuple[str, str]], *
|
||||
if two:
|
||||
for c in by:
|
||||
base.column(c, "维度列")
|
||||
for c in by:
|
||||
reserved_check(c, reserved, "维度列")
|
||||
params["by"] = by
|
||||
cols: dict[str, str] = {}
|
||||
for key, what in roles:
|
||||
@@ -284,7 +287,7 @@ def _share_rank(segments: Sequence[Any], share: Any) -> list[Any]:
|
||||
|
||||
def additive(raw: Mapping[str, Any], env: Env) -> Any:
|
||||
used = list(USED["additive"])
|
||||
params, bs, ts, cols, by = _sides(raw, env, [("measure", "度量列")])
|
||||
params, bs, ts, cols, by = _sides(raw, env, [("measure", "度量列")], reserved=_FIXED_ADDITIVE)
|
||||
measure = cols["measure"]
|
||||
label = label_for(env, raw.get("label"), ts, measure)
|
||||
if raw.get("label") is not None:
|
||||
@@ -444,7 +447,8 @@ def _additive_result(env: Env, params: dict[str, Any], used: list[str], segs: li
|
||||
|
||||
def ratio(raw: Mapping[str, Any], env: Env) -> Any:
|
||||
used = list(USED["ratio"])
|
||||
params, bs, ts, cols, by = _sides(raw, env, [("numerator", "分子列"), ("denominator", "分母列")])
|
||||
params, bs, ts, cols, by = _sides(raw, env, [("numerator", "分子列"), ("denominator", "分母列")],
|
||||
reserved=_FIXED_RATIO)
|
||||
num, den = cols["numerator"], cols["denominator"]
|
||||
num_label, den_label = column_label(env, ts, num), column_label(env, ts, den)
|
||||
label = ph.pick_label(raw.get("label"), ph.say(num_label, "与", den_label, "之比"))
|
||||
|
||||
@@ -31,7 +31,8 @@ from app.analysis import comparable as cmp
|
||||
from app.analysis import periods as pd
|
||||
from app.analysis import phrases as ph
|
||||
from app.analysis import stats
|
||||
from app.analysis.common import Env, build, column_label, column_types, label_for, need, refuse, sign_word, today_param
|
||||
from app.analysis.common import (Env, build, column_label, column_types, label_for, need, refuse, reserved_check,
|
||||
sign_word, today_param)
|
||||
from app.analysis.contract import CallError
|
||||
from app.analysis.inputs import Snap, same_value
|
||||
from app.analysis.numbers import canon, to_fraction
|
||||
@@ -45,6 +46,8 @@ USED: dict[str, tuple[str, ...]] = {
|
||||
"baseline": ("k_warn", "k_alert", "min_history", "max_history"),
|
||||
"trend": ("trend_min_points", "trend_alpha", "max_history"),
|
||||
}
|
||||
#: 结果表在期次列之外固定的几列(期次列不能叫这些)
|
||||
FIXED: dict[str, tuple[str, ...]] = {"baseline": ("value", "change", "role"), "trend": ("value", "change")}
|
||||
|
||||
|
||||
def refusal_basis(op: str, settings: Mapping[str, Any]) -> tuple[list[str], str]:
|
||||
@@ -83,6 +86,7 @@ def _load(raw: Mapping[str, Any], env: Env, used: list[str]) -> Series | Any:
|
||||
"""读出一条序列;算不了时直接交回 refused 的结论。"""
|
||||
snap = env.inputs.snap(need(raw, "query", "查询编号"), "查询编号")
|
||||
period = snap.column(need(raw, "period", "期次列"), "期次列")
|
||||
reserved_check(period, FIXED[env.op], "期次列")
|
||||
measure = snap.column(need(raw, "measure", "度量列"), "度量列")
|
||||
grain = raw.get("grain")
|
||||
if grain is not None and grain not in (*pd.GRAINS, "seq"):
|
||||
@@ -369,4 +373,4 @@ def _tail_gap(series: Series, ordinal: int) -> str:
|
||||
return "再往前缺期"
|
||||
|
||||
|
||||
__all__ = ["MAD_SCALE", "USED", "baseline", "refusal_basis", "run_length", "trend", "unreadable_text"]
|
||||
__all__ = ["FIXED", "MAD_SCALE", "USED", "baseline", "refusal_basis", "run_length", "trend", "unreadable_text"]
|
||||
|
||||
@@ -9,6 +9,7 @@ from fractions import Fraction
|
||||
|
||||
import pytest
|
||||
|
||||
from app.analysis import DATA_ERRORS
|
||||
from app.analysis.decompose import lmdi, ratio_effects
|
||||
from tests.analysis_samples import analyze, call_error, fixed_prober, notes, snap
|
||||
|
||||
@@ -487,3 +488,19 @@ async def test_a_label_cannot_carry_a_judgment_into_the_phrase():
|
||||
# 消过一遍会变的叫法:复算拿记下的叫法再消,判定语逐字相同(analyze 核对复算逐字段相同)
|
||||
marked = await additive(label="销售额[ [v:Q1.r0.gmv] ]")
|
||||
assert marked["params"]["label"] == "销售额" and marked["phrase"].startswith("销售额下降 194")
|
||||
|
||||
|
||||
async def test_a_dimension_named_like_a_result_column_is_a_call_error():
|
||||
rows = [["线上", "2026-W41", 820], ["线上", "2026-W42", 700], ["线下", "2026-W41", 600], ["线下", "2026-W42", 560]]
|
||||
flagged = snap("Q2", "SELECT channel AS flag, week, SUM(amount) AS gmv FROM orders GROUP BY 1, 2",
|
||||
["flag", "week", "gmv"], rows)
|
||||
error = await call_error("decompose", "additive", snaps=[flagged], query="Q2", by=["flag"], period="week",
|
||||
base="2026-W41", target="2026-W42", measure="gmv")
|
||||
assert error.code == "reserved_column" and "在 SQL 里给它换个别名" in error.message and "AS flag_name" in error.message
|
||||
assert error.code not in DATA_ERRORS # 结构性的:分析卡里照旧让卡失败
|
||||
ratio_rows = [["线上", "W41", 50, 1000], ["线上", "W42", 80, 1000], ["线下", "W41", 20, 1000], ["线下", "W42", 26, 1000]]
|
||||
mixed = snap("Q1", "SELECT channel AS Mix, week, SUM(refund_flag) AS refunds, COUNT(*) AS orders FROM orders "
|
||||
"GROUP BY 1, 2", ["Mix", "week", "refunds", "orders"], ratio_rows)
|
||||
error = await call_error("decompose", "ratio", snaps=[mixed], query="Q1", by="Mix", period="week", base="W41",
|
||||
target="W42", numerator="refunds", denominator="orders")
|
||||
assert error.code == "reserved_column" and "「Mix」" in error.message
|
||||
|
||||
@@ -319,3 +319,11 @@ async def test_a_falling_trend_whose_last_weeks_rise_says_but():
|
||||
falling = await trend(weeks([200, 196, 190, 188, 181, 176, 170, 165, 160, 162, 165, 168]))
|
||||
assert falling["verdict"]["code"] == "falling" and falling["values"]["run_direction"] == "up"
|
||||
assert falling["phrase"].endswith(",但最近已连续 3 周上升")
|
||||
|
||||
|
||||
async def test_a_period_column_named_like_a_result_column_is_a_call_error():
|
||||
rows = [[r[0], r[1]] for r in weeks(NOISE + [101])]
|
||||
named = snap("Q1", "SELECT DATE(order_date, 'weekday 0', '-6 days') AS value, COUNT(*) AS orders FROM orders "
|
||||
"GROUP BY 1", ["value", "orders"], rows)
|
||||
error = await call_error("baseline", snaps=[named], query="Q1", period="value", measure="orders")
|
||||
assert error.code == "reserved_column" and "期次列「value」和结果表的列重名" in error.message
|
||||
|
||||
Reference in New Issue
Block a user