feat: add baseline diagnosis signal store
This commit is contained in:
@@ -0,0 +1,154 @@
|
|||||||
|
"""SqliteDiagnosisSignalStore:baseline 逐题诊断信号的 SQLite 持久化适配器。
|
||||||
|
|
||||||
|
实现 core/evolution/protocols.py::DiagnosisSignalStore 端口。信号行以
|
||||||
|
(question_id, baseline_run_id, diag_fingerprint) 为主键,INSERT OR REPLACE
|
||||||
|
保证逐题幂等 upsert;bool 字段以 0/1 存储,可空字段以 NULL 存储。表建在
|
||||||
|
harness.db,供离线诊断编排写入、视频级切分选择器读取。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from core.evolution.types import DiagnosisSignalRow
|
||||||
|
|
||||||
|
# 表列顺序即 DiagnosisSignalRow 字段顺序(question_id..session_id),
|
||||||
|
# upsert 写入与 load 还原共用,避免手写列名两处漂移。
|
||||||
|
_COLUMNS: tuple[str, ...] = (
|
||||||
|
"question_id",
|
||||||
|
"video_id",
|
||||||
|
"baseline_run_id",
|
||||||
|
"diag_fingerprint",
|
||||||
|
"task_type",
|
||||||
|
"error_type",
|
||||||
|
"cause_category",
|
||||||
|
"tier",
|
||||||
|
"evolution_target",
|
||||||
|
"degraded",
|
||||||
|
"infra",
|
||||||
|
"session_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SqliteDiagnosisSignalStore:
|
||||||
|
"""逐题诊断信号的 SQLite 存储实现。
|
||||||
|
|
||||||
|
构造时按需建表(CREATE TABLE IF NOT EXISTS),主键
|
||||||
|
(question_id, baseline_run_id, diag_fingerprint) 保证同键覆盖。
|
||||||
|
每次写操作单事务 commit,保证原子落盘与断点续跑。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
db_path: SQLite 数据库文件路径(通常为 harness.db)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, db_path: str) -> None:
|
||||||
|
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._conn = sqlite3.connect(db_path)
|
||||||
|
self._conn.row_factory = sqlite3.Row
|
||||||
|
self._init_table()
|
||||||
|
|
||||||
|
def _init_table(self) -> None:
|
||||||
|
"""创建 baseline_diagnosis 表(若不存在)。"""
|
||||||
|
self._conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS baseline_diagnosis (
|
||||||
|
question_id TEXT NOT NULL,
|
||||||
|
video_id TEXT NOT NULL,
|
||||||
|
baseline_run_id TEXT NOT NULL,
|
||||||
|
diag_fingerprint TEXT NOT NULL,
|
||||||
|
task_type TEXT NOT NULL,
|
||||||
|
error_type TEXT,
|
||||||
|
cause_category TEXT,
|
||||||
|
tier TEXT NOT NULL,
|
||||||
|
evolution_target TEXT,
|
||||||
|
degraded INTEGER NOT NULL,
|
||||||
|
infra INTEGER NOT NULL,
|
||||||
|
session_id TEXT,
|
||||||
|
PRIMARY KEY (question_id, baseline_run_id, diag_fingerprint)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self._conn.commit()
|
||||||
|
|
||||||
|
def upsert(self, row: DiagnosisSignalRow) -> None:
|
||||||
|
"""写入或覆盖单题诊断信号(按主键幂等,单事务)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
row: 待持久化的诊断信号行。bool 字段转 0/1,None 存 NULL。
|
||||||
|
|
||||||
|
关键实现:
|
||||||
|
用 INSERT OR REPLACE 按主键覆盖,避免重复行;commit 保证原子。
|
||||||
|
"""
|
||||||
|
placeholders = ", ".join("?" for _ in _COLUMNS)
|
||||||
|
col_names = ", ".join(_COLUMNS)
|
||||||
|
values = (
|
||||||
|
row.question_id,
|
||||||
|
row.video_id,
|
||||||
|
row.baseline_run_id,
|
||||||
|
row.diag_fingerprint,
|
||||||
|
row.task_type,
|
||||||
|
row.error_type,
|
||||||
|
row.cause_category,
|
||||||
|
row.tier,
|
||||||
|
row.evolution_target,
|
||||||
|
int(row.degraded),
|
||||||
|
int(row.infra),
|
||||||
|
row.session_id,
|
||||||
|
)
|
||||||
|
self._conn.execute(
|
||||||
|
f"INSERT OR REPLACE INTO baseline_diagnosis ({col_names}) VALUES ({placeholders})",
|
||||||
|
values,
|
||||||
|
)
|
||||||
|
self._conn.commit()
|
||||||
|
|
||||||
|
def done_question_ids(self, baseline_run_id: str, diag_fingerprint: str) -> set[str]:
|
||||||
|
"""查询指定 run 与诊断指纹下已完成的 question_id 集合。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
baseline_run_id: baseline run 标识。
|
||||||
|
diag_fingerprint: 诊断口径指纹。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
已落盘信号的 question_id 集合;无匹配时为空集,供断点续跑跳过。
|
||||||
|
"""
|
||||||
|
cursor = self._conn.execute(
|
||||||
|
"SELECT DISTINCT question_id FROM baseline_diagnosis"
|
||||||
|
" WHERE baseline_run_id = ? AND diag_fingerprint = ?",
|
||||||
|
(baseline_run_id, diag_fingerprint),
|
||||||
|
)
|
||||||
|
return {r["question_id"] for r in cursor.fetchall()}
|
||||||
|
|
||||||
|
def load(self, baseline_run_id: str, diag_fingerprint: str) -> list[DiagnosisSignalRow]:
|
||||||
|
"""加载指定 run 与诊断指纹下的全部诊断信号行。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
baseline_run_id: baseline run 标识。
|
||||||
|
diag_fingerprint: 诊断口径指纹。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
还原后的 DiagnosisSignalRow 列表(0/1→bool,NULL→None)。
|
||||||
|
"""
|
||||||
|
col_names = ", ".join(_COLUMNS)
|
||||||
|
cursor = self._conn.execute(
|
||||||
|
f"SELECT {col_names} FROM baseline_diagnosis"
|
||||||
|
" WHERE baseline_run_id = ? AND diag_fingerprint = ?",
|
||||||
|
(baseline_run_id, diag_fingerprint),
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
DiagnosisSignalRow(
|
||||||
|
question_id=r["question_id"],
|
||||||
|
video_id=r["video_id"],
|
||||||
|
baseline_run_id=r["baseline_run_id"],
|
||||||
|
diag_fingerprint=r["diag_fingerprint"],
|
||||||
|
task_type=r["task_type"],
|
||||||
|
error_type=r["error_type"],
|
||||||
|
cause_category=r["cause_category"],
|
||||||
|
tier=r["tier"],
|
||||||
|
evolution_target=r["evolution_target"],
|
||||||
|
degraded=bool(r["degraded"]),
|
||||||
|
infra=bool(r["infra"]),
|
||||||
|
session_id=r["session_id"],
|
||||||
|
)
|
||||||
|
for r in cursor.fetchall()
|
||||||
|
]
|
||||||
@@ -7,7 +7,10 @@ SkillStore / PromptStore 为同步(文件读取量小且快),RunLog 为异
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Protocol, runtime_checkable
|
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from core.evolution.types import DiagnosisSignalRow
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
@@ -104,3 +107,44 @@ class RunLog(Protocol):
|
|||||||
轨迹记录字典列表。
|
轨迹记录字典列表。
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class DiagnosisSignalStore(Protocol):
|
||||||
|
"""逐题诊断信号存储端口。
|
||||||
|
|
||||||
|
隔离 SQLite 实现细节,app/core 不写裸 SQL。逐题 upsert 落盘、
|
||||||
|
支持断点续跑(done_question_ids 查已完成集合)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def upsert(self, row: DiagnosisSignalRow) -> None:
|
||||||
|
"""写入或覆盖单题诊断信号(按主键幂等)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
row: 待持久化的诊断信号行。
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
def done_question_ids(self, baseline_run_id: str, diag_fingerprint: str) -> set[str]:
|
||||||
|
"""查询指定 run 与诊断指纹下已完成的 question_id 集合。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
baseline_run_id: baseline run 标识。
|
||||||
|
diag_fingerprint: 诊断口径指纹。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
已落盘信号的 question_id 集合,用于断点续跑跳过。
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
def load(self, baseline_run_id: str, diag_fingerprint: str) -> list[DiagnosisSignalRow]:
|
||||||
|
"""加载指定 run 与诊断指纹下的全部诊断信号行。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
baseline_run_id: baseline run 标识。
|
||||||
|
diag_fingerprint: 诊断口径指纹。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
还原后的 DiagnosisSignalRow 列表。
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|||||||
@@ -300,6 +300,44 @@ class DiagnosisResult:
|
|||||||
degraded_question_ids: list[str] = field(default_factory=list)
|
degraded_question_ids: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DiagnosisSignalRow:
|
||||||
|
"""单题诊断信号行,即 baseline run 逐题诊断的持久化单元。
|
||||||
|
|
||||||
|
供后续视频级切分选择器消费;由 (question_id, baseline_run_id,
|
||||||
|
diag_fingerprint) 唯一确定,逐题 upsert 支持断点续跑。
|
||||||
|
|
||||||
|
属性:
|
||||||
|
question_id: 题目唯一标识。
|
||||||
|
video_id: 对应视频唯一标识。
|
||||||
|
baseline_run_id: 产出该信号的 baseline run 标识。
|
||||||
|
diag_fingerprint: 诊断口径指纹,隔离不同诊断配置的信号。
|
||||||
|
task_type: 题目任务类型。
|
||||||
|
error_type: 错误类别(extraction/search/reasoning/mixed);
|
||||||
|
T0/uncertain 行为 None。
|
||||||
|
cause_category: 病因类别(defect/lapse);不适用为 None。
|
||||||
|
tier: 诊断分层(T0/T1/T2/uncertain)。
|
||||||
|
evolution_target: 进化目标(tool/skill/system);
|
||||||
|
error_type 为 None 时亦为 None。
|
||||||
|
degraded: 是否为降级信号(judge 解析失败时生成)。
|
||||||
|
infra: 是否为 INFRA 护栏排除行。
|
||||||
|
session_id: 关联的会话标识;不适用为 None。
|
||||||
|
"""
|
||||||
|
|
||||||
|
question_id: str
|
||||||
|
video_id: str
|
||||||
|
baseline_run_id: str
|
||||||
|
diag_fingerprint: str
|
||||||
|
task_type: str
|
||||||
|
error_type: str | None
|
||||||
|
cause_category: str | None
|
||||||
|
tier: str
|
||||||
|
evolution_target: str | None
|
||||||
|
degraded: bool
|
||||||
|
infra: bool
|
||||||
|
session_id: str | None
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# 3. 进化类型
|
# 3. 进化类型
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""SqliteDiagnosisSignalStore 单元测试。
|
||||||
|
|
||||||
|
验证逐题诊断信号的 upsert 幂等、断点续跑(done_question_ids)、
|
||||||
|
load 往返还原,以及 INFRA 行的可空字段处理。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from adapters.baseline_diagnosis_store import DiagnosisSignalRow, SqliteDiagnosisSignalStore
|
||||||
|
|
||||||
|
|
||||||
|
def _store(tmp_path):
|
||||||
|
"""构造指向临时 harness.db 的信号存储。"""
|
||||||
|
return SqliteDiagnosisSignalStore(str(tmp_path / "h.db"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_upsert_and_get_done_ids(tmp_path):
|
||||||
|
s = _store(tmp_path)
|
||||||
|
row = DiagnosisSignalRow(
|
||||||
|
question_id="q1",
|
||||||
|
video_id="v1",
|
||||||
|
baseline_run_id="r",
|
||||||
|
diag_fingerprint="fp",
|
||||||
|
task_type="Counting",
|
||||||
|
error_type="search_failure",
|
||||||
|
cause_category="defect",
|
||||||
|
tier="T2",
|
||||||
|
evolution_target="skill",
|
||||||
|
degraded=False,
|
||||||
|
infra=False,
|
||||||
|
session_id="s",
|
||||||
|
)
|
||||||
|
s.upsert(row)
|
||||||
|
assert s.done_question_ids("r", "fp") == {"q1"}
|
||||||
|
s.upsert(row) # 同键覆盖,不重复
|
||||||
|
assert s.done_question_ids("r", "fp") == {"q1"}
|
||||||
|
assert s.done_question_ids("r", "other_fp") == set() # 不同 fingerprint 隔离
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_rows_roundtrip(tmp_path):
|
||||||
|
s = _store(tmp_path)
|
||||||
|
s.upsert(
|
||||||
|
DiagnosisSignalRow(
|
||||||
|
"q2",
|
||||||
|
"v2",
|
||||||
|
"r",
|
||||||
|
"fp",
|
||||||
|
"OCR Problems",
|
||||||
|
"extraction_failure",
|
||||||
|
"lapse",
|
||||||
|
"T1",
|
||||||
|
"tool",
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
"s",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rows = s.load("r", "fp")
|
||||||
|
assert len(rows) == 1 and rows[0].tier == "T1" and rows[0].evolution_target == "tool"
|
||||||
|
|
||||||
|
|
||||||
|
def test_null_fields_for_infra_row(tmp_path):
|
||||||
|
s = _store(tmp_path)
|
||||||
|
# T0 INFRA 行:error_type/cause_category/evolution_target 为 None
|
||||||
|
s.upsert(
|
||||||
|
DiagnosisSignalRow(
|
||||||
|
"q3",
|
||||||
|
"v3",
|
||||||
|
"r",
|
||||||
|
"fp",
|
||||||
|
"Counting",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"T0",
|
||||||
|
None,
|
||||||
|
degraded=False,
|
||||||
|
infra=True,
|
||||||
|
session_id=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rows = s.load("r", "fp")
|
||||||
|
assert rows[0].error_type is None and rows[0].evolution_target is None and rows[0].infra is True
|
||||||
Reference in New Issue
Block a user