blitz-cli 0.12.0__tar.gz → 0.13.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/PKG-INFO +1 -1
- blitz_cli-0.13.0/blitz_cli/_collect.py +383 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/_execute.py +254 -37
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/cli.py +168 -1
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/commands/tasks.md +1 -1
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/pyproject.toml +1 -1
- blitz_cli-0.13.0/specs/004-dpo-collect-pairs/checklists/requirements.md +36 -0
- blitz_cli-0.13.0/specs/004-dpo-collect-pairs/contracts/cli.md +52 -0
- blitz_cli-0.13.0/specs/004-dpo-collect-pairs/contracts/export-format.md +21 -0
- blitz_cli-0.13.0/specs/004-dpo-collect-pairs/data-model.md +105 -0
- blitz_cli-0.13.0/specs/004-dpo-collect-pairs/plan.md +112 -0
- blitz_cli-0.13.0/specs/004-dpo-collect-pairs/quickstart.md +88 -0
- blitz_cli-0.13.0/specs/004-dpo-collect-pairs/research.md +70 -0
- blitz_cli-0.13.0/specs/004-dpo-collect-pairs/spec.md +140 -0
- blitz_cli-0.13.0/specs/004-dpo-collect-pairs/tasks.md +156 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/tests/test_cli.py +120 -0
- blitz_cli-0.13.0/tests/test_collect.py +358 -0
- blitz_cli-0.13.0/tests/test_execute.py +708 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/tests/test_ontology.py +2 -2
- blitz_cli-0.12.0/tests/test_execute.py +0 -336
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/.blitz/memory/playbook.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/.blitz/scripts/bash/check-prerequisites.sh +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/.blitz/scripts/bash/common.sh +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/.blitz/scripts/bash/create-new-feature.sh +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/.blitz/scripts/bash/setup-plan.sh +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/.blitz/scripts/bash/setup-tasks.sh +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/.blitz/templates/plan-template.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/.blitz/templates/spec-template.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/.blitz/templates/tasks-template.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/.github/workflows/homebrew.yml +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/.github/workflows/publish.yml +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/.gitignore +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/.mcp.json +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/LICENSE +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/README.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/__init__.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/_activity.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/_code_graph.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/_corpus.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/_dataset.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/_git.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/_gitignore.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/_make.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/_ontology.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/_playbooks.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/_redact.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/_scaffold.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/_tool_schemas.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/_train.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/__init__.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/commands/__init__.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/commands/analyze.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/commands/clarify.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/commands/implement.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/commands/plan.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/commands/playbook.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/commands/specify.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/scaffold/.blitz/memory/playbook.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/check-prerequisites.sh +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/common.sh +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/create-new-feature.sh +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/setup-plan.sh +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/setup-tasks.sh +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/scaffold/.blitz/templates/plan-template.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/scaffold/.blitz/templates/spec-template.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/scaffold/.blitz/templates/tasks-template.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/workspace/__init__.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/playbooks/workspace/huddle.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/templates/__init__.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/templates/locker_room_Makefile.tmpl +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/templates/locker_room_README.md.tmpl +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/templates/locker_room_compose.yml.tmpl +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/blitz_cli/templates/workspace_playbook.md.tmpl +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/001-cli-model-namespace/checklists/requirements.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/001-cli-model-namespace/contracts/cli-commands.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/001-cli-model-namespace/plan.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/001-cli-model-namespace/quickstart.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/001-cli-model-namespace/research.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/001-cli-model-namespace/spec.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/001-cli-model-namespace/tasks.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/002-label-experimental-surfaces/checklists/requirements.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/002-label-experimental-surfaces/contracts/experimental-labels.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/002-label-experimental-surfaces/data-model.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/002-label-experimental-surfaces/plan.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/002-label-experimental-surfaces/quickstart.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/002-label-experimental-surfaces/research.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/002-label-experimental-surfaces/spec.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/002-label-experimental-surfaces/tasks.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/003-init-gitignore-rules/checklists/requirements.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/003-init-gitignore-rules/contracts/cli-output.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/003-init-gitignore-rules/contracts/ensure-rules.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/003-init-gitignore-rules/contracts/gitignore-block.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/003-init-gitignore-rules/data-model.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/003-init-gitignore-rules/plan.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/003-init-gitignore-rules/quickstart.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/003-init-gitignore-rules/research.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/003-init-gitignore-rules/spec.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/specs/003-init-gitignore-rules/tasks.md +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/tests/test_activity.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/tests/test_corpus.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/tests/test_gitignore.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/tests/test_make.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/tests/test_playbooks.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/tests/test_redact.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/tests/test_scaffold.py +0 -0
- {blitz_cli-0.12.0 → blitz_cli-0.13.0}/tests/test_tool_schemas.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blitz-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Developer CLI for Blitz locker-room: spec-driven playbooks, a codebase/huddle ontology graph with an MCP tool, and the composable watcher/trainer/graph containers.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""DPO data-collection store and preference-pair tooling for
|
|
2
|
+
`blitz model execute --collect` and the `blitz model pairs` command family.
|
|
3
|
+
|
|
4
|
+
`CollectStore` owns four tables (`collect_runs`, `collect_attempts`,
|
|
5
|
+
`dpo_pairs`, `sft_queue`) in the same locker-room activity database that
|
|
6
|
+
`_corpus.py`'s `TurnStore` writes its transcript corpus to (default
|
|
7
|
+
`data/activity.db`, env override `ACTIVITY_DB_PATH`, WAL journal). Like
|
|
8
|
+
`TurnStore`, it is the one writer of its tables and creates its schema on
|
|
9
|
+
first connect.
|
|
10
|
+
|
|
11
|
+
The execute loop inserts one `collect_runs` row per `--collect` invocation
|
|
12
|
+
and one `collect_attempts` row per model call, capturing the exact prompt
|
|
13
|
+
messages sent to the model verbatim. `extract_pairs` turns those attempts
|
|
14
|
+
into chosen/rejected DPO preference pairs (pass/fail contrast on byte
|
|
15
|
+
identical prompt lineage, or judge-ranked green-vs-green), `export_pairs`
|
|
16
|
+
emits TRL-shaped JSONL, and `stats` reports collection health per run.
|
|
17
|
+
`sft_queue` quarantines placeholders for tasks no chain solved; it never
|
|
18
|
+
feeds pairs.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import sqlite3
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Optional
|
|
29
|
+
|
|
30
|
+
_SCHEMA = """
|
|
31
|
+
CREATE TABLE IF NOT EXISTS collect_runs (
|
|
32
|
+
run_id TEXT PRIMARY KEY,
|
|
33
|
+
repo_path TEXT NOT NULL,
|
|
34
|
+
spec TEXT NOT NULL,
|
|
35
|
+
tasks_file TEXT NOT NULL,
|
|
36
|
+
model TEXT NOT NULL,
|
|
37
|
+
endpoint TEXT NOT NULL,
|
|
38
|
+
temperature REAL NOT NULL,
|
|
39
|
+
max_retries INTEGER NOT NULL,
|
|
40
|
+
k INTEGER NOT NULL,
|
|
41
|
+
started_at TEXT NOT NULL,
|
|
42
|
+
finished_at TEXT
|
|
43
|
+
);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_collect_runs_started_at ON collect_runs(started_at);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE IF NOT EXISTS collect_attempts (
|
|
47
|
+
attempt_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
48
|
+
run_id TEXT NOT NULL REFERENCES collect_runs(run_id),
|
|
49
|
+
task_id TEXT NOT NULL,
|
|
50
|
+
sample_index INTEGER NOT NULL,
|
|
51
|
+
attempt_index INTEGER NOT NULL,
|
|
52
|
+
prompt_messages_json TEXT NOT NULL,
|
|
53
|
+
reply TEXT NOT NULL,
|
|
54
|
+
actions_json TEXT NOT NULL,
|
|
55
|
+
passed INTEGER NOT NULL,
|
|
56
|
+
error_detail TEXT,
|
|
57
|
+
guard_violation TEXT,
|
|
58
|
+
input_tokens INTEGER,
|
|
59
|
+
output_tokens INTEGER,
|
|
60
|
+
env_failure INTEGER NOT NULL DEFAULT 0,
|
|
61
|
+
judge_rank INTEGER,
|
|
62
|
+
judge_notes TEXT,
|
|
63
|
+
created_at TEXT NOT NULL,
|
|
64
|
+
UNIQUE(run_id, task_id, sample_index, attempt_index)
|
|
65
|
+
);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_attempts_task_lineage
|
|
67
|
+
ON collect_attempts(task_id, prompt_messages_json);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_attempts_run ON collect_attempts(run_id);
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS dpo_pairs (
|
|
71
|
+
pair_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
72
|
+
chosen_attempt_id INTEGER NOT NULL REFERENCES collect_attempts(attempt_id),
|
|
73
|
+
rejected_attempt_id INTEGER NOT NULL REFERENCES collect_attempts(attempt_id),
|
|
74
|
+
task_id TEXT NOT NULL,
|
|
75
|
+
chosen_source TEXT NOT NULL CHECK (chosen_source IN ('local', 'judge-ranked')),
|
|
76
|
+
status TEXT NOT NULL DEFAULT 'candidate'
|
|
77
|
+
CHECK (status IN ('candidate', 'approved', 'rejected')),
|
|
78
|
+
judge_notes TEXT,
|
|
79
|
+
created_at TEXT NOT NULL,
|
|
80
|
+
UNIQUE(chosen_attempt_id, rejected_attempt_id),
|
|
81
|
+
CHECK (chosen_attempt_id != rejected_attempt_id)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
CREATE TABLE IF NOT EXISTS sft_queue (
|
|
85
|
+
item_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
86
|
+
run_id TEXT NOT NULL REFERENCES collect_runs(run_id),
|
|
87
|
+
task_id TEXT NOT NULL,
|
|
88
|
+
prompt_messages_json TEXT NOT NULL,
|
|
89
|
+
solution_text TEXT,
|
|
90
|
+
source TEXT NOT NULL DEFAULT 'placeholder',
|
|
91
|
+
status TEXT NOT NULL DEFAULT 'needs_solution',
|
|
92
|
+
created_at TEXT NOT NULL,
|
|
93
|
+
UNIQUE(run_id, task_id)
|
|
94
|
+
);
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def default_db_path() -> Path:
|
|
99
|
+
"""The activity database path: ACTIVITY_DB_PATH env override, else the
|
|
100
|
+
conventional data/activity.db relative to the CWD (same idiom as _train.py)."""
|
|
101
|
+
return Path(os.environ.get("ACTIVITY_DB_PATH", "data/activity.db"))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _now_iso() -> str:
|
|
105
|
+
return datetime.now(timezone.utc).isoformat()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class CollectStore:
|
|
109
|
+
"""Writer for the DPO collection tables. Owns its schema and creates it on
|
|
110
|
+
first connect, the same writer-owns-schema pattern as `_corpus.TurnStore`,
|
|
111
|
+
against the same database file."""
|
|
112
|
+
|
|
113
|
+
def __init__(self, db_path: Path):
|
|
114
|
+
self._db_path = Path(db_path)
|
|
115
|
+
self._schema_ready = False
|
|
116
|
+
|
|
117
|
+
def _connect(self) -> sqlite3.Connection:
|
|
118
|
+
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
conn = sqlite3.connect(self._db_path)
|
|
120
|
+
conn.row_factory = sqlite3.Row
|
|
121
|
+
if not self._schema_ready:
|
|
122
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
123
|
+
conn.executescript(_SCHEMA)
|
|
124
|
+
conn.commit()
|
|
125
|
+
self._schema_ready = True
|
|
126
|
+
return conn
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def db_path(self) -> Path:
|
|
130
|
+
return self._db_path
|
|
131
|
+
|
|
132
|
+
def check_writable(self) -> None:
|
|
133
|
+
"""Create the schema and probe write access with BEGIN IMMEDIATE.
|
|
134
|
+
Raises sqlite3.OperationalError or OSError when the database is
|
|
135
|
+
unwritable or locked, so a --collect run can fail loudly before any
|
|
136
|
+
task runs instead of dropping attempt rows mid-run."""
|
|
137
|
+
conn = self._connect()
|
|
138
|
+
try:
|
|
139
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
140
|
+
conn.execute("ROLLBACK")
|
|
141
|
+
finally:
|
|
142
|
+
conn.close()
|
|
143
|
+
|
|
144
|
+
def insert_run(
|
|
145
|
+
self,
|
|
146
|
+
run_id: str,
|
|
147
|
+
repo_path: str,
|
|
148
|
+
spec: str,
|
|
149
|
+
tasks_file: str,
|
|
150
|
+
model: str,
|
|
151
|
+
endpoint: str,
|
|
152
|
+
temperature: float,
|
|
153
|
+
max_retries: int,
|
|
154
|
+
k: int,
|
|
155
|
+
) -> None:
|
|
156
|
+
conn = self._connect()
|
|
157
|
+
try:
|
|
158
|
+
conn.execute(
|
|
159
|
+
"INSERT INTO collect_runs (run_id, repo_path, spec, tasks_file, model, "
|
|
160
|
+
"endpoint, temperature, max_retries, k, started_at) "
|
|
161
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
162
|
+
(run_id, repo_path, spec, tasks_file, model, endpoint,
|
|
163
|
+
temperature, max_retries, k, _now_iso()),
|
|
164
|
+
)
|
|
165
|
+
conn.commit()
|
|
166
|
+
finally:
|
|
167
|
+
conn.close()
|
|
168
|
+
|
|
169
|
+
def finish_run(self, run_id: str) -> None:
|
|
170
|
+
conn = self._connect()
|
|
171
|
+
try:
|
|
172
|
+
conn.execute(
|
|
173
|
+
"UPDATE collect_runs SET finished_at = ? WHERE run_id = ?",
|
|
174
|
+
(_now_iso(), run_id),
|
|
175
|
+
)
|
|
176
|
+
conn.commit()
|
|
177
|
+
finally:
|
|
178
|
+
conn.close()
|
|
179
|
+
|
|
180
|
+
def insert_attempt(
|
|
181
|
+
self,
|
|
182
|
+
run_id: str,
|
|
183
|
+
task_id: str,
|
|
184
|
+
sample_index: int,
|
|
185
|
+
attempt_index: int,
|
|
186
|
+
prompt_messages_json: str,
|
|
187
|
+
reply: str,
|
|
188
|
+
actions_json: str,
|
|
189
|
+
passed: int,
|
|
190
|
+
error_detail: Optional[str] = None,
|
|
191
|
+
guard_violation: Optional[str] = None,
|
|
192
|
+
input_tokens: Optional[int] = None,
|
|
193
|
+
output_tokens: Optional[int] = None,
|
|
194
|
+
) -> int:
|
|
195
|
+
conn = self._connect()
|
|
196
|
+
try:
|
|
197
|
+
cur = conn.execute(
|
|
198
|
+
"INSERT INTO collect_attempts (run_id, task_id, sample_index, "
|
|
199
|
+
"attempt_index, prompt_messages_json, reply, actions_json, passed, "
|
|
200
|
+
"error_detail, guard_violation, input_tokens, output_tokens, created_at) "
|
|
201
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
202
|
+
(run_id, task_id, sample_index, attempt_index, prompt_messages_json,
|
|
203
|
+
reply, actions_json, passed, error_detail, guard_violation,
|
|
204
|
+
input_tokens, output_tokens, _now_iso()),
|
|
205
|
+
)
|
|
206
|
+
conn.commit()
|
|
207
|
+
return cur.lastrowid
|
|
208
|
+
finally:
|
|
209
|
+
conn.close()
|
|
210
|
+
|
|
211
|
+
def insert_sft_placeholder(
|
|
212
|
+
self, run_id: str, task_id: str, prompt_messages_json: str
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Queue a repair placeholder for a task no chain solved. INSERT OR
|
|
215
|
+
IGNORE plus UNIQUE(run_id, task_id) keeps it one row per task per run."""
|
|
216
|
+
conn = self._connect()
|
|
217
|
+
try:
|
|
218
|
+
conn.execute(
|
|
219
|
+
"INSERT OR IGNORE INTO sft_queue "
|
|
220
|
+
"(run_id, task_id, prompt_messages_json, created_at) "
|
|
221
|
+
"VALUES (?, ?, ?, ?)",
|
|
222
|
+
(run_id, task_id, prompt_messages_json, _now_iso()),
|
|
223
|
+
)
|
|
224
|
+
conn.commit()
|
|
225
|
+
finally:
|
|
226
|
+
conn.close()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def extract_pairs(store: CollectStore, run_id: Optional[str] = None) -> dict:
|
|
230
|
+
"""Materialize eligible DPO preference pairs from collected attempts.
|
|
231
|
+
|
|
232
|
+
Pass/fail pairs join a passing attempt (chosen) with a failing attempt
|
|
233
|
+
(rejected) of the same task on byte-identical prompt_messages_json, both
|
|
234
|
+
free of the judge's env_failure flag, chosen_source 'local'. Green-vs-green
|
|
235
|
+
pairs exist only where the judge has ranked both passing attempts (lower
|
|
236
|
+
judge_rank wins), chosen_source 'judge-ranked'. sft_queue is never read.
|
|
237
|
+
Idempotent: UNIQUE(chosen_attempt_id, rejected_attempt_id) plus INSERT OR
|
|
238
|
+
IGNORE makes repeated runs, with or without a run filter, no-ops.
|
|
239
|
+
|
|
240
|
+
Returns {"created_local": n, "created_judge": n, "existing": n}."""
|
|
241
|
+
run_filter = ""
|
|
242
|
+
args: tuple = ()
|
|
243
|
+
if run_id is not None:
|
|
244
|
+
run_filter = " AND a.run_id = ? AND b.run_id = ?"
|
|
245
|
+
args = (run_id, run_id)
|
|
246
|
+
|
|
247
|
+
local_sql = (
|
|
248
|
+
"SELECT a.attempt_id, b.attempt_id, a.task_id "
|
|
249
|
+
"FROM collect_attempts a JOIN collect_attempts b "
|
|
250
|
+
"ON a.task_id = b.task_id "
|
|
251
|
+
"AND a.prompt_messages_json = b.prompt_messages_json "
|
|
252
|
+
"WHERE a.passed = 1 AND b.passed = 0 "
|
|
253
|
+
"AND a.env_failure = 0 AND b.env_failure = 0" + run_filter
|
|
254
|
+
)
|
|
255
|
+
judge_sql = (
|
|
256
|
+
"SELECT a.attempt_id, b.attempt_id, a.task_id "
|
|
257
|
+
"FROM collect_attempts a JOIN collect_attempts b "
|
|
258
|
+
"ON a.task_id = b.task_id "
|
|
259
|
+
"AND a.prompt_messages_json = b.prompt_messages_json "
|
|
260
|
+
"WHERE a.passed = 1 AND b.passed = 1 "
|
|
261
|
+
"AND a.judge_rank IS NOT NULL AND b.judge_rank IS NOT NULL "
|
|
262
|
+
"AND a.judge_rank < b.judge_rank "
|
|
263
|
+
"AND a.env_failure = 0 AND b.env_failure = 0" + run_filter
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
counts = {"created_local": 0, "created_judge": 0, "existing": 0}
|
|
267
|
+
conn = store._connect()
|
|
268
|
+
try:
|
|
269
|
+
for sql, source, key in (
|
|
270
|
+
(local_sql, "local", "created_local"),
|
|
271
|
+
(judge_sql, "judge-ranked", "created_judge"),
|
|
272
|
+
):
|
|
273
|
+
for chosen_id, rejected_id, task_id in conn.execute(sql, args).fetchall():
|
|
274
|
+
cur = conn.execute(
|
|
275
|
+
"INSERT OR IGNORE INTO dpo_pairs (chosen_attempt_id, "
|
|
276
|
+
"rejected_attempt_id, task_id, chosen_source, status, created_at) "
|
|
277
|
+
"VALUES (?, ?, ?, ?, 'candidate', ?)",
|
|
278
|
+
(chosen_id, rejected_id, task_id, source, _now_iso()),
|
|
279
|
+
)
|
|
280
|
+
if cur.rowcount:
|
|
281
|
+
counts[key] += 1
|
|
282
|
+
else:
|
|
283
|
+
counts["existing"] += 1
|
|
284
|
+
conn.commit()
|
|
285
|
+
finally:
|
|
286
|
+
conn.close()
|
|
287
|
+
return counts
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def export_pairs(
|
|
291
|
+
store: CollectStore, out_path: Path, statuses: Optional[list] = None
|
|
292
|
+
) -> int:
|
|
293
|
+
"""Write pairs as dpo-jsonl (contracts/export-format.md): one JSON object
|
|
294
|
+
per line with exactly the keys prompt (the pair's shared message list),
|
|
295
|
+
chosen, and rejected. Returns the number of lines written; no matching
|
|
296
|
+
pairs writes an empty file."""
|
|
297
|
+
sql = (
|
|
298
|
+
"SELECT c.prompt_messages_json AS prompt, c.reply AS chosen, "
|
|
299
|
+
"r.reply AS rejected "
|
|
300
|
+
"FROM dpo_pairs p "
|
|
301
|
+
"JOIN collect_attempts c ON c.attempt_id = p.chosen_attempt_id "
|
|
302
|
+
"JOIN collect_attempts r ON r.attempt_id = p.rejected_attempt_id"
|
|
303
|
+
)
|
|
304
|
+
args: tuple = ()
|
|
305
|
+
if statuses:
|
|
306
|
+
sql += f" WHERE p.status IN ({', '.join('?' * len(statuses))})"
|
|
307
|
+
args = tuple(statuses)
|
|
308
|
+
sql += " ORDER BY p.pair_id"
|
|
309
|
+
|
|
310
|
+
conn = store._connect()
|
|
311
|
+
try:
|
|
312
|
+
rows = conn.execute(sql, args).fetchall()
|
|
313
|
+
finally:
|
|
314
|
+
conn.close()
|
|
315
|
+
|
|
316
|
+
out_path = Path(out_path)
|
|
317
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
318
|
+
with out_path.open("w", encoding="utf-8") as f:
|
|
319
|
+
for row in rows:
|
|
320
|
+
f.write(json.dumps(
|
|
321
|
+
{
|
|
322
|
+
"prompt": json.loads(row["prompt"]),
|
|
323
|
+
"chosen": row["chosen"],
|
|
324
|
+
"rejected": row["rejected"],
|
|
325
|
+
},
|
|
326
|
+
ensure_ascii=False,
|
|
327
|
+
) + "\n")
|
|
328
|
+
return len(rows)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def stats(store: CollectStore, run_id: Optional[str] = None) -> list:
|
|
332
|
+
"""Per-run collection health: attempt count, per-task pass rates with the
|
|
333
|
+
20 to 80 percent band flag, pair yield (pairs whose chosen attempt belongs
|
|
334
|
+
to the run), and repair-queue depth (needs_solution rows for the run)."""
|
|
335
|
+
conn = store._connect()
|
|
336
|
+
try:
|
|
337
|
+
if run_id is not None:
|
|
338
|
+
runs = conn.execute(
|
|
339
|
+
"SELECT run_id, started_at FROM collect_runs WHERE run_id = ? "
|
|
340
|
+
"ORDER BY started_at", (run_id,),
|
|
341
|
+
).fetchall()
|
|
342
|
+
else:
|
|
343
|
+
runs = conn.execute(
|
|
344
|
+
"SELECT run_id, started_at FROM collect_runs ORDER BY started_at"
|
|
345
|
+
).fetchall()
|
|
346
|
+
|
|
347
|
+
out = []
|
|
348
|
+
for run in runs:
|
|
349
|
+
rid = run["run_id"]
|
|
350
|
+
task_rows = conn.execute(
|
|
351
|
+
"SELECT task_id, COUNT(*) AS attempts, SUM(passed) AS passes "
|
|
352
|
+
"FROM collect_attempts WHERE run_id = ? GROUP BY task_id "
|
|
353
|
+
"ORDER BY task_id", (rid,),
|
|
354
|
+
).fetchall()
|
|
355
|
+
tasks = []
|
|
356
|
+
for t in task_rows:
|
|
357
|
+
rate = (t["passes"] or 0) / t["attempts"]
|
|
358
|
+
tasks.append({
|
|
359
|
+
"task_id": t["task_id"],
|
|
360
|
+
"attempts": t["attempts"],
|
|
361
|
+
"passes": t["passes"] or 0,
|
|
362
|
+
"pass_rate": rate,
|
|
363
|
+
"in_band": 0.2 <= rate <= 0.8,
|
|
364
|
+
})
|
|
365
|
+
pair_yield = conn.execute(
|
|
366
|
+
"SELECT COUNT(*) FROM dpo_pairs p JOIN collect_attempts a "
|
|
367
|
+
"ON a.attempt_id = p.chosen_attempt_id WHERE a.run_id = ?", (rid,),
|
|
368
|
+
).fetchone()[0]
|
|
369
|
+
repair_depth = conn.execute(
|
|
370
|
+
"SELECT COUNT(*) FROM sft_queue WHERE run_id = ? "
|
|
371
|
+
"AND status = 'needs_solution'", (rid,),
|
|
372
|
+
).fetchone()[0]
|
|
373
|
+
out.append({
|
|
374
|
+
"run_id": rid,
|
|
375
|
+
"started_at": run["started_at"],
|
|
376
|
+
"attempts": sum(t["attempts"] for t in tasks),
|
|
377
|
+
"tasks": tasks,
|
|
378
|
+
"pair_yield": pair_yield,
|
|
379
|
+
"repair_depth": repair_depth,
|
|
380
|
+
})
|
|
381
|
+
return out
|
|
382
|
+
finally:
|
|
383
|
+
conn.close()
|