Files
projectAIpopular/gateway/sense/store.py
T
tzt 1aaa05cacc feat(sense): T-X5 云端评判晋升表×conformal 双闸门——本地档自动接管(采纳 cortiq promotion)
- gateway/sense/promotion.py:PromotionTable 状态机(candidate/promoted/demoted)
  * 晋升门:n_total>=n_min 且 通过率>=promote_lb 且 soak 浸泡期满足
  * 退化:promoted 期间通过率<demote_lb 自动降级;降级后可凭数据恢复
  * 一票否决:tier=T3 观察不进通过率统计,已晋升者立即降级
  * 持久化 sense_promotion 表(CREATE IF NOT EXISTS 幂等)
- grader:live 模式对已晋升标签的 T2 决策(无 T3 信号/非 fallback)升级 T1,
  hard_gates.promotion_applied 如实标注;T1/T3/fallback 路径不受影响
- 回填链路:T1 审计抽样带 promo:<label> 标签 -> 人工 verdict approve=ok
  经 /review/{id} 提交时回填晋升表(失败不影响审核主流程)
- 双闸门语义:conformal 阈值保证单条决策风险率,晋升表保证标签级接管节奏;
  collect 数据不足(n_min 未满)不晋升,全部迁移可经 T-X4 留痕审计

pytest 474 passed(T-X4 后 466 + 8)
2026-09-18 23:01:52 +08:00

270 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Sense 存储(T-G0DDL + 基础查询;labeler/grader 的查询在后续任务落地)。
§4 数据模型:tier_observations(观察-标签闭环主表)+ sense_artifacts(工件登记)。
WAL;全部访问经全局锁 + 每操作新连接(ReviewQueue 模式),异步调用方 to_thread。
隐私(D-G6):不落 query 原文(只存哈希+特征+int8 embedding BLOB);留存 180 天。
"""
from __future__ import annotations
import sqlite3
import json
import threading
import time
from pathlib import Path
from typing import Any, Dict, List, Optional
_SCHEMA = """
CREATE TABLE IF NOT EXISTS tier_observations(
id INTEGER PRIMARY KEY, ts INTEGER NOT NULL,
request_id TEXT NOT NULL, consumer TEXT NOT NULL,
bucket TEXT NOT NULL DEFAULT 'default', domain TEXT DEFAULT '',
decided_tier TEXT NOT NULL, executed_tier TEXT NOT NULL,
probs TEXT NOT NULL,
policy_version TEXT NOT NULL, features TEXT NOT NULL,
embedding BLOB,
outcome TEXT DEFAULT '', true_tier TEXT DEFAULT '', human_override TEXT DEFAULT '');
CREATE INDEX IF NOT EXISTS idx_obs_ts ON tier_observations(ts);
CREATE INDEX IF NOT EXISTS idx_obs_policy ON tier_observations(policy_version);
CREATE TABLE IF NOT EXISTS sense_artifacts(
version TEXT PRIMARY KEY, kind TEXT NOT NULL,
path TEXT NOT NULL, metrics TEXT NOT NULL, created_ts INTEGER NOT NULL,
active INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS sense_decisions(
id INTEGER PRIMARY KEY, ts INTEGER NOT NULL,
request_id TEXT NOT NULL, consumer TEXT NOT NULL,
decided_tier TEXT NOT NULL, executed_tier TEXT NOT NULL DEFAULT '',
mode TEXT NOT NULL DEFAULT 'collect', fallback INTEGER NOT NULL DEFAULT 0,
policy_version TEXT DEFAULT '', q_hash TEXT DEFAULT '',
reasons TEXT NOT NULL DEFAULT '[]',
candidate_scores TEXT NOT NULL DEFAULT '{}',
rejected TEXT NOT NULL DEFAULT '[]');
CREATE INDEX IF NOT EXISTS idx_dec_ts ON sense_decisions(ts);
CREATE TABLE IF NOT EXISTS sense_promotion(
label TEXT PRIMARY KEY, n_total INTEGER NOT NULL DEFAULT 0,
n_ok INTEGER NOT NULL DEFAULT 0,
state TEXT NOT NULL DEFAULT 'candidate',
first_ts INTEGER NOT NULL DEFAULT 0,
promoted_ts INTEGER NOT NULL DEFAULT 0,
last_ts INTEGER NOT NULL DEFAULT 0);
"""
class SenseStore:
"""sense.sqlite3 访问(同步实现;异步调用方 to_thread)。"""
def __init__(self, db_path: str | Path):
self.db_path = Path(db_path)
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._lock = threading.Lock()
self._init_db()
@classmethod
def init_db(cls, db_path: str | Path) -> "SenseStore":
return cls(db_path)
def _connect(self) -> sqlite3.Connection:
conn = sqlite3.connect(self.db_path, check_same_thread=False)
conn.row_factory = sqlite3.Row
return conn
def _init_db(self) -> None:
with self._lock, self._connect() as conn:
conn.executescript(_SCHEMA)
# ---------- 观察 ----------
def insert_observation(self, obs: Dict[str, Any]) -> None:
"""写入一条观察(T-G2 批量缓冲的落盘终点)。"""
with self._lock, self._connect() as conn:
conn.execute(
"""INSERT OR IGNORE INTO tier_observations
(ts, request_id, consumer, bucket, domain, decided_tier,
executed_tier, probs, policy_version, features, embedding,
outcome, true_tier, human_override)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(int(obs.get("ts", time.time())), obs["request_id"],
obs["consumer"], obs.get("bucket", "default"),
obs.get("domain", ""), obs["decided_tier"], obs["executed_tier"],
obs.get("probs", "{}"), obs.get("policy_version", ""),
obs.get("features", "{}"), obs.get("embedding"),
obs.get("outcome", ""), obs.get("true_tier", ""),
obs.get("human_override", "")))
def update_outcome(self, request_id: str, outcome: str,
executed_tier: Optional[str] = None) -> bool:
"""升级阶梯回写(outcome=escalated 等)。"""
with self._lock, self._connect() as conn:
if executed_tier:
cur = conn.execute(
"UPDATE tier_observations SET outcome = ?, executed_tier = ?"
" WHERE request_id = ?", (outcome, executed_tier, request_id))
else:
cur = conn.execute(
"UPDATE tier_observations SET outcome = ? WHERE request_id = ?",
(outcome, request_id))
return cur.rowcount > 0
def set_true_tier(self, request_id: str, true_tier: str) -> bool:
"""labeler 回填(T-G3)。"""
with self._lock, self._connect() as conn:
cur = conn.execute(
"UPDATE tier_observations SET true_tier = ? WHERE request_id = ?",
(true_tier, request_id))
return cur.rowcount > 0
def labeled_rows(self, policy_version: str = "", limit: int = 100000) -> List[Dict[str, Any]]:
"""true_tier 非空的行(训练/校准输入)。"""
with self._lock, self._connect() as conn:
if policy_version:
rows = conn.execute(
"SELECT * FROM tier_observations WHERE true_tier != ''"
" AND policy_version = ? ORDER BY ts LIMIT ?",
(policy_version, limit)).fetchall()
else:
rows = conn.execute(
"SELECT * FROM tier_observations WHERE true_tier != ''"
" ORDER BY ts LIMIT ?", (limit,)).fetchall()
return [dict(r) for r in rows]
def count_labeled(self) -> int:
"""已标签条数(min_labels 晋升门检查)。"""
with self._lock, self._connect() as conn:
row = conn.execute(
"SELECT COUNT(*) AS c FROM tier_observations"
" WHERE true_tier != ''").fetchone()
return int(row["c"])
def all_observations(self, limit: int = 200000) -> List[Dict[str, Any]]:
"""全量遍历(labeler 夜间推导输入;量大时可分页)。"""
with self._lock, self._connect() as conn:
rows = conn.execute(
"SELECT * FROM tier_observations ORDER BY id LIMIT ?",
(limit,)).fetchall()
return [dict(r) for r in rows]
def purge_older_than(self, ts: float) -> int:
"""留存清理(D-G6:默认 180 天,夜间任务顺带)。"""
with self._lock, self._connect() as conn:
cur = conn.execute(
"DELETE FROM tier_observations WHERE ts < ?", (int(ts),))
return cur.rowcount
# ---------- 决策留痕(T-X4:可解释路由三元结构) ----------
def insert_decision(self, rec: Dict[str, Any]) -> None:
"""写入一条路由决策记录(reasons/candidate_scores/rejected 为 JSON 串)。"""
with self._lock, self._connect() as conn:
conn.execute(
"""INSERT INTO sense_decisions
(ts, request_id, consumer, decided_tier, executed_tier, mode,
fallback, policy_version, q_hash, reasons, candidate_scores,
rejected)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
(int(rec.get("ts", time.time())), rec["request_id"],
rec["consumer"], rec["decided_tier"],
rec.get("executed_tier", ""), rec.get("mode", "collect"),
1 if rec.get("fallback") else 0,
rec.get("policy_version", ""), rec.get("q_hash", ""),
rec.get("reasons", "[]"), rec.get("candidate_scores", "{}"),
rec.get("rejected", "[]")))
def list_decisions(self, limit: int = 50) -> List[Dict[str, Any]]:
"""最近 N 条决策(新在前;JSON 字段反序列化为对象)。"""
with self._lock, self._connect() as conn:
rows = conn.execute(
"SELECT * FROM sense_decisions ORDER BY id DESC LIMIT ?",
(max(1, int(limit)),)).fetchall()
out: List[Dict[str, Any]] = []
for r in rows:
d = dict(r)
for f in ("reasons", "candidate_scores", "rejected"):
try:
d[f] = json.loads(d.get(f) or ("{}" if f == "candidate_scores" else "[]"))
except json.JSONDecodeError:
pass
out.append(d)
return out
# ---------- 晋升表(T-X5:本地档自动接管的双闸门之一) ----------
def upsert_promotion(self, row: Dict[str, Any]) -> None:
with self._lock, self._connect() as conn:
conn.execute(
"""INSERT OR REPLACE INTO sense_promotion
(label, n_total, n_ok, state, first_ts, promoted_ts, last_ts)
VALUES (?,?,?,?,?,?,?)""",
(row["label"], int(row.get("n_total", 0)),
int(row.get("n_ok", 0)), str(row.get("state", "candidate")),
int(row.get("first_ts", 0)), int(row.get("promoted_ts", 0)),
int(row.get("last_ts", 0))))
def get_promotion(self, label: str) -> Optional[Dict[str, Any]]:
with self._lock, self._connect() as conn:
row = conn.execute(
"SELECT * FROM sense_promotion WHERE label = ?", (label,)).fetchone()
return dict(row) if row else None
def list_promotions(self) -> List[Dict[str, Any]]:
with self._lock, self._connect() as conn:
rows = conn.execute(
"SELECT * FROM sense_promotion ORDER BY last_ts DESC").fetchall()
return [dict(r) for r in rows]
# ---------- 工件 ----------
def register_artifact(self, version: str, kind: str, path: str,
metrics: Dict[str, Any], active: bool = False,
created_ts: Optional[int] = None) -> None:
"""登记模型/阈值工件(默认 active=0,人工 promote 切换)。"""
with self._lock, self._connect() as conn:
conn.execute(
"INSERT OR REPLACE INTO sense_artifacts"
"(version, kind, path, metrics, created_ts, active)"
" VALUES (?,?,?,?,?,?)",
(version, kind, path, json_dumps(metrics),
int(created_ts if created_ts is not None else time.time()),
1 if active else 0))
def activate_artifact(self, version: str, kind: str) -> bool:
"""切换 active(同 kind 互斥)。"""
with self._lock, self._connect() as conn:
conn.execute("UPDATE sense_artifacts SET active = 0 WHERE kind = ?",
(kind,))
cur = conn.execute(
"UPDATE sense_artifacts SET active = 1 WHERE version = ? AND kind = ?",
(version, kind))
return cur.rowcount > 0
def active_artifact(self, kind: str) -> Optional[Dict[str, Any]]:
with self._lock, self._connect() as conn:
row = conn.execute(
"SELECT * FROM sense_artifacts WHERE kind = ? AND active = 1"
" ORDER BY created_ts DESC LIMIT 1", (kind,)).fetchone()
return dict(row) if row else None
def list_artifacts(self, kind: str) -> List[Dict[str, Any]]:
"""同 kind 工件按时间倒序(load_active 的 last-good 扫描用)。"""
with self._lock, self._connect() as conn:
rows = conn.execute(
"SELECT * FROM sense_artifacts WHERE kind = ?"
" ORDER BY created_ts DESC", (kind,)).fetchall()
return [dict(r) for r in rows]
# ---------- 自省 ----------
def table_names(self) -> List[str]:
with self._lock, self._connect() as conn:
rows = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table'").fetchall()
return [r["name"] for r in rows]
def index_names(self) -> List[str]:
with self._lock, self._connect() as conn:
rows = conn.execute(
"SELECT name FROM sqlite_master WHERE type='index'"
" AND name LIKE 'idx_%'").fetchall()
return [r["name"] for r in rows]
def json_dumps(obj: Any) -> str:
import json
return json.dumps(obj, ensure_ascii=False)