sqbyl 0.1.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.
- sqbyl-0.1.0/.gitignore +44 -0
- sqbyl-0.1.0/PKG-INFO +40 -0
- sqbyl-0.1.0/README.md +10 -0
- sqbyl-0.1.0/pyproject.toml +54 -0
- sqbyl-0.1.0/src/sqbyl/__init__.py +7 -0
- sqbyl-0.1.0/src/sqbyl/annotate.py +150 -0
- sqbyl-0.1.0/src/sqbyl/attention.py +209 -0
- sqbyl-0.1.0/src/sqbyl/calibration_io.py +87 -0
- sqbyl-0.1.0/src/sqbyl/candidates_io.py +66 -0
- sqbyl-0.1.0/src/sqbyl/cli.py +1717 -0
- sqbyl-0.1.0/src/sqbyl/coach.py +442 -0
- sqbyl-0.1.0/src/sqbyl/console/__init__.py +14 -0
- sqbyl-0.1.0/src/sqbyl/console/app.py +366 -0
- sqbyl-0.1.0/src/sqbyl/console/static/index.html +443 -0
- sqbyl-0.1.0/src/sqbyl/estimates.py +204 -0
- sqbyl-0.1.0/src/sqbyl/eval/__init__.py +10 -0
- sqbyl-0.1.0/src/sqbyl/eval/benchmarks_io.py +112 -0
- sqbyl-0.1.0/src/sqbyl/eval/comparator.py +158 -0
- sqbyl-0.1.0/src/sqbyl/eval/heldout.py +26 -0
- sqbyl-0.1.0/src/sqbyl/eval/judges.py +324 -0
- sqbyl-0.1.0/src/sqbyl/eval/report.py +89 -0
- sqbyl-0.1.0/src/sqbyl/eval/runner.py +250 -0
- sqbyl-0.1.0/src/sqbyl/eval/scorers.py +182 -0
- sqbyl-0.1.0/src/sqbyl/eval/selection.py +189 -0
- sqbyl-0.1.0/src/sqbyl/importers.py +373 -0
- sqbyl-0.1.0/src/sqbyl/init.py +573 -0
- sqbyl-0.1.0/src/sqbyl/introspect.py +177 -0
- sqbyl-0.1.0/src/sqbyl/kpis.py +124 -0
- sqbyl-0.1.0/src/sqbyl/llm.py +53 -0
- sqbyl-0.1.0/src/sqbyl/models/__init__.py +120 -0
- sqbyl-0.1.0/src/sqbyl/models/attention.py +141 -0
- sqbyl-0.1.0/src/sqbyl/models/benchmarks.py +38 -0
- sqbyl-0.1.0/src/sqbyl/models/candidates.py +152 -0
- sqbyl-0.1.0/src/sqbyl/models/coach.py +149 -0
- sqbyl-0.1.0/src/sqbyl/models/feedback.py +44 -0
- sqbyl-0.1.0/src/sqbyl/models/judges.py +61 -0
- sqbyl-0.1.0/src/sqbyl/models/kpis.py +99 -0
- sqbyl-0.1.0/src/sqbyl/models/manifest.py +133 -0
- sqbyl-0.1.0/src/sqbyl/models/optimize.py +126 -0
- sqbyl-0.1.0/src/sqbyl/models/runs.py +363 -0
- sqbyl-0.1.0/src/sqbyl/optimize.py +297 -0
- sqbyl-0.1.0/src/sqbyl/orchestrator.py +357 -0
- sqbyl-0.1.0/src/sqbyl/profile.py +219 -0
- sqbyl-0.1.0/src/sqbyl/project.py +126 -0
- sqbyl-0.1.0/src/sqbyl/projectfiles.py +87 -0
- sqbyl-0.1.0/src/sqbyl/py.typed +0 -0
- sqbyl-0.1.0/src/sqbyl/release.py +196 -0
- sqbyl-0.1.0/src/sqbyl/semantics_io.py +123 -0
- sqbyl-0.1.0/src/sqbyl/serve.py +441 -0
- sqbyl-0.1.0/src/sqbyl/state/__init__.py +7 -0
- sqbyl-0.1.0/src/sqbyl/state/hashing.py +69 -0
- sqbyl-0.1.0/src/sqbyl/stats.py +75 -0
- sqbyl-0.1.0/src/sqbyl/synth.py +371 -0
- sqbyl-0.1.0/src/sqbyl/yamlio.py +68 -0
sqbyl-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Claude Code / local agent workspace (designer-dev only, not part of the project)
|
|
2
|
+
.claude/
|
|
3
|
+
|
|
4
|
+
# sqbyl local state: runs, traces, usage, caches (per design spec §4)
|
|
5
|
+
.sqbyl/
|
|
6
|
+
|
|
7
|
+
# Python
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
*$py.class
|
|
11
|
+
*.egg-info/
|
|
12
|
+
.eggs/
|
|
13
|
+
build/
|
|
14
|
+
dist/
|
|
15
|
+
*.egg
|
|
16
|
+
|
|
17
|
+
# uv / virtualenvs
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
env/
|
|
21
|
+
.python-version
|
|
22
|
+
|
|
23
|
+
# Tooling caches
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
.ruff_cache/
|
|
26
|
+
.mypy_cache/
|
|
27
|
+
.pyright/
|
|
28
|
+
.coverage
|
|
29
|
+
htmlcov/
|
|
30
|
+
.tox/
|
|
31
|
+
|
|
32
|
+
# Environment / secrets (never commit ANTHROPIC_API_KEY or DATABASE_URL)
|
|
33
|
+
.env
|
|
34
|
+
.env.*
|
|
35
|
+
!.env.example
|
|
36
|
+
|
|
37
|
+
# OS / editor
|
|
38
|
+
.DS_Store
|
|
39
|
+
.idea/
|
|
40
|
+
.vscode/
|
|
41
|
+
*.swp
|
|
42
|
+
|
|
43
|
+
# Private planning doc — kept local, not published
|
|
44
|
+
sqbyl-enhancements.md
|
sqbyl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqbyl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The full sqbyl dev toolkit: introspect, profile, annotate, synth, eval, Coach, judges, console, optimizer, release builder.
|
|
5
|
+
Project-URL: Homepage, https://github.com/jackwerner/sqbyl
|
|
6
|
+
Project-URL: Repository, https://github.com/jackwerner/sqbyl
|
|
7
|
+
Project-URL: Issues, https://github.com/jackwerner/sqbyl/issues
|
|
8
|
+
Author: Jack Werner
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agent,anthropic,claude,evaluation,llm,sql,text-to-sql
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Database
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: fastapi>=0.139.0
|
|
21
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
22
|
+
Requires-Dist: pyyaml>=6
|
|
23
|
+
Requires-Dist: sqbyl-runtime==0.1.0
|
|
24
|
+
Requires-Dist: uvicorn>=0.50.2
|
|
25
|
+
Provides-Extra: anthropic
|
|
26
|
+
Requires-Dist: sqbyl-runtime[anthropic]==0.1.0; extra == 'anthropic'
|
|
27
|
+
Provides-Extra: openai
|
|
28
|
+
Requires-Dist: sqbyl-runtime[openai]==0.1.0; extra == 'openai'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# sqbyl
|
|
32
|
+
|
|
33
|
+
The full text-to-SQL dev toolkit. See the [repository root README](../../README.md)
|
|
34
|
+
for the product overview, and `docs/sqbyl-design-spec.md` /
|
|
35
|
+
`docs/sqbyl-implementation-plan.md` for the design and build sequence.
|
|
36
|
+
|
|
37
|
+
This package contains the dev machinery — introspect, profile, annotate, synth,
|
|
38
|
+
the eval harness, the Coach, LLM judges, the review console, the orchestrator,
|
|
39
|
+
the optimizer, and the release builder. It depends on `sqbyl-runtime`
|
|
40
|
+
(never the reverse).
|
sqbyl-0.1.0/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# sqbyl
|
|
2
|
+
|
|
3
|
+
The full text-to-SQL dev toolkit. See the [repository root README](../../README.md)
|
|
4
|
+
for the product overview, and `docs/sqbyl-design-spec.md` /
|
|
5
|
+
`docs/sqbyl-implementation-plan.md` for the design and build sequence.
|
|
6
|
+
|
|
7
|
+
This package contains the dev machinery — introspect, profile, annotate, synth,
|
|
8
|
+
the eval harness, the Coach, LLM judges, the review console, the orchestrator,
|
|
9
|
+
the optimizer, and the release builder. It depends on `sqbyl-runtime`
|
|
10
|
+
(never the reverse).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sqbyl"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "The full sqbyl dev toolkit: introspect, profile, annotate, synth, eval, Coach, judges, console, optimizer, release builder."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "Jack Werner" }]
|
|
9
|
+
keywords = ["text-to-sql", "sql", "llm", "claude", "anthropic", "agent", "evaluation"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Topic :: Database",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
# Lockstep: the toolkit and runtime are cut together, so pin the exact runtime
|
|
22
|
+
# this toolkit was built and tested against.
|
|
23
|
+
"sqbyl-runtime==0.1.0",
|
|
24
|
+
"pydantic>=2.7,<3",
|
|
25
|
+
"pyyaml>=6",
|
|
26
|
+
# The review console (spec §6.5) is a local FastAPI app with a bundled UI. It lives
|
|
27
|
+
# in the dev toolkit only — the shipped `sqbyl-runtime` never carries a web stack.
|
|
28
|
+
"fastapi>=0.139.0",
|
|
29
|
+
"uvicorn>=0.50.2",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
# Provider SDKs are optional and provider-neutral (see sqbyl-runtime): pick one and install
|
|
34
|
+
# its extra alongside the toolkit, e.g. `pip install sqbyl[openai]`.
|
|
35
|
+
anthropic = ["sqbyl-runtime[anthropic]==0.1.0"]
|
|
36
|
+
openai = ["sqbyl-runtime[openai]==0.1.0"]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
sqbyl = "sqbyl.cli:main"
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/jackwerner/sqbyl"
|
|
43
|
+
Repository = "https://github.com/jackwerner/sqbyl"
|
|
44
|
+
Issues = "https://github.com/jackwerner/sqbyl/issues"
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["hatchling"]
|
|
48
|
+
build-backend = "hatchling.build"
|
|
49
|
+
|
|
50
|
+
[tool.uv.sources]
|
|
51
|
+
sqbyl-runtime = { workspace = true }
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.wheel]
|
|
54
|
+
packages = ["src/sqbyl"]
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""The annotator — ``sqbyl annotate`` (spec §3 #1, plan 2.3).
|
|
2
|
+
|
|
3
|
+
Claude drafts table/column **descriptions and synonyms**, grounded in the Phase 1.3
|
|
4
|
+
**profile** (ranges, distinct counts, sample values) rather than guessing from
|
|
5
|
+
names — it can see that ``amount_cents`` spans 127..310229 with no nulls and infer
|
|
6
|
+
the cents unit, that ``status`` has three values, that ``created_at`` covers
|
|
7
|
+
2019→today. Each annotation carries a **confidence** the attention router consumes
|
|
8
|
+
later (Phase 6).
|
|
9
|
+
|
|
10
|
+
This is dev-only authoring, so it lives in ``sqbyl`` and depends on the runtime's
|
|
11
|
+
LLM seam. It is per-table (parallel fan-out is Phase 6; here it is sequential). As
|
|
12
|
+
a paid command it is metered through the cost stub from day one (invariant 5).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
|
|
19
|
+
from sqbyl_runtime.llm.base import LLMClient, LLMRequest, LLMResponse, Message
|
|
20
|
+
from sqbyl_runtime.models import Column, Profile, TableSemantics
|
|
21
|
+
from sqbyl_runtime.state.traces import TraceWriter, llm_call_span, new_trace_id
|
|
22
|
+
|
|
23
|
+
_SYSTEM = (
|
|
24
|
+
"You are a data analyst documenting a SQL table for a text-to-SQL agent. "
|
|
25
|
+
"Write concise, factual descriptions grounded in the COLUMN PROFILE STATISTICS "
|
|
26
|
+
"provided (ranges, distinct counts, sample values) — infer units and meaning from "
|
|
27
|
+
"the data, never invent facts you can't support. Add a few natural-language "
|
|
28
|
+
"synonyms a user might say. Give each description a confidence in [0,1]: high when "
|
|
29
|
+
"the data makes the meaning obvious, low when the column is cryptic."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ColumnAnnotation(BaseModel):
|
|
34
|
+
"""A drafted description for one column, with the router's confidence signal."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
description: str = Field(description="One factual sentence, grounded in the profile.")
|
|
38
|
+
synonyms: list[str] = Field(default_factory=list)
|
|
39
|
+
confidence: float = Field(ge=0.0, le=1.0)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TableAnnotation(BaseModel):
|
|
43
|
+
"""A drafted description for a table and its columns."""
|
|
44
|
+
|
|
45
|
+
description: str = Field(description="What one row represents, grounded in the data.")
|
|
46
|
+
synonyms: list[str] = Field(default_factory=list)
|
|
47
|
+
confidence: float = Field(ge=0.0, le=1.0)
|
|
48
|
+
columns: list[ColumnAnnotation] = Field(default_factory=list)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def annotate_table(
|
|
52
|
+
llm: LLMClient,
|
|
53
|
+
table: TableSemantics,
|
|
54
|
+
*,
|
|
55
|
+
model: str,
|
|
56
|
+
trace_writer: TraceWriter | None = None,
|
|
57
|
+
trace_id: str | None = None,
|
|
58
|
+
parent_span_id: str | None = None,
|
|
59
|
+
) -> tuple[TableAnnotation, LLMResponse]:
|
|
60
|
+
"""Draft a description/synonyms for a table and its columns, grounded in the profile.
|
|
61
|
+
|
|
62
|
+
Like the agent pipeline, this builds the request explicitly so the token-spending
|
|
63
|
+
call can be written as an OTel-GenAI span when a ``trace_writer`` is given
|
|
64
|
+
(invariant 7). ``max_tokens`` matches the seam's structured default so existing
|
|
65
|
+
cassettes stay valid.
|
|
66
|
+
"""
|
|
67
|
+
request = LLMRequest(
|
|
68
|
+
model=model,
|
|
69
|
+
messages=[Message(role="user", content=_render_for_annotation(table))],
|
|
70
|
+
system=_SYSTEM,
|
|
71
|
+
response_schema=TableAnnotation.model_json_schema(),
|
|
72
|
+
max_tokens=4096,
|
|
73
|
+
temperature=0.0,
|
|
74
|
+
cache_system=True,
|
|
75
|
+
)
|
|
76
|
+
response = llm.complete(request)
|
|
77
|
+
if trace_writer is not None:
|
|
78
|
+
trace_writer.write(
|
|
79
|
+
llm_call_span(
|
|
80
|
+
request,
|
|
81
|
+
response,
|
|
82
|
+
operation="chat",
|
|
83
|
+
name=f"annotate {table.table}",
|
|
84
|
+
trace_id=trace_id or new_trace_id(),
|
|
85
|
+
parent_span_id=parent_span_id,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
return response.parse(TableAnnotation), response
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def apply_annotation(table: TableSemantics, annotation: TableAnnotation) -> TableSemantics:
|
|
92
|
+
"""Merge drafted descriptions/synonyms onto a table, preserving its profile.
|
|
93
|
+
|
|
94
|
+
Only ``description`` and ``synonyms`` are written (the durable authoring fields);
|
|
95
|
+
profiling stays as profiled. Columns are matched by name; unknown names are ignored.
|
|
96
|
+
"""
|
|
97
|
+
by_name = {c.name: c for c in annotation.columns}
|
|
98
|
+
columns = [
|
|
99
|
+
col.model_copy(
|
|
100
|
+
update={
|
|
101
|
+
"description": by_name[col.name].description,
|
|
102
|
+
"synonyms": by_name[col.name].synonyms,
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
if col.name in by_name
|
|
106
|
+
else col
|
|
107
|
+
for col in table.columns
|
|
108
|
+
]
|
|
109
|
+
return table.model_copy(
|
|
110
|
+
update={
|
|
111
|
+
"description": annotation.description,
|
|
112
|
+
"synonyms": annotation.synonyms,
|
|
113
|
+
"columns": columns,
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _render_for_annotation(table: TableSemantics) -> str:
|
|
119
|
+
"""Expose the raw profile so the model grounds its descriptions in data."""
|
|
120
|
+
lines = [f"Table: {table.table}", "", "Columns (name, type, profile):"]
|
|
121
|
+
for col in table.columns:
|
|
122
|
+
lines.append(f"- {col.name} ({col.type}){_profile_summary(col)}")
|
|
123
|
+
if table.joins:
|
|
124
|
+
lines.append("")
|
|
125
|
+
lines.append("Joins:")
|
|
126
|
+
for join in table.joins:
|
|
127
|
+
lines.append(f"- {join.type} -> {join.to} ON {join.on}")
|
|
128
|
+
lines.append("")
|
|
129
|
+
lines.append(
|
|
130
|
+
"Draft a table description (what one row is) plus a description and synonyms for "
|
|
131
|
+
"every column, each with a confidence. Return them all."
|
|
132
|
+
)
|
|
133
|
+
return "\n".join(lines)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _profile_summary(col: Column) -> str:
|
|
137
|
+
if col.sample_values:
|
|
138
|
+
return f" values: {', '.join(str(v) for v in col.sample_values)}"
|
|
139
|
+
if isinstance(col.profile, Profile):
|
|
140
|
+
p = col.profile
|
|
141
|
+
bits = []
|
|
142
|
+
if p.nulls is not None:
|
|
143
|
+
bits.append(f"nulls={p.nulls}")
|
|
144
|
+
if p.distinct is not None:
|
|
145
|
+
bits.append(f"distinct={p.distinct}")
|
|
146
|
+
if p.min is not None and p.max is not None:
|
|
147
|
+
bits.append(f"range={p.min}..{p.max}")
|
|
148
|
+
if bits:
|
|
149
|
+
return " " + ", ".join(bits)
|
|
150
|
+
return ""
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""The attention router + readiness scorer (spec §3 #9, §5.5, plan 6.2).
|
|
2
|
+
|
|
3
|
+
Given a bag of scored :class:`Decision`s, this:
|
|
4
|
+
|
|
5
|
+
1. **auto-applies** the high-confidence, machine-decidable ones (confidence ≥
|
|
6
|
+
:data:`AUTO_APPLY_THRESHOLD` and not ``requires_human``) — the console shows them
|
|
7
|
+
collapsed/done with one-click undo;
|
|
8
|
+
2. **surfaces the rest** into a single queue **sorted by leverage** (highest first — the
|
|
9
|
+
fewest decisions that move readiness the most);
|
|
10
|
+
3. computes the **readiness signal**: current accuracy with its interval, and how many queued
|
|
11
|
+
decisions it takes to reach the project's target.
|
|
12
|
+
|
|
13
|
+
The adapters turn the artifacts sqbyl already produces — a Coach report, an eval's review
|
|
14
|
+
pile, an orchestrator run's failed units — into decisions, so the queue is populated from
|
|
15
|
+
real work rather than hand-built. Dev-only review machinery, so it lives in ``sqbyl``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
from sqbyl.models.attention import AttentionQueue, Decision, DecisionKind, ReadinessSignal
|
|
23
|
+
from sqbyl.stats import wilson_interval
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from sqbyl.models.coach import CoachReport
|
|
27
|
+
from sqbyl.models.runs import QuestionResult, ScoredRun
|
|
28
|
+
from sqbyl.orchestrator import OrchestratorResult
|
|
29
|
+
|
|
30
|
+
# Default confidence at/above which a machine decision is applied without asking (one-click
|
|
31
|
+
# undo). An unvalidated heuristic, like the judge's DEFAULT_MIN_CONFIDENCE — still needs
|
|
32
|
+
# re-deriving against human accept/reject rates once the console logs them. Overridable per
|
|
33
|
+
# project via ``DefaultsConfig.auto_apply_threshold`` (set 1.0 to require a human on all).
|
|
34
|
+
AUTO_APPLY_THRESHOLD = 0.85
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _leverage_sort_key(d: Decision) -> tuple[float, float, str]:
|
|
38
|
+
"""Highest leverage first; ties broken by *lower* confidence (more ambiguous → look sooner),
|
|
39
|
+
then id for a stable order."""
|
|
40
|
+
return (-d.leverage, d.confidence, d.id)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def route(
|
|
44
|
+
decisions: list[Decision],
|
|
45
|
+
*,
|
|
46
|
+
n_correct: int,
|
|
47
|
+
n: int,
|
|
48
|
+
target: float,
|
|
49
|
+
auto_apply_threshold: float = AUTO_APPLY_THRESHOLD,
|
|
50
|
+
) -> AttentionQueue:
|
|
51
|
+
"""Split ``decisions`` into auto-applied vs. a leverage-sorted queue + a readiness signal.
|
|
52
|
+
|
|
53
|
+
``n_correct``/``n`` are the eval's exact deterministic counts (passed as integers, not a
|
|
54
|
+
reconstructed float, so the interval is honest); ``target`` is the shippable bar from the
|
|
55
|
+
manifest; ``auto_apply_threshold`` gates unsighted apply (the manifest's
|
|
56
|
+
``auto_apply_threshold`` knob — 1.0 requires a human on everything).
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def auto(d: Decision) -> bool:
|
|
60
|
+
return d.auto_applyable and d.confidence >= auto_apply_threshold
|
|
61
|
+
|
|
62
|
+
auto_applied = [d for d in decisions if auto(d)]
|
|
63
|
+
queue = sorted((d for d in decisions if not auto(d)), key=_leverage_sort_key)
|
|
64
|
+
readiness = _readiness(queue, n_correct=n_correct, n=n, target=target)
|
|
65
|
+
return AttentionQueue(auto_applied=auto_applied, queue=queue, readiness=readiness)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _readiness(queue: list[Decision], *, n_correct: int, n: int, target: float) -> ReadinessSignal:
|
|
69
|
+
"""Greedy 'fewest decisions to target': walk the queue high-leverage first, accumulating
|
|
70
|
+
*de-duplicated* estimated leverage until it clears ``target``.
|
|
71
|
+
|
|
72
|
+
Leverage is summed across picked decisions, but two decisions that both claim the same
|
|
73
|
+
question can't fix it twice — so a decision's contribution is capped by the fraction of the
|
|
74
|
+
eval its *newly-covered* questions represent (union, not naive sum). The result is a
|
|
75
|
+
projection built on unverified estimates; :class:`ReadinessSignal` labels it as such."""
|
|
76
|
+
accuracy = n_correct / n if n else 0.0
|
|
77
|
+
low, high = wilson_interval(n_correct, n)
|
|
78
|
+
base = ReadinessSignal(
|
|
79
|
+
accuracy=accuracy, target=target, n=n, accuracy_low=low, accuracy_high=high
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if accuracy >= target:
|
|
83
|
+
base.decisions_to_target = 0
|
|
84
|
+
base.projected = accuracy
|
|
85
|
+
base.reachable = True
|
|
86
|
+
return base
|
|
87
|
+
|
|
88
|
+
projected = accuracy
|
|
89
|
+
count = 0
|
|
90
|
+
covered: set[str] = set() # questions already claimed — never credited twice
|
|
91
|
+
for d in queue:
|
|
92
|
+
if projected >= target:
|
|
93
|
+
break
|
|
94
|
+
if d.leverage <= 0.0:
|
|
95
|
+
# A blocked/judge card (leverage 0) is surfaced but never closes the gap —
|
|
96
|
+
# it's absence-of-result, not a gain (the 6.1 contract on failed units).
|
|
97
|
+
continue
|
|
98
|
+
gain = d.leverage
|
|
99
|
+
if d.affected_questions:
|
|
100
|
+
new_q = [q for q in d.affected_questions if q not in covered]
|
|
101
|
+
if not new_q:
|
|
102
|
+
continue # fully overlaps an earlier pick — contributes nothing new
|
|
103
|
+
gain = min(d.leverage, len(new_q) / n) if n else 0.0
|
|
104
|
+
covered.update(d.affected_questions)
|
|
105
|
+
projected = min(1.0, projected + gain)
|
|
106
|
+
count += 1
|
|
107
|
+
|
|
108
|
+
base.decisions_to_target = count
|
|
109
|
+
base.projected = projected
|
|
110
|
+
base.reachable = projected >= target
|
|
111
|
+
return base
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ── adapters: real artifacts → decisions ────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
_LAYER_TO_KIND: dict[str, DecisionKind] = {
|
|
117
|
+
"example": DecisionKind.example,
|
|
118
|
+
"measure": DecisionKind.measure,
|
|
119
|
+
"synonym": DecisionKind.synonym,
|
|
120
|
+
"named_filter": DecisionKind.named_filter,
|
|
121
|
+
"column_description": DecisionKind.column_description,
|
|
122
|
+
"table_description": DecisionKind.table_description,
|
|
123
|
+
"instruction": DecisionKind.instruction,
|
|
124
|
+
"trusted_asset": DecisionKind.example,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def decisions_from_coach_report(report: CoachReport, *, total: int) -> list[Decision]:
|
|
129
|
+
"""Turn Coach proposals into decisions. Leverage is ``predicted_fixes / total`` (accuracy
|
|
130
|
+
points), an estimate; prose and memorization-risk proposals are forced to require a human
|
|
131
|
+
(they never auto-apply), consistent with the Coach's own ranking (spec §8)."""
|
|
132
|
+
decisions: list[Decision] = []
|
|
133
|
+
for p in report.proposals:
|
|
134
|
+
leverage = (p.predicted_fixes / total) if total else 0.0
|
|
135
|
+
# Prose (the last-resort layer) and single-question memorization-risk examples never
|
|
136
|
+
# auto-apply — the schema owns "is this prose" via CoachProposal.is_prose (invariant 2).
|
|
137
|
+
requires_human = p.is_prose or p.memorization_risk
|
|
138
|
+
decisions.append(
|
|
139
|
+
Decision(
|
|
140
|
+
id=f"coach:{p.id}",
|
|
141
|
+
kind=_LAYER_TO_KIND.get(p.layer.value, DecisionKind.instruction),
|
|
142
|
+
title=p.title,
|
|
143
|
+
detail=p.root_cause,
|
|
144
|
+
suggestion=p.render_diff(),
|
|
145
|
+
confidence=p.confidence,
|
|
146
|
+
leverage=leverage,
|
|
147
|
+
affected_questions=list(p.question_ids),
|
|
148
|
+
source=f"coach:{p.id}",
|
|
149
|
+
target_file=p.target_file,
|
|
150
|
+
requires_human=requires_human,
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
return decisions
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def decisions_from_review_pile(run: ScoredRun) -> list[Decision]:
|
|
157
|
+
"""Turn an eval's ``manual_review`` rows into judge-review cards — always the human's call.
|
|
158
|
+
|
|
159
|
+
These never auto-apply (``requires_human``) and carry no leverage: a review ruling resolves
|
|
160
|
+
a *reported* number, it doesn't teach the agent, so it must not inflate readiness."""
|
|
161
|
+
decisions: list[Decision] = []
|
|
162
|
+
for r in run.results:
|
|
163
|
+
if not r.needs_review:
|
|
164
|
+
continue
|
|
165
|
+
suggestion = r.judge_suggestion.value if r.judge_suggestion is not None else ""
|
|
166
|
+
confidence = _least_judge_confidence(r)
|
|
167
|
+
decisions.append(
|
|
168
|
+
Decision(
|
|
169
|
+
id=f"judge:{run.run_id}:{r.id}",
|
|
170
|
+
kind=DecisionKind.judge_review,
|
|
171
|
+
title=f"Review {r.id}",
|
|
172
|
+
detail=r.question,
|
|
173
|
+
suggestion=suggestion,
|
|
174
|
+
confidence=confidence,
|
|
175
|
+
leverage=0.0,
|
|
176
|
+
affected_questions=[r.id],
|
|
177
|
+
source=f"judge:{r.id}",
|
|
178
|
+
requires_human=True,
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
return decisions
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def decisions_from_outcomes(result: OrchestratorResult[object]) -> list[Decision]:
|
|
185
|
+
"""Turn orchestrator outcomes into cards: a **failed** unit becomes a blocked card the
|
|
186
|
+
human must retry/handle (spec §3 #8) — never auto-applied and carrying no leverage, so it
|
|
187
|
+
surfaces without pretending to be a weak result. Skipped (budget) units aren't cards."""
|
|
188
|
+
decisions: list[Decision] = []
|
|
189
|
+
for o in result.failures:
|
|
190
|
+
decisions.append(
|
|
191
|
+
Decision(
|
|
192
|
+
id=f"blocked:{o.unit.id}",
|
|
193
|
+
kind=DecisionKind.blocked,
|
|
194
|
+
title=o.unit.label or o.unit.id,
|
|
195
|
+
detail=o.error or "unit failed",
|
|
196
|
+
confidence=0.0,
|
|
197
|
+
leverage=0.0,
|
|
198
|
+
source=o.unit.id,
|
|
199
|
+
requires_human=True,
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
return decisions
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _least_judge_confidence(result: QuestionResult) -> float:
|
|
206
|
+
"""The least-sure judge on a row — a rough 'how ambiguous is this' signal for ordering."""
|
|
207
|
+
if not result.judge_verdicts:
|
|
208
|
+
return 0.0
|
|
209
|
+
return min(v.confidence for v in result.judge_verdicts)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""The judge calibration set — ``.sqbyl/calibration.jsonl`` (spec §7, plan 5.2).
|
|
2
|
+
|
|
3
|
+
Every time a human confirms or overrides a judged row in the review console, we append a
|
|
4
|
+
:class:`CalibrationRecord` here: what the judge suggested, what the human decided, and
|
|
5
|
+
whether they agreed. Accumulated, these give the live **judge↔human agreement** score —
|
|
6
|
+
the number that tells a curator how far to trust the judge on rows nobody has reviewed
|
|
7
|
+
(standard inter-rater agreement between the LLM judge and human reviewers).
|
|
8
|
+
|
|
9
|
+
Append-only JSONL: the calibration set is an audit trail, not mutable state, so a review
|
|
10
|
+
is never silently rewritten. This is dev-loop data (it only exists because a human is
|
|
11
|
+
reviewing benchmarks), so the path is derived here rather than in the runtime layout.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from sqbyl.models import CalibrationRecord, JudgeAgreement
|
|
19
|
+
from sqbyl.project import Project
|
|
20
|
+
from sqbyl_runtime.state.layout import SqbylPaths
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def calibration_path(project: Project) -> Path:
|
|
24
|
+
return SqbylPaths(project.root).root / "calibration.jsonl"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_calibration(project: Project) -> list[CalibrationRecord]:
|
|
28
|
+
"""Every recorded review, oldest first. Empty when nothing has been reviewed yet."""
|
|
29
|
+
path = calibration_path(project)
|
|
30
|
+
if not path.exists():
|
|
31
|
+
return []
|
|
32
|
+
records = [
|
|
33
|
+
CalibrationRecord.model_validate_json(line)
|
|
34
|
+
for line in path.read_text().splitlines()
|
|
35
|
+
if line.strip()
|
|
36
|
+
]
|
|
37
|
+
return records
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def append_calibration(project: Project, record: CalibrationRecord) -> None:
|
|
41
|
+
"""Append one review to the calibration trail (creates the file/dir on first write)."""
|
|
42
|
+
path = calibration_path(project)
|
|
43
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
with path.open("a") as fh:
|
|
45
|
+
fh.write(record.model_dump_json() + "\n")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _latest_per_row(records: list[CalibrationRecord]) -> list[CalibrationRecord]:
|
|
49
|
+
"""Keep only the last ruling for each ``(run_id, question_id)``, in first-seen order.
|
|
50
|
+
|
|
51
|
+
A human who re-opens and re-resolves a row appends a fresh record (the JSONL stays an
|
|
52
|
+
append-only audit trail), but only their latest call should count — otherwise a repeated
|
|
53
|
+
click would double-count in the agreement rate and duplicate a few-shot anchor."""
|
|
54
|
+
latest: dict[tuple[str, str], CalibrationRecord] = {}
|
|
55
|
+
for r in records:
|
|
56
|
+
latest[(r.run_id, r.question_id)] = r # later write wins
|
|
57
|
+
return list(latest.values())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def judge_agreement(project: Project, *, split: str | None = None) -> JudgeAgreement:
|
|
61
|
+
"""The live judge↔human agreement, deduped to each row's latest ruling (spec §7).
|
|
62
|
+
|
|
63
|
+
Optionally scoped to one split. Selection-biased by construction — see
|
|
64
|
+
:class:`~sqbyl.models.JudgeAgreement`."""
|
|
65
|
+
records = load_calibration(project)
|
|
66
|
+
if split is not None:
|
|
67
|
+
records = [r for r in records if r.split == split]
|
|
68
|
+
return JudgeAgreement.from_records(_latest_per_row(records))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# How many prior human rulings to replay to the judge as few-shot examples. Small: a few
|
|
72
|
+
# concrete anchors coach without ballooning every judge prompt (and cost).
|
|
73
|
+
FEW_SHOT_LIMIT = 3
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def few_shot_examples(
|
|
77
|
+
project: Project, *, split: str, limit: int = FEW_SHOT_LIMIT
|
|
78
|
+
) -> list[CalibrationRecord]:
|
|
79
|
+
"""The most recent human rulings **for one split**, to coach that split's judge (spec §7).
|
|
80
|
+
|
|
81
|
+
Split-scoped so dev rulings never coach the held-out test judge (invariant 3), and deduped
|
|
82
|
+
to each row's latest ruling. Recency, not cherry-picking: the last ``limit`` reviews (both
|
|
83
|
+
agreements and overrides) so the judge sees honest ground-truth anchors, not a
|
|
84
|
+
disagreement-biased sample. Empty until a human has reviewed something on this split — so
|
|
85
|
+
an un-reviewed project's judge prompts, and the CI cassettes, are unchanged."""
|
|
86
|
+
records = [r for r in load_calibration(project) if r.split == split]
|
|
87
|
+
return _latest_per_row(records)[-limit:]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""The synth → review candidate queue on disk (``.sqbyl/candidates.yaml``).
|
|
2
|
+
|
|
3
|
+
``sqbyl synth`` writes survivors here; the review console reads them, shows the executed
|
|
4
|
+
evidence, and on accept promotes a candidate into ``benchmarks/dev.yaml`` (via
|
|
5
|
+
:func:`sqbyl.eval.benchmarks_io.append_to_dev_set`) and marks it accepted. The queue is
|
|
6
|
+
``.sqbyl/`` scratch state — regenerable, gitignored — never a source of truth for the
|
|
7
|
+
golden set itself.
|
|
8
|
+
|
|
9
|
+
This module is deliberately **dev-safe**: it reads and writes only the candidate queue and
|
|
10
|
+
never touches ``test.yaml`` (invariant 3).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from sqbyl.models import Candidate
|
|
18
|
+
from sqbyl.project import Project
|
|
19
|
+
from sqbyl.yamlio import dump_yaml, load_yaml
|
|
20
|
+
from sqbyl_runtime.state.layout import SqbylPaths
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def candidates_path(project: Project) -> Path:
|
|
24
|
+
return SqbylPaths(project.root).root / "candidates.yaml"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_candidates(project: Project) -> list[Candidate]:
|
|
28
|
+
path = candidates_path(project)
|
|
29
|
+
if not path.exists():
|
|
30
|
+
return []
|
|
31
|
+
raw = load_yaml(path.read_text()) or []
|
|
32
|
+
return [Candidate.model_validate(item) for item in raw]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def save_candidates(project: Project, candidates: list[Candidate]) -> Path:
|
|
36
|
+
"""Overwrite the queue with ``candidates`` (JSON-native scalars for portability)."""
|
|
37
|
+
path = candidates_path(project)
|
|
38
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
data = [c.model_dump(mode="json") for c in candidates]
|
|
40
|
+
path.write_text(dump_yaml(data))
|
|
41
|
+
return path
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def add_candidates(project: Project, new: list[Candidate]) -> list[Candidate]:
|
|
45
|
+
"""Append newly synthesized candidates to the queue, replacing any with the same id.
|
|
46
|
+
|
|
47
|
+
Returns the full queue after the merge. New candidates win on an id collision so a
|
|
48
|
+
re-synth refreshes stale pending items rather than duplicating them.
|
|
49
|
+
"""
|
|
50
|
+
by_id = {c.id: c for c in load_candidates(project)}
|
|
51
|
+
for candidate in new:
|
|
52
|
+
by_id[candidate.id] = candidate
|
|
53
|
+
merged = list(by_id.values())
|
|
54
|
+
save_candidates(project, merged)
|
|
55
|
+
return merged
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_candidate(project: Project, candidate_id: str) -> Candidate | None:
|
|
59
|
+
return next((c for c in load_candidates(project) if c.id == candidate_id), None)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def update_candidate(project: Project, candidate: Candidate) -> None:
|
|
63
|
+
"""Persist an updated candidate (matched by id) back into the queue."""
|
|
64
|
+
candidates = load_candidates(project)
|
|
65
|
+
replaced = [candidate if c.id == candidate.id else c for c in candidates]
|
|
66
|
+
save_candidates(project, replaced)
|