asa / scripts /lib /chat_eval /tuning.py
defenestrate's picture
ASA Space: squashed history (instructions and settings live in the private pack)
526950e
Raw History Blame Contribute Delete
7.75 kB
"""Retrieval-tuning step logic and named constants.
Pure-function module: takes analytics outputs (doc_utilization, score_summary)
and a tuning state dict, returns the next state. Does no I/O.
See docs/superpowers/specs/2026-05-08-retrieval-tuning-loop-design.html.
"""
from __future__ import annotations
import copy
from datetime import datetime, timezone
#: Maximum cumulative penalty per doc. 0.40 = 40% score haircut. Keeps a
#: stuck doc retrievable when nothing else is in the room.
MAX_PENALTY = 0.40
#: Per-step penalty increment. With once-daily cadence, full saturation
#: takes >= 8 consecutive steps on the same doc.
PENALTY_DELTA = 0.05
#: A doc is eligible for a penalty increment iff its util_pct (over the
#: window) is below this threshold. Same 0-100 scale as
#: analytics.doc_utilization()'s util_pct field.
UTIL_PENALTY_THRESHOLD = 30.0
#: Minimum retrievals in the window before a doc is eligible for action.
#: Cuts noise from one-off retrievals.
MIN_RETRIEVALS_FOR_ACTION = 10
#: Per-dimension regression floor on a 1-5 scale. If any rubric dimension's
#: post-step mean drops by >= this value vs. pre-step, freeze + rollback.
REGRESSION_THRESHOLD = 0.30
#: Minimum judged turns in the post-step window before regression check
#: is meaningful. Below this, skip the check (and skip proposing a step).
MIN_TURNS_FOR_REGRESSION_CHECK = 20
#: Tuning state schema version. Bump on incompatible schema change.
TUNING_STATE_VERSION = 1
#: Penalty value at or above which a doc is excluded from kind-quota fill.
#: Below the floor, penalties only adjust within-kind ordering (already true).
#: 0.30 is six consecutive penalty steps, signalling the tuner is sure.
QUOTA_INELIGIBILITY_FLOOR = 0.30
def empty_state() -> dict:
"""Default tuning state: no penalties, not frozen."""
return {
"version": TUNING_STATE_VERSION,
"updated_at": None,
"frozen": False,
"frozen_reason": None,
"step_count": 0,
"penalties": {},
}
def now_utc_iso() -> str:
"""ISO-8601 UTC timestamp with second precision: '2026-05-08T18:45:03Z'."""
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def propose_step(
util: dict[str, dict],
*,
current_state: dict,
) -> dict | None:
"""Propose at most one penalty increment for the lowest-util eligible doc.
`util`: output of analytics.doc_utilization() filtered to the working
window. Each value has retrieved_count, used_count, util_pct.
`current_state`: the live retrieval_tuning state (read-only here).
Caller (cli._cmd_tune) is responsible for short-circuiting on
current_state["frozen"] before invoking this function — the
frozen check is not performed here.
Returns a step_action dict matching the audit-log shape, or None when
no doc is eligible (all above threshold, or all already at MAX_PENALTY,
or below MIN_RETRIEVALS_FOR_ACTION).
"""
pens = current_state.get("penalties", {})
eligible: list[tuple[str, dict]] = []
for doc_id, info in util.items():
if info["retrieved_count"] < MIN_RETRIEVALS_FOR_ACTION:
continue
if info["util_pct"] >= UTIL_PENALTY_THRESHOLD:
continue
if pens.get(doc_id, 0.0) >= MAX_PENALTY:
continue
eligible.append((doc_id, info))
if not eligible:
return None
# Lowest util_pct first; ties broken by highest retrieved_count.
eligible.sort(key=lambda kv: (kv[1]["util_pct"], -kv[1]["retrieved_count"]))
doc_id, info = eligible[0]
before = float(pens.get(doc_id, 0.0))
after = min(before + PENALTY_DELTA, MAX_PENALTY)
return {
"kind": "penalty_increment",
"doc_id": doc_id,
"delta": round(after - before, 4),
"penalty_before": round(before, 4),
"penalty_after": round(after, 4),
"reason": (
f"util_pct={info['util_pct']} over {info['retrieved_count']} "
f"retrievals"
),
}
def check_regression(
scores_before: dict[str, float],
scores_after: dict[str, float],
*,
judged_turns: int,
before_stats: dict[str, dict] | None = None,
after_stats: dict[str, dict] | None = None,
) -> dict:
"""Pure check: did any dimension drop by >= REGRESSION_THRESHOLD?
Returns a dict matching the audit log's `regression_check` shape.
Sets `skipped=True` (and `any_dimension_regressed=False`) when there
aren't enough turns to evaluate.
When `before_stats` and `after_stats` are provided (each shaped as
{dim: {mean, std, n}} per analytics.score_summary_with_stats), the
return value also includes a confidence block with sample sizes and a
Welch's t on the max-drop dimension. Telemetry only — does not gate
the freeze decision.
"""
if judged_turns < MIN_TURNS_FOR_REGRESSION_CHECK:
return {
"any_dimension_regressed": False,
"max_drop_dimension": None,
"max_drop_value": 0.0,
"threshold": REGRESSION_THRESHOLD,
"skipped": True,
}
shared = set(scores_before.keys()) & set(scores_after.keys())
drops = {
d: scores_before[d] - scores_after[d]
for d in shared
}
if not drops:
return {
"any_dimension_regressed": False,
"max_drop_dimension": None,
"max_drop_value": 0.0,
"threshold": REGRESSION_THRESHOLD,
"skipped": False,
}
max_dim, max_drop = max(drops.items(), key=lambda kv: kv[1])
out = {
"any_dimension_regressed": max_drop >= REGRESSION_THRESHOLD,
"max_drop_dimension": max_dim,
"max_drop_value": round(max_drop, 4),
"threshold": REGRESSION_THRESHOLD,
"skipped": False,
}
if (
before_stats and after_stats
and max_dim in before_stats and max_dim in after_stats
):
b = before_stats[max_dim]
a = after_stats[max_dim]
n_b, n_a = b["n"], a["n"]
s_b, s_a = b["std"], a["std"]
# Welch's t and pooled std (descriptive). Guard against n<2 / std=0.
denom_sq = (
(s_b * s_b / n_b if n_b else 0.0)
+ (s_a * s_a / n_a if n_a else 0.0)
)
denom = denom_sq ** 0.5 if denom_sq > 0 else 0.0
t_stat = round(max_drop / denom, 4) if denom > 0 else None
std_pooled = round(((s_b * s_b + s_a * s_a) / 2) ** 0.5, 4)
out["confidence"] = {
"n_before": n_b,
"n_after": n_a,
"max_drop_t_stat": t_stat,
"max_drop_std_pooled": std_pooled,
"interpretation": "informational_only",
}
return out
def apply_step(state: dict, step_action: dict) -> dict:
"""Return a new state with the step applied. Does not mutate `state`."""
new = copy.deepcopy(state)
if step_action["kind"] == "penalty_increment":
new["penalties"][step_action["doc_id"]] = round(
float(step_action["penalty_after"]), 4
)
new["step_count"] = int(state.get("step_count", 0)) + 1
new["updated_at"] = now_utc_iso()
return new
def rollback_step(state: dict, rolled_back_step: dict, *, reason: str) -> dict:
"""Restore the prior penalty value and set frozen=True."""
new = copy.deepcopy(state)
sa = rolled_back_step["step_action"]
doc_id = sa["doc_id"]
before = float(sa["penalty_before"])
if before > 0.0:
new["penalties"][doc_id] = round(before, 4)
else:
new["penalties"].pop(doc_id, None)
new["frozen"] = True
new["frozen_reason"] = reason
new["updated_at"] = now_utc_iso()
# step_count stays as-is — audit log is monotonic.
return new