sqbyl-runtime 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_runtime-0.1.0/.gitignore +44 -0
- sqbyl_runtime-0.1.0/PKG-INFO +57 -0
- sqbyl_runtime-0.1.0/README.md +17 -0
- sqbyl_runtime-0.1.0/pyproject.toml +56 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/__init__.py +40 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/context.py +319 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/cost.py +265 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/db/__init__.py +49 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/db/connection.py +172 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/db/dialects.py +329 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/db/errors.py +35 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/db/guard.py +99 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/export.py +230 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/fingerprint.py +154 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/llm/__init__.py +43 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/llm/_errors.py +30 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/llm/anthropic_client.py +141 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/llm/base.py +177 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/llm/factory.py +40 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/llm/mock.py +74 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/llm/openai_client.py +204 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/llm/replay.py +73 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/models/__init__.py +50 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/models/assets.py +36 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/models/base.py +23 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/models/judges.py +19 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/models/release.py +109 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/models/selection.py +26 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/models/semantics.py +100 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/pipeline.py +288 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/py.typed +0 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/runtime.py +202 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/schema.py +41 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/selection.py +348 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/state/__init__.py +26 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/state/layout.py +46 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/state/traces.py +127 -0
- sqbyl_runtime-0.1.0/src/sqbyl_runtime/state/usage.py +164 -0
|
@@ -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
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqbyl-runtime
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The minimal, dependency-light sqbyl runtime: load a release + ask() + structured logging.
|
|
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,llm,openai,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: duckdb-engine>=0.13
|
|
21
|
+
Requires-Dist: duckdb>=1.0
|
|
22
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
23
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
24
|
+
Requires-Dist: sqlglot>=25
|
|
25
|
+
Provides-Extra: anthropic
|
|
26
|
+
Requires-Dist: anthropic>=0.116.0; extra == 'anthropic'
|
|
27
|
+
Provides-Extra: bigquery
|
|
28
|
+
Requires-Dist: sqlalchemy-bigquery>=1.9; extra == 'bigquery'
|
|
29
|
+
Provides-Extra: langchain
|
|
30
|
+
Requires-Dist: langchain-core>=0.3; extra == 'langchain'
|
|
31
|
+
Provides-Extra: mysql
|
|
32
|
+
Requires-Dist: pymysql>=1.1; extra == 'mysql'
|
|
33
|
+
Provides-Extra: openai
|
|
34
|
+
Requires-Dist: openai>=1.40; extra == 'openai'
|
|
35
|
+
Provides-Extra: postgres
|
|
36
|
+
Requires-Dist: psycopg[binary]>=3.3.4; extra == 'postgres'
|
|
37
|
+
Provides-Extra: snowflake
|
|
38
|
+
Requires-Dist: snowflake-sqlalchemy>=1.5; extra == 'snowflake'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# sqbyl-runtime
|
|
42
|
+
|
|
43
|
+
The minimal, dependency-light runtime for [sqbyl](https://github.com/jackwerner/sqbyl).
|
|
44
|
+
|
|
45
|
+
Embed a released sqbyl agent into your production app with three lines:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from sqbyl_runtime import load
|
|
49
|
+
|
|
50
|
+
agent = load("revenue-analytics.v1.json", db=env.DATABASE_URL, model="claude-opus-4-8")
|
|
51
|
+
agent.ask("net revenue last month") # → {plan, sql, rows, used_assets, usage, latency}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This package contains **only** release `load()` + `ask()` + structured logging.
|
|
55
|
+
None of the dev toolkit (eval, synth, Coach, judges, review console) ships here —
|
|
56
|
+
that all lives in the full `sqbyl` package. The one-way dependency arrow
|
|
57
|
+
(`sqbyl` → `sqbyl-runtime`, never the reverse) is enforced in CI.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# sqbyl-runtime
|
|
2
|
+
|
|
3
|
+
The minimal, dependency-light runtime for [sqbyl](https://github.com/jackwerner/sqbyl).
|
|
4
|
+
|
|
5
|
+
Embed a released sqbyl agent into your production app with three lines:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from sqbyl_runtime import load
|
|
9
|
+
|
|
10
|
+
agent = load("revenue-analytics.v1.json", db=env.DATABASE_URL, model="claude-opus-4-8")
|
|
11
|
+
agent.ask("net revenue last month") # → {plan, sql, rows, used_assets, usage, latency}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This package contains **only** release `load()` + `ask()` + structured logging.
|
|
15
|
+
None of the dev toolkit (eval, synth, Coach, judges, review console) ships here —
|
|
16
|
+
that all lives in the full `sqbyl` package. The one-way dependency arrow
|
|
17
|
+
(`sqbyl` → `sqbyl-runtime`, never the reverse) is enforced in CI.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sqbyl-runtime"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "The minimal, dependency-light sqbyl runtime: load a release + ask() + structured logging."
|
|
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", "openai", "agent"]
|
|
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
|
+
"pydantic>=2.7,<3",
|
|
22
|
+
"sqlalchemy>=2.0",
|
|
23
|
+
"duckdb>=1.0",
|
|
24
|
+
"duckdb-engine>=0.13",
|
|
25
|
+
"sqlglot>=25",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
# Provider SDKs are optional so the runtime is provider-neutral: pick one and install its
|
|
30
|
+
# extra, e.g. `pip install sqbyl-runtime[openai]`. A bare install talks to no provider until
|
|
31
|
+
# you choose. The client for each raises a matching install hint if its SDK is missing.
|
|
32
|
+
anthropic = ["anthropic>=0.116.0"]
|
|
33
|
+
openai = ["openai>=1.40"]
|
|
34
|
+
# Dialect drivers are optional too, so the minimal runtime stays light. DuckDB is the default
|
|
35
|
+
# (a hard dep above); SQLite needs no driver (Python stdlib). Install the extra for the
|
|
36
|
+
# dialect you target, e.g. `pip install sqbyl-runtime[postgres]`. The dialect seam raises
|
|
37
|
+
# a matching install hint when a driver is missing (db/dialects.py).
|
|
38
|
+
postgres = ["psycopg[binary]>=3.3.4"]
|
|
39
|
+
mysql = ["pymysql>=1.1"]
|
|
40
|
+
snowflake = ["snowflake-sqlalchemy>=1.5"]
|
|
41
|
+
bigquery = ["sqlalchemy-bigquery>=1.9"]
|
|
42
|
+
# Export a release as a LangChain tool (sqbyl_runtime.langchain_tool). Optional — the MCP
|
|
43
|
+
# and plain-callable export shapes need no extra dependency.
|
|
44
|
+
langchain = ["langchain-core>=0.3"]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/jackwerner/sqbyl"
|
|
48
|
+
Repository = "https://github.com/jackwerner/sqbyl"
|
|
49
|
+
Issues = "https://github.com/jackwerner/sqbyl/issues"
|
|
50
|
+
|
|
51
|
+
[build-system]
|
|
52
|
+
requires = ["hatchling"]
|
|
53
|
+
build-backend = "hatchling.build"
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.wheel]
|
|
56
|
+
packages = ["src/sqbyl_runtime"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""sqbyl-runtime — the minimal, shippable sqbyl runtime.
|
|
2
|
+
|
|
3
|
+
Contains only what a production app needs to embed a released agent: release
|
|
4
|
+
``load()``, ``ask()``, the ``LLMClient`` seam, and structured logging. None of
|
|
5
|
+
the dev toolkit (eval, synth, Coach, judges, console) lives here or is importable
|
|
6
|
+
from here — that one-way dependency arrow is enforced by import-linter in CI.
|
|
7
|
+
|
|
8
|
+
from sqbyl_runtime import load
|
|
9
|
+
agent = load("revenue-analytics.v3.json", db=DATABASE_URL, model="claude-opus-4-8")
|
|
10
|
+
agent.ask("How many orders shipped last month?") # → AgentResult
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from sqbyl_runtime.export import (
|
|
14
|
+
McpServer,
|
|
15
|
+
answer_dict,
|
|
16
|
+
as_callable,
|
|
17
|
+
langchain_tool,
|
|
18
|
+
serve_mcp_stdio,
|
|
19
|
+
)
|
|
20
|
+
from sqbyl_runtime.runtime import (
|
|
21
|
+
Agent,
|
|
22
|
+
ModelMismatchWarning,
|
|
23
|
+
SchemaMismatchWarning,
|
|
24
|
+
load,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__version__ = "0.0.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Agent",
|
|
31
|
+
"McpServer",
|
|
32
|
+
"ModelMismatchWarning",
|
|
33
|
+
"SchemaMismatchWarning",
|
|
34
|
+
"answer_dict",
|
|
35
|
+
"as_callable",
|
|
36
|
+
"langchain_tool",
|
|
37
|
+
"load",
|
|
38
|
+
"serve_mcp_stdio",
|
|
39
|
+
"__version__",
|
|
40
|
+
]
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""The context compiler (spec §5 steps 1-2, plan 2.1).
|
|
2
|
+
|
|
3
|
+
Turn a project's knowledge (semantics + examples + trusted assets + instructions)
|
|
4
|
+
plus a question into the prompt the agent runtime sends to Claude. Two halves:
|
|
5
|
+
|
|
6
|
+
* a **stable system block** — instructions, annotated schema/semantics, trusted
|
|
7
|
+
assets, and few-shot examples. It does not vary with the question, so it is the
|
|
8
|
+
prompt-cache unit (``cache_system=True`` downstream).
|
|
9
|
+
* a **question turn** — the one varying part, sent as the user message.
|
|
10
|
+
|
|
11
|
+
This lives in ``sqbyl-runtime`` because ``ask()`` compiles context at inference
|
|
12
|
+
time (the same compiler runs in dev and in a shipped release). The small-project
|
|
13
|
+
path is "include everything"; large-schema shortlisting (Phase 9) runs as a
|
|
14
|
+
separate :mod:`~sqbyl_runtime.selection` step *before* this renderer and hands the
|
|
15
|
+
narrowed table set in — so the renderer itself stays a pure function of its inputs
|
|
16
|
+
(deterministic, snapshot-testable): no clocks, no IDs, no LLM calls, stable ordering.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
|
|
23
|
+
from pydantic import BaseModel, Field
|
|
24
|
+
|
|
25
|
+
from sqbyl_runtime.llm.base import LLMClient, Usage
|
|
26
|
+
from sqbyl_runtime.models import (
|
|
27
|
+
Column,
|
|
28
|
+
Dialect,
|
|
29
|
+
Example,
|
|
30
|
+
Profile,
|
|
31
|
+
ReleaseArtifact,
|
|
32
|
+
ScalarBound,
|
|
33
|
+
SelectionConfig,
|
|
34
|
+
TableSemantics,
|
|
35
|
+
TrustedAsset,
|
|
36
|
+
)
|
|
37
|
+
from sqbyl_runtime.selection import LLMCallHook, ValueMatch, select_context
|
|
38
|
+
|
|
39
|
+
# Past this many tables "include everything" stops being viable and Phase 9's
|
|
40
|
+
# LLM/lexical shortlisting is needed; until then we still include all but flag it.
|
|
41
|
+
_INCLUDE_ALL_TABLE_LIMIT = 30
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CompiledContext(BaseModel):
|
|
45
|
+
"""The compiled prompt plus what went into it (for citation + caching).
|
|
46
|
+
|
|
47
|
+
``usage`` carries any tokens the *selection* step spent (LLM shortlisting on a
|
|
48
|
+
large schema); the pipeline folds it into the run's total so it's metered and
|
|
49
|
+
budgeted like generation (invariant 5). It is ``Usage()`` for the include-all /
|
|
50
|
+
lexical paths, which spend nothing.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
system: str
|
|
54
|
+
user: str
|
|
55
|
+
selected_tables: list[str] = Field(default_factory=list)
|
|
56
|
+
offered_assets: list[str] = Field(default_factory=list)
|
|
57
|
+
notes: list[str] = Field(default_factory=list)
|
|
58
|
+
usage: Usage = Field(default_factory=Usage)
|
|
59
|
+
# The selection strategy that actually ran (rewritten to ``include_all`` on a fallback)
|
|
60
|
+
# and whether a narrowing strategy degraded to include-all — carried out so the pipeline
|
|
61
|
+
# can put the drop rationale on the run's trace/result (invariant 7, transparency).
|
|
62
|
+
selection_strategy: str = "include_all"
|
|
63
|
+
selection_fell_back: bool = False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ProjectKnowledge(BaseModel):
|
|
67
|
+
"""Everything the agent reasons over, decoupled from where it came from.
|
|
68
|
+
|
|
69
|
+
Both a dev project (loaded from files) and a shipped ``ReleaseArtifact`` produce
|
|
70
|
+
one of these, so the runtime pipeline is identical in dev and in production.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
dialect: Dialect
|
|
74
|
+
semantics: list[TableSemantics] = Field(default_factory=list)
|
|
75
|
+
instructions: str = ""
|
|
76
|
+
examples: list[Example] = Field(default_factory=list)
|
|
77
|
+
trusted_assets: list[TrustedAsset] = Field(default_factory=list)
|
|
78
|
+
selection: SelectionConfig = Field(default_factory=SelectionConfig)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_release(cls, release: ReleaseArtifact) -> ProjectKnowledge:
|
|
82
|
+
return cls(
|
|
83
|
+
dialect=release.dialect,
|
|
84
|
+
semantics=release.semantics,
|
|
85
|
+
instructions=release.instructions,
|
|
86
|
+
examples=release.examples,
|
|
87
|
+
trusted_assets=release.trusted_assets,
|
|
88
|
+
selection=release.selection,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def compile(
|
|
92
|
+
self,
|
|
93
|
+
question: str,
|
|
94
|
+
*,
|
|
95
|
+
llm: LLMClient | None = None,
|
|
96
|
+
model: str | None = None,
|
|
97
|
+
on_llm_call: LLMCallHook | None = None,
|
|
98
|
+
) -> CompiledContext:
|
|
99
|
+
"""Select the relevant subset (LLM/lexical for large schemas) then render it.
|
|
100
|
+
|
|
101
|
+
``llm`` and ``model`` power the ``llm``/``llm_lexical`` selection strategies;
|
|
102
|
+
without them (or on the include-all/lexical strategies) selection spends nothing
|
|
103
|
+
and this is a pure function of the project's files. ``on_llm_call`` lets a caller
|
|
104
|
+
(the pipeline) trace the shortlisting call as its own GenAI span.
|
|
105
|
+
"""
|
|
106
|
+
return compile_context(
|
|
107
|
+
question,
|
|
108
|
+
dialect=self.dialect,
|
|
109
|
+
semantics=self.semantics,
|
|
110
|
+
instructions=self.instructions,
|
|
111
|
+
examples=self.examples,
|
|
112
|
+
trusted_assets=self.trusted_assets,
|
|
113
|
+
selection=self.selection,
|
|
114
|
+
llm=llm,
|
|
115
|
+
model=model,
|
|
116
|
+
on_llm_call=on_llm_call,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def compile_context(
|
|
121
|
+
question: str,
|
|
122
|
+
*,
|
|
123
|
+
dialect: Dialect,
|
|
124
|
+
semantics: Sequence[TableSemantics],
|
|
125
|
+
instructions: str = "",
|
|
126
|
+
examples: Sequence[Example] = (),
|
|
127
|
+
trusted_assets: Sequence[TrustedAsset] = (),
|
|
128
|
+
selection: SelectionConfig | None = None,
|
|
129
|
+
llm: LLMClient | None = None,
|
|
130
|
+
model: str | None = None,
|
|
131
|
+
on_llm_call: LLMCallHook | None = None,
|
|
132
|
+
) -> CompiledContext:
|
|
133
|
+
"""Compile the question + project knowledge into a system/user prompt pair.
|
|
134
|
+
|
|
135
|
+
Runs the :mod:`~sqbyl_runtime.selection` step first (include-all for small
|
|
136
|
+
projects; lexical/LLM shortlisting past ``max_tables``), then renders only the
|
|
137
|
+
surviving tables and the examples that reference them, plus any value-match hints.
|
|
138
|
+
"""
|
|
139
|
+
selection = selection or SelectionConfig()
|
|
140
|
+
picked = select_context(
|
|
141
|
+
question,
|
|
142
|
+
semantics=semantics,
|
|
143
|
+
config=selection,
|
|
144
|
+
llm=llm,
|
|
145
|
+
model=model,
|
|
146
|
+
on_llm_call=on_llm_call,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
chosen_names = set(picked.tables)
|
|
150
|
+
selected = [t for t in semantics if t.table in chosen_names]
|
|
151
|
+
notes = list(picked.notes)
|
|
152
|
+
if len(semantics) > _INCLUDE_ALL_TABLE_LIMIT and selection.strategy == "include_all":
|
|
153
|
+
notes.append(
|
|
154
|
+
f"{len(semantics)} tables exceeds the include-everything limit "
|
|
155
|
+
f"({_INCLUDE_ALL_TABLE_LIMIT}); set selection.strategy to lexical/llm to narrow"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# When selection narrowed the schema, keep only examples that reference a surviving
|
|
159
|
+
# table (an example about a dropped table is noise for this question). Include-all
|
|
160
|
+
# keeps every example.
|
|
161
|
+
shown_examples = _relevant_examples(examples, selected, narrowed=len(selected) < len(semantics))
|
|
162
|
+
|
|
163
|
+
system = _render_system(
|
|
164
|
+
dialect=dialect,
|
|
165
|
+
instructions=instructions,
|
|
166
|
+
semantics=selected,
|
|
167
|
+
trusted_assets=trusted_assets,
|
|
168
|
+
examples=shown_examples,
|
|
169
|
+
value_matches=picked.value_matches,
|
|
170
|
+
)
|
|
171
|
+
return CompiledContext(
|
|
172
|
+
system=system,
|
|
173
|
+
user=_render_question(question),
|
|
174
|
+
selected_tables=[t.table for t in selected],
|
|
175
|
+
offered_assets=[a.name for a in trusted_assets],
|
|
176
|
+
notes=notes,
|
|
177
|
+
usage=picked.usage,
|
|
178
|
+
selection_strategy=picked.strategy,
|
|
179
|
+
selection_fell_back=picked.fell_back,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _relevant_examples(
|
|
184
|
+
examples: Sequence[Example], selected: Sequence[TableSemantics], *, narrowed: bool
|
|
185
|
+
) -> list[Example]:
|
|
186
|
+
"""Keep examples whose SQL names a selected table; on include-all keep them all.
|
|
187
|
+
|
|
188
|
+
Best-effort substring match on table names — an example that mentions none of the
|
|
189
|
+
surviving tables is dropped only when we actually narrowed, so a project that never
|
|
190
|
+
narrows behaves exactly as before.
|
|
191
|
+
"""
|
|
192
|
+
if not narrowed:
|
|
193
|
+
return list(examples)
|
|
194
|
+
names = [t.table for t in selected]
|
|
195
|
+
kept = [ex for ex in examples if any(_names_table(ex.sql, name) for name in names)]
|
|
196
|
+
# Never strip every example to zero on a narrow — keep the originals if none match,
|
|
197
|
+
# since a few-shot example is cheap and dropping all of them helps nothing.
|
|
198
|
+
return kept or list(examples)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _names_table(sql: str, table: str) -> bool:
|
|
202
|
+
"""Whether ``sql`` references ``table`` as a whole word (case-insensitive)."""
|
|
203
|
+
import re
|
|
204
|
+
|
|
205
|
+
return re.search(rf"\b{re.escape(table)}\b", sql, re.IGNORECASE) is not None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# --- rendering (deterministic) --------------------------------------------------
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _render_system(
|
|
212
|
+
*,
|
|
213
|
+
dialect: Dialect,
|
|
214
|
+
instructions: str,
|
|
215
|
+
semantics: Sequence[TableSemantics],
|
|
216
|
+
trusted_assets: Sequence[TrustedAsset],
|
|
217
|
+
examples: Sequence[Example],
|
|
218
|
+
value_matches: Sequence[ValueMatch] = (),
|
|
219
|
+
) -> str:
|
|
220
|
+
blocks: list[str] = [
|
|
221
|
+
f"You are a careful {dialect.value} SQL analyst. Answer questions by writing a "
|
|
222
|
+
"single read-only SELECT, grounded in the semantic layer below. Prefer measures, "
|
|
223
|
+
"filters, and trusted assets over ad-hoc arithmetic.",
|
|
224
|
+
]
|
|
225
|
+
if instructions.strip():
|
|
226
|
+
# Rendered verbatim: instructions.md is author-owned markdown (it brings its
|
|
227
|
+
# own headings), so we don't wrap it in another.
|
|
228
|
+
blocks.append(instructions.strip())
|
|
229
|
+
blocks.append(_render_tables(semantics))
|
|
230
|
+
if value_matches:
|
|
231
|
+
blocks.append(_render_value_matches(value_matches))
|
|
232
|
+
if trusted_assets:
|
|
233
|
+
blocks.append(_render_trusted_assets(trusted_assets))
|
|
234
|
+
if examples:
|
|
235
|
+
blocks.append(_render_examples(examples))
|
|
236
|
+
return "\n\n".join(blocks).strip() + "\n"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _render_value_matches(value_matches: Sequence[ValueMatch]) -> str:
|
|
240
|
+
"""Grounding hints mapping question literals to canonical declared values (§5.1)."""
|
|
241
|
+
lines = ["# Value hints (question terms mapped to declared column values)"]
|
|
242
|
+
for match in value_matches:
|
|
243
|
+
lines.append(f'- "{match.term}" → {match.table}.{match.column} = {match.value!r}')
|
|
244
|
+
return "\n".join(lines)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _render_tables(semantics: Sequence[TableSemantics]) -> str:
|
|
248
|
+
lines = ["# Schema"]
|
|
249
|
+
for table in semantics:
|
|
250
|
+
header = f"## {table.table}"
|
|
251
|
+
if table.description:
|
|
252
|
+
header += f" — {table.description}"
|
|
253
|
+
lines.append(header)
|
|
254
|
+
if table.synonyms:
|
|
255
|
+
lines.append(f"synonyms: {', '.join(table.synonyms)}")
|
|
256
|
+
lines.append("columns:")
|
|
257
|
+
for col in table.columns:
|
|
258
|
+
lines.append(_render_column(col))
|
|
259
|
+
for join in table.joins:
|
|
260
|
+
conf = "" if join.confidence is None else f" [confidence {join.confidence:.2f}]"
|
|
261
|
+
lines.append(f"join: {join.type} -> {join.to} ON {join.on}{conf}")
|
|
262
|
+
for measure in table.measures:
|
|
263
|
+
desc = f" — {measure.description}" if measure.description else ""
|
|
264
|
+
lines.append(f"measure {measure.name}: {measure.sql}{desc}")
|
|
265
|
+
for filt in table.filters:
|
|
266
|
+
desc = f" — {filt.description}" if filt.description else ""
|
|
267
|
+
lines.append(f"filter {filt.name}: {filt.sql}{desc}")
|
|
268
|
+
return "\n".join(lines)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _render_column(col: Column) -> str:
|
|
272
|
+
parts = [f"- {col.name} ({col.type})"]
|
|
273
|
+
if col.description:
|
|
274
|
+
parts.append(f" — {col.description}")
|
|
275
|
+
if col.synonyms:
|
|
276
|
+
parts.append(f" [synonyms: {', '.join(col.synonyms)}]")
|
|
277
|
+
hint = _profile_hint(col.profile, col.sample_values)
|
|
278
|
+
if hint:
|
|
279
|
+
parts.append(f" {hint}")
|
|
280
|
+
return "".join(parts)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _profile_hint(profile: Profile | bool | None, sample_values: list[ScalarBound] | None) -> str:
|
|
284
|
+
"""A compact grounding hint a human would eyeball: range or representative values."""
|
|
285
|
+
if sample_values:
|
|
286
|
+
shown = ", ".join(str(v) for v in sample_values)
|
|
287
|
+
return f"[values: {shown}]"
|
|
288
|
+
if isinstance(profile, Profile) and profile.min is not None and profile.max is not None:
|
|
289
|
+
return f"[range: {profile.min}..{profile.max}]"
|
|
290
|
+
return ""
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _render_trusted_assets(assets: Sequence[TrustedAsset]) -> str:
|
|
294
|
+
lines = ["# Trusted assets (prefer these over ad-hoc SQL; cite the one you use)"]
|
|
295
|
+
for asset in assets:
|
|
296
|
+
params = ", ".join(f"{p.name} {p.type}" for p in asset.params)
|
|
297
|
+
header = f"## {asset.name}({params})"
|
|
298
|
+
if asset.description:
|
|
299
|
+
header += f" — {asset.description}"
|
|
300
|
+
lines.append(header)
|
|
301
|
+
lines.append(asset.sql.strip())
|
|
302
|
+
return "\n".join(lines)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _render_examples(examples: Sequence[Example]) -> str:
|
|
306
|
+
lines = ["# Examples"]
|
|
307
|
+
for ex in examples:
|
|
308
|
+
lines.append(f"Q: {ex.question}")
|
|
309
|
+
lines.append("SQL:")
|
|
310
|
+
lines.append(ex.sql.strip())
|
|
311
|
+
return "\n".join(lines)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _render_question(question: str) -> str:
|
|
315
|
+
return (
|
|
316
|
+
f"Question: {question.strip()}\n\n"
|
|
317
|
+
"Write a single read-only SELECT that answers it. Think briefly about which "
|
|
318
|
+
"tables, measures, and trusted assets apply, then give the SQL."
|
|
319
|
+
)
|