atif-sql 0.2.0__py3-none-any.whl
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.
- atif_analytics/__init__.py +9 -0
- atif_analytics/application/__init__.py +3 -0
- atif_analytics/application/analyze.py +158 -0
- atif_analytics/application/prompts.py +750 -0
- atif_analytics/application/use_cases/__init__.py +3 -0
- atif_analytics/application/use_cases/_shared.py +264 -0
- atif_analytics/application/use_cases/classify.py +430 -0
- atif_analytics/application/use_cases/conflicts.py +448 -0
- atif_analytics/application/use_cases/friction.py +634 -0
- atif_analytics/application/use_cases/perceived.py +451 -0
- atif_analytics/domain/__init__.py +3 -0
- atif_analytics/domain/authorship.py +166 -0
- atif_analytics/domain/config.py +30 -0
- atif_analytics/domain/costs.py +50 -0
- atif_analytics/domain/friction.py +117 -0
- atif_analytics/domain/layout.py +94 -0
- atif_analytics/domain/models.py +385 -0
- atif_analytics/domain/ports.py +100 -0
- atif_analytics/domain/transcript.py +359 -0
- atif_analytics/infrastructure/__init__.py +3 -0
- atif_analytics/infrastructure/corpus_reader.py +546 -0
- atif_analytics/infrastructure/parquet_cache.py +186 -0
- atif_analytics/infrastructure/settings.py +114 -0
- atif_analytics/infrastructure/sqlite_state/__init__.py +3 -0
- atif_analytics/infrastructure/sqlite_state/checkpointer.py +326 -0
- atif_analytics/infrastructure/sqlite_state/retry_queue.py +296 -0
- atif_analytics/py.typed +0 -0
- atif_cli/__init__.py +3 -0
- atif_cli/__main__.py +8 -0
- atif_cli/app.py +2459 -0
- atif_cli/converter_adapter.py +151 -0
- atif_cli/corpus.py +409 -0
- atif_cli/cron.py +311 -0
- atif_cli/duck_errors.py +97 -0
- atif_cli/embed_lake.py +101 -0
- atif_cli/errors.py +89 -0
- atif_cli/lake.py +373 -0
- atif_cli/lake_sessions.py +270 -0
- atif_cli/output.py +297 -0
- atif_cli/py.typed +0 -0
- atif_converter/__init__.py +3 -0
- atif_converter/application/__init__.py +3 -0
- atif_converter/application/convert_and_audit.py +294 -0
- atif_converter/application/convert_codex.py +210 -0
- atif_converter/domain/__init__.py +9 -0
- atif_converter/domain/agents.py +40 -0
- atif_converter/domain/atif/__init__.py +55 -0
- atif_converter/domain/atif/agent.py +37 -0
- atif_converter/domain/atif/content.py +161 -0
- atif_converter/domain/atif/final_metrics.py +45 -0
- atif_converter/domain/atif/metrics.py +49 -0
- atif_converter/domain/atif/observation.py +21 -0
- atif_converter/domain/atif/observation_result.py +47 -0
- atif_converter/domain/atif/step.py +144 -0
- atif_converter/domain/atif/subagent_trajectory_ref.py +90 -0
- atif_converter/domain/atif/tool_call.py +36 -0
- atif_converter/domain/atif/trajectory.py +211 -0
- atif_converter/domain/atif/trajectory_validator.py +299 -0
- atif_converter/domain/blobs.py +510 -0
- atif_converter/domain/claude_code_conversion.py +1077 -0
- atif_converter/domain/codex_conversion.py +1036 -0
- atif_converter/domain/codex_edges.py +192 -0
- atif_converter/domain/codex_enrichment.py +419 -0
- atif_converter/domain/codex_fidelity.py +124 -0
- atif_converter/domain/codex_nested_calls.py +165 -0
- atif_converter/domain/edges.py +113 -0
- atif_converter/domain/enrichment.py +412 -0
- atif_converter/domain/errors.py +60 -0
- atif_converter/domain/fidelity.py +171 -0
- atif_converter/domain/model_prices.json +8538 -0
- atif_converter/domain/pricing.py +790 -0
- atif_converter/domain/result_signals.py +774 -0
- atif_converter/domain/schema_version.py +90 -0
- atif_converter/domain/session_events.py +408 -0
- atif_converter/infrastructure/__init__.py +3 -0
- atif_converter/infrastructure/census.py +89 -0
- atif_converter/infrastructure/claude_code_converter.py +127 -0
- atif_converter/infrastructure/codex_adapter.py +132 -0
- atif_converter/infrastructure/codex_census.py +100 -0
- atif_converter/infrastructure/codex_converter.py +82 -0
- atif_converter/infrastructure/harbor_adapter.py +187 -0
- atif_converter/infrastructure/raw_records.py +426 -0
- atif_converter/infrastructure/source_archive.py +154 -0
- atif_converter/py.typed +0 -0
- atif_corpus/__init__.py +13 -0
- atif_corpus/application/__init__.py +7 -0
- atif_corpus/application/materialize.py +1826 -0
- atif_corpus/domain/__init__.py +9 -0
- atif_corpus/domain/agents.py +33 -0
- atif_corpus/domain/generation.py +34 -0
- atif_corpus/domain/layout.py +168 -0
- atif_corpus/domain/ports.py +175 -0
- atif_corpus/domain/session_id.py +68 -0
- atif_corpus/domain/sessions.py +228 -0
- atif_corpus/domain/slug.py +72 -0
- atif_corpus/domain/source_layout.py +143 -0
- atif_corpus/domain/watermark.py +76 -0
- atif_corpus/infrastructure/__init__.py +7 -0
- atif_corpus/infrastructure/atomic.py +266 -0
- atif_corpus/infrastructure/compress_artifacts.py +161 -0
- atif_corpus/infrastructure/fake_converter.py +174 -0
- atif_corpus/infrastructure/scanner.py +316 -0
- atif_corpus/infrastructure/settings.py +130 -0
- atif_corpus/infrastructure/source_archive.py +202 -0
- atif_corpus/py.typed +0 -0
- atif_duck/__init__.py +43 -0
- atif_duck/domain/__init__.py +32 -0
- atif_duck/domain/artifacts.py +66 -0
- atif_duck/domain/authorship.py +165 -0
- atif_duck/domain/catalog.py +626 -0
- atif_duck/domain/columnar.py +149 -0
- atif_duck/domain/embedding_guard.py +88 -0
- atif_duck/domain/examples.py +265 -0
- atif_duck/domain/lake.py +427 -0
- atif_duck/domain/lake_sessions.py +97 -0
- atif_duck/domain/raw_readers.py +140 -0
- atif_duck/domain/session_id.py +69 -0
- atif_duck/domain/sql_literal.py +50 -0
- atif_duck/infrastructure/__init__.py +34 -0
- atif_duck/infrastructure/analytics.py +469 -0
- atif_duck/infrastructure/authorship.py +139 -0
- atif_duck/infrastructure/columnar.py +641 -0
- atif_duck/infrastructure/lake.py +1383 -0
- atif_duck/infrastructure/lake_sessions.py +194 -0
- atif_duck/infrastructure/lake_settings.py +63 -0
- atif_duck/infrastructure/lake_steps.py +225 -0
- atif_duck/infrastructure/projections.py +221 -0
- atif_duck/infrastructure/registry.py +2194 -0
- atif_duck/infrastructure/stored_artifacts.py +115 -0
- atif_duck/py.typed +0 -0
- atif_embed/__init__.py +12 -0
- atif_embed/application/__init__.py +3 -0
- atif_embed/application/embed.py +293 -0
- atif_embed/application/prune.py +80 -0
- atif_embed/domain/__init__.py +3 -0
- atif_embed/domain/discovery.py +70 -0
- atif_embed/domain/embedding_guard.py +83 -0
- atif_embed/domain/errors.py +94 -0
- atif_embed/domain/ports.py +223 -0
- atif_embed/domain/sql_literal.py +49 -0
- atif_embed/domain/text_stamp.py +71 -0
- atif_embed/infrastructure/__init__.py +3 -0
- atif_embed/infrastructure/cohere_bedrock.py +433 -0
- atif_embed/infrastructure/corpus_text_rows.py +273 -0
- atif_embed/infrastructure/gemma_local.py +355 -0
- atif_embed/infrastructure/lake_text_rows.py +203 -0
- atif_embed/infrastructure/lance_store.py +440 -0
- atif_embed/infrastructure/providers.py +30 -0
- atif_embed/infrastructure/settings.py +154 -0
- atif_embed/py.typed +0 -0
- atif_models/__init__.py +14 -0
- atif_models/domain/__init__.py +9 -0
- atif_models/domain/ports.py +143 -0
- atif_models/domain/registry.py +151 -0
- atif_models/domain/schema.py +109 -0
- atif_models/infrastructure/__init__.py +8 -0
- atif_models/infrastructure/openai_bedrock.py +314 -0
- atif_models/infrastructure/settings.py +85 -0
- atif_models/py.typed +0 -0
- atif_sql-0.2.0.dist-info/METADATA +272 -0
- atif_sql-0.2.0.dist-info/RECORD +164 -0
- atif_sql-0.2.0.dist-info/WHEEL +4 -0
- atif_sql-0.2.0.dist-info/entry_points.txt +2 -0
- atif_sql-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""The typed columnar artifact contract: names, schema version, column shapes.
|
|
4
|
+
|
|
5
|
+
Beside the JSON artifacts in ``<corpus_root>/sessions/<id>/``, a session
|
|
6
|
+
may carry five Parquet files that hold what the transcript-derived views need,
|
|
7
|
+
already typed, so a query never parses ``trajectory.json``:
|
|
8
|
+
|
|
9
|
+
* ``session.parquet`` one row: the trajectory's top-level members minus
|
|
10
|
+
``steps`` (what the ``sessions`` view reads), keyed by ``session_id_path``.
|
|
11
|
+
* ``steps.parquet`` one row per ATIF step, exactly the ``steps`` view's
|
|
12
|
+
columns.
|
|
13
|
+
* ``tool_calls.parquet`` one row per tool call, exactly the ``tool_calls``
|
|
14
|
+
view's columns.
|
|
15
|
+
* ``tool_results.parquet`` one row per observation result, exactly the
|
|
16
|
+
``tool_results`` view's columns.
|
|
17
|
+
* ``session_events.parquet`` one row per kept non-message record, exactly the
|
|
18
|
+
``session_events`` view's columns (schema 2 onward). Built from the staged
|
|
19
|
+
``session_events.jsonl``, not from the trajectory.
|
|
20
|
+
|
|
21
|
+
atif-duck owns this shape (the views define what the columns mean) and writes
|
|
22
|
+
it through :class:`atif_duck.infrastructure.columnar.ColumnarArtifactProducer`;
|
|
23
|
+
atif-corpus calls that producer through its ``ArtifactProducer`` port without
|
|
24
|
+
knowing these names. The registry reads a session from these files only when
|
|
25
|
+
``meta.json`` carries ``columnar_schema`` equal to
|
|
26
|
+
:data:`COLUMNAR_SCHEMA_VERSION` and all five files are present; otherwise it
|
|
27
|
+
reads the session's ``trajectory.json`` as before, so a corpus materialized
|
|
28
|
+
before this contract existed (or by a build with a different schema version)
|
|
29
|
+
still queries correctly.
|
|
30
|
+
|
|
31
|
+
Pure constants and path arithmetic: no duckdb import, no filesystem access.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from typing import TYPE_CHECKING
|
|
37
|
+
|
|
38
|
+
from atif_duck.domain import artifacts
|
|
39
|
+
from atif_duck.domain.catalog import VIEW_SCHEMA
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
#: Bump when any column, type, or filename below changes. A session whose
|
|
45
|
+
#: ``meta.json`` names another version is read from ``trajectory.json``.
|
|
46
|
+
#:
|
|
47
|
+
#: 2: ``session_events.parquet`` added; ``steps`` gained ``agent_id`` /
|
|
48
|
+
#: ``images``; ``tool_results`` gained ``is_error`` / ``exit_code`` /
|
|
49
|
+
#: ``interrupted`` / ``images``. One bump for both changes: they ship together,
|
|
50
|
+
#: and no corpus was ever stamped with only one of them, so a single version
|
|
51
|
+
#: separates every schema-1 session from every schema-2 one.
|
|
52
|
+
COLUMNAR_SCHEMA_VERSION: int = 2
|
|
53
|
+
|
|
54
|
+
#: The ``meta.json`` key that records which columnar schema a session's
|
|
55
|
+
#: parquet files were written against. Absent on sessions materialized
|
|
56
|
+
#: before the columnar artifacts existed, which is a legal, JSON-read state.
|
|
57
|
+
META_COLUMNAR_KEY: str = "columnar_schema"
|
|
58
|
+
|
|
59
|
+
SESSION_PARQUET: str = "session.parquet"
|
|
60
|
+
STEPS_PARQUET: str = "steps.parquet"
|
|
61
|
+
TOOL_CALLS_PARQUET: str = "tool_calls.parquet"
|
|
62
|
+
TOOL_RESULTS_PARQUET: str = "tool_results.parquet"
|
|
63
|
+
#: Added in schema 2: the ``session_events`` view's rows, built from the staged
|
|
64
|
+
#: ``session_events.jsonl`` the corpus writer puts beside ``edges.jsonl``.
|
|
65
|
+
SESSION_EVENTS_PARQUET: str = "session_events.parquet"
|
|
66
|
+
|
|
67
|
+
#: Every columnar filename, in the order the producer writes them.
|
|
68
|
+
COLUMNAR_FILENAMES: tuple[str, ...] = (
|
|
69
|
+
SESSION_PARQUET,
|
|
70
|
+
STEPS_PARQUET,
|
|
71
|
+
TOOL_CALLS_PARQUET,
|
|
72
|
+
TOOL_RESULTS_PARQUET,
|
|
73
|
+
SESSION_EVENTS_PARQUET,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
#: The JSON artifact the ``session_events`` rows come from (atif-corpus writes
|
|
77
|
+
#: it, compressed on a current corpus: see :mod:`atif_duck.domain.artifacts`).
|
|
78
|
+
SESSION_EVENTS_JSONL: str = artifacts.SESSION_EVENTS_JSONL
|
|
79
|
+
|
|
80
|
+
#: ``session.parquet`` columns. ``session_id_path`` is the session directory
|
|
81
|
+
#: name, the canonical key every view uses as ``session_id``; the rest are the
|
|
82
|
+
#: trajectory's top-level members with the types the JSON reader projects them
|
|
83
|
+
#: to (``atif_duck.infrastructure.registry._TRAJECTORY_COLUMNS``), minus
|
|
84
|
+
#: ``steps``.
|
|
85
|
+
SESSION_COLUMNS: tuple[tuple[str, str], ...] = (
|
|
86
|
+
("session_id_path", "VARCHAR"),
|
|
87
|
+
("schema_version", "VARCHAR"),
|
|
88
|
+
("session_id", "VARCHAR"),
|
|
89
|
+
("trajectory_id", "VARCHAR"),
|
|
90
|
+
("agent", "STRUCT(name VARCHAR, version VARCHAR, model_name VARCHAR, extra JSON)"),
|
|
91
|
+
(
|
|
92
|
+
"final_metrics",
|
|
93
|
+
(
|
|
94
|
+
"STRUCT(total_prompt_tokens BIGINT, total_completion_tokens BIGINT, "
|
|
95
|
+
"total_cached_tokens BIGINT, total_cost_usd DOUBLE, total_steps BIGINT, extra JSON)"
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
("extra", "JSON"),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
#: Column shape per columnar file. The three step-derived files carry exactly
|
|
102
|
+
#: their view's catalog columns, so the catalog stays the one place a column
|
|
103
|
+
#: is declared and the schema drift test covers these files too.
|
|
104
|
+
COLUMNAR_SCHEMAS: dict[str, tuple[tuple[str, str], ...]] = {
|
|
105
|
+
SESSION_PARQUET: SESSION_COLUMNS,
|
|
106
|
+
STEPS_PARQUET: VIEW_SCHEMA["steps"],
|
|
107
|
+
TOOL_CALLS_PARQUET: VIEW_SCHEMA["tool_calls"],
|
|
108
|
+
TOOL_RESULTS_PARQUET: VIEW_SCHEMA["tool_results"],
|
|
109
|
+
SESSION_EVENTS_PARQUET: VIEW_SCHEMA["session_events"],
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#: Smallest byte count a readable parquet file can have (``PAR1`` + footer +
|
|
113
|
+
#: footer length + ``PAR1``). A shorter file is what a crashed writer leaves
|
|
114
|
+
#: behind; the registry treats it as absent and reads the session from JSON.
|
|
115
|
+
MIN_PARQUET_BYTES: int = 16
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def columnar_paths(session_dir: Path) -> tuple[Path, ...]:
|
|
119
|
+
"""The five columnar artifact paths inside one session directory."""
|
|
120
|
+
return tuple(session_dir / name for name in COLUMNAR_FILENAMES)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def is_current_columnar_schema(value: object) -> bool:
|
|
124
|
+
"""True when a ``meta.json`` ``columnar_schema`` value names this build's schema.
|
|
125
|
+
|
|
126
|
+
Anything but the exact integer (absent key, ``None``, a string, another
|
|
127
|
+
version) answers ``False`` and sends the session down the JSON path.
|
|
128
|
+
"""
|
|
129
|
+
return (
|
|
130
|
+
isinstance(value, int) and not isinstance(value, bool) and value == COLUMNAR_SCHEMA_VERSION
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
__all__ = [
|
|
135
|
+
"COLUMNAR_FILENAMES",
|
|
136
|
+
"COLUMNAR_SCHEMAS",
|
|
137
|
+
"COLUMNAR_SCHEMA_VERSION",
|
|
138
|
+
"META_COLUMNAR_KEY",
|
|
139
|
+
"MIN_PARQUET_BYTES",
|
|
140
|
+
"SESSION_COLUMNS",
|
|
141
|
+
"SESSION_EVENTS_JSONL",
|
|
142
|
+
"SESSION_EVENTS_PARQUET",
|
|
143
|
+
"SESSION_PARQUET",
|
|
144
|
+
"STEPS_PARQUET",
|
|
145
|
+
"TOOL_CALLS_PARQUET",
|
|
146
|
+
"TOOL_RESULTS_PARQUET",
|
|
147
|
+
"columnar_paths",
|
|
148
|
+
"is_current_columnar_schema",
|
|
149
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Fail-loud embedding provider/dimension guard for the VSS bind path.
|
|
4
|
+
|
|
5
|
+
DELIBERATE TWIN of ``atif_embed.domain.embedding_guard``: atif-duck may never
|
|
6
|
+
import atif-embed (import-linter independence contract — only atif-cli
|
|
7
|
+
composes), and this is a tiny pure rule (string/int comparison, no I/O), so
|
|
8
|
+
each package carries its own copy rather than growing a shared-kernel package
|
|
9
|
+
for one function. ``test_guard_twin_pin.py`` is what keeps the copies honest.
|
|
10
|
+
The two copies are coupled through the CONTRACT's store-stamp semantics; a
|
|
11
|
+
behavioral change in one must be ported to the other.
|
|
12
|
+
|
|
13
|
+
The read/bind path (``register_vss``) reads the Lance store's stamped
|
|
14
|
+
``(model, dim)`` back and calls :func:`ensure_store_matches` BEFORE binding
|
|
15
|
+
the ``message_embeddings`` view — a store written by a different provider
|
|
16
|
+
raises rather than serving numerically valid but garbage cosine scores.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
#: Recovery instruction appended to the mismatch message. The store lives at
|
|
22
|
+
#: ``<corpus_root>/embeddings_lance`` by default, NOT under a fixed home
|
|
23
|
+
#: directory — naming a path the store isn't at makes the operator delete
|
|
24
|
+
#: nothing and stay broken. Kept identical to atif-embed's twin copy.
|
|
25
|
+
RECOVERY_HINT = (
|
|
26
|
+
"Re-embed under the new provider: rm -rf the Lance store directory "
|
|
27
|
+
"(ATIF_SQL_LANCE_URI, or <corpus-root>/embeddings_lance by default) "
|
|
28
|
+
"and re-run `atif-sql embed --all --no-dry-run` (a bare `atif-sql embed` "
|
|
29
|
+
"exits 64: a real run needs an explicit scope)."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EmbeddingProviderMismatch(Exception): # noqa: N818 — names a store state, not an "*Error"
|
|
34
|
+
"""Raised on a stamped-vs-active ``(model, dim)`` mismatch.
|
|
35
|
+
|
|
36
|
+
The Lance store records the identity of the embedder that wrote it, and it
|
|
37
|
+
differs from the identity of the embedder now asking to read or extend it.
|
|
38
|
+
|
|
39
|
+
The name carries no ``Error`` suffix because it names a STATE OF THE STORE
|
|
40
|
+
that is true before anything is attempted, not a fault that occurred while
|
|
41
|
+
doing something. Nothing has gone wrong at runtime: two consistent stores
|
|
42
|
+
simply cannot be the same store.
|
|
43
|
+
|
|
44
|
+
Different embedding models produce vectors in incompatible spaces, so
|
|
45
|
+
querying across a provider switch yields numerically valid but
|
|
46
|
+
semantically garbage cosine scores. This error is terminal: the store
|
|
47
|
+
must be dropped and re-embedded under the new provider.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def ensure_store_matches(
|
|
52
|
+
*,
|
|
53
|
+
stored_model: str | None,
|
|
54
|
+
stored_dim: int | None,
|
|
55
|
+
expected_model: str,
|
|
56
|
+
expected_dim: int | None,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Fail loud if a store's stamped identity differs from the active embedder.
|
|
59
|
+
|
|
60
|
+
``None`` for either stored value means the store is empty (fresh
|
|
61
|
+
install) and any provider may claim it, so the check is a no-op.
|
|
62
|
+
``model_id`` is the primary identity; ``expected_dim`` is also checked
|
|
63
|
+
when supplied (Cohere's single model id can emit different Matryoshka
|
|
64
|
+
widths). ``expected_dim=None`` trusts ``model_id`` alone.
|
|
65
|
+
"""
|
|
66
|
+
if stored_model is None or stored_dim is None:
|
|
67
|
+
return
|
|
68
|
+
model_ok = stored_model == expected_model
|
|
69
|
+
dim_ok = expected_dim is None or stored_dim == expected_dim
|
|
70
|
+
if model_ok and dim_ok:
|
|
71
|
+
return
|
|
72
|
+
# The message stays INSIDE the raise: `test_guard_twin_pin.py` reads both
|
|
73
|
+
# twin modules as source text and requires `{RECOVERY_HINT}` to appear after
|
|
74
|
+
# the `raise EmbeddingProviderMismatch` keyword, which is what proves the
|
|
75
|
+
# shared constant reaches the operator rather than only sitting in the
|
|
76
|
+
# module. Hoisting the message to a local satisfies EM102 / TRY003 and
|
|
77
|
+
# defeats that pin, so both are suppressed here instead.
|
|
78
|
+
raise EmbeddingProviderMismatch( # noqa: TRY003
|
|
79
|
+
"Embedding store was written by a different provider/model. " # noqa: EM102
|
|
80
|
+
f"stored=(model={stored_model!r}, dim={stored_dim}) "
|
|
81
|
+
f"active=(model={expected_model!r}, dim={expected_dim}). "
|
|
82
|
+
"Vectors from different models live in incompatible spaces (even at "
|
|
83
|
+
"matching dimensions), so mixing them silently corrupts kNN search. "
|
|
84
|
+
f"{RECOVERY_HINT}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = ["RECOVERY_HINT", "EmbeddingProviderMismatch", "ensure_store_matches"]
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Derived, test-executed example queries for the atif-duck SQL surface.
|
|
4
|
+
|
|
5
|
+
Operator's principle: DON'T hardcode example strings — they rot the moment a
|
|
6
|
+
macro signature or view changes. Every example here is DERIVED from the
|
|
7
|
+
static catalogs (:data:`~atif_duck.domain.catalog.VIEW_NAMES`,
|
|
8
|
+
:data:`~atif_duck.domain.catalog.ANALYTICS_VIEW_NAMES`,
|
|
9
|
+
:data:`~atif_duck.domain.catalog.MACRO_SIGNATURES`,
|
|
10
|
+
:data:`~atif_duck.domain.catalog.ANALYTICS_MACRO_SIGNATURES`) plus one small
|
|
11
|
+
exemplar table mapping parameter NAMES to literal SQL argument text
|
|
12
|
+
(:data:`ARG_EXEMPLARS`). The catalogs are already drift-pinned against the
|
|
13
|
+
real DDL by the tests in ``tests/test_duck_views.py`` /
|
|
14
|
+
``tests/test_analytics_views.py``, so the examples inherit that freshness.
|
|
15
|
+
|
|
16
|
+
THE TEST IS THE CONTRACT: ``tests/test_examples.py`` EXECUTES every example
|
|
17
|
+
:func:`build_examples` emits against the fixture corpus (analytics parquets
|
|
18
|
+
registered, tiny Lance store attached). An example that stops running stops
|
|
19
|
+
shipping — the CLI surface (``atif-sql examples``) can honestly claim
|
|
20
|
+
"test-executed against this version".
|
|
21
|
+
|
|
22
|
+
The only hand-maintained prose is
|
|
23
|
+
:data:`~atif_duck.domain.catalog.DESCRIPTIONS` in ``catalog.py``; a coverage
|
|
24
|
+
drift test forces one description per catalog object.
|
|
25
|
+
|
|
26
|
+
Pure domain module: no duckdb import, no I/O — safe on the CLI's lean
|
|
27
|
+
import path (``schema`` / ``examples`` / ``--help``).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from typing import Literal, cast, get_args
|
|
34
|
+
|
|
35
|
+
from atif_duck.domain.catalog import (
|
|
36
|
+
ANALYTICS_MACRO_SIGNATURES,
|
|
37
|
+
ANALYTICS_VIEW_NAMES,
|
|
38
|
+
ANALYTICS_VIEW_SCHEMA,
|
|
39
|
+
DESCRIPTIONS,
|
|
40
|
+
MACRO_SIGNATURES,
|
|
41
|
+
TABLE_MACRO_NAMES,
|
|
42
|
+
VIEW_NAMES,
|
|
43
|
+
VIEW_SCHEMA,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
#: What an example needs registered before it runs: the base corpus views
|
|
47
|
+
#: (``core``), the analytics parquet views (``analytics``), or the
|
|
48
|
+
#: Lance-backed embeddings surface (``vss``).
|
|
49
|
+
Requires = Literal["core", "analytics", "vss"]
|
|
50
|
+
|
|
51
|
+
#: Structural kind, derived from which catalog the object lives in and
|
|
52
|
+
#: whether its DDL is ``AS TABLE`` (see
|
|
53
|
+
#: :data:`~atif_duck.domain.catalog.TABLE_MACRO_NAMES`).
|
|
54
|
+
Category = Literal["view", "table-macro", "scalar-macro"]
|
|
55
|
+
|
|
56
|
+
REQUIRES_VALUES: tuple[str, ...] = cast("tuple[str, ...]", get_args(Requires))
|
|
57
|
+
CATEGORY_VALUES: tuple[str, ...] = cast("tuple[str, ...]", get_args(Category))
|
|
58
|
+
|
|
59
|
+
#: Exemplar SQL argument text per macro parameter NAME. The single seam
|
|
60
|
+
#: between the derived examples and real macro signatures: a new parameter
|
|
61
|
+
#: name lands here (one line) or ``build_examples`` fails loud — and the
|
|
62
|
+
#: executing test in ``tests/test_examples.py`` fails with it.
|
|
63
|
+
#:
|
|
64
|
+
#: * ``sid`` — a real session id, picked from the corpus itself so the
|
|
65
|
+
#: example runs verbatim on ANY materialized corpus.
|
|
66
|
+
#: * ``query_vec`` — a stored embedding L2-NORMALIZED into a unit-norm probe,
|
|
67
|
+
#: matching what ``embed_query`` returns, so the example runs on any corpus
|
|
68
|
+
#: without fabricating a dim-wide literal. The normalization is not
|
|
69
|
+
#: cosmetic: stored vectors are un-normalized, and a RAW stored vector is
|
|
70
|
+
#: its own nearest neighbor under every metric, so an un-normalized probe
|
|
71
|
+
#: would make this example score sim == 1.0 no matter which metric the macro
|
|
72
|
+
#: ranks by — it could not tell a correct macro from a broken one. For TEXT
|
|
73
|
+
#: queries the flow is two-step: embed the query text first (``atif-sql
|
|
74
|
+
#: search`` wraps both steps); see
|
|
75
|
+
#: :data:`~atif_duck.domain.catalog.DESCRIPTIONS`.
|
|
76
|
+
ARG_EXEMPLARS: dict[str, str] = {
|
|
77
|
+
"sid": "(SELECT session_id FROM sessions LIMIT 1)",
|
|
78
|
+
"since_days": "30",
|
|
79
|
+
"last_n_days": "30",
|
|
80
|
+
"k": "10",
|
|
81
|
+
"n": "10",
|
|
82
|
+
"label_name": "'correction'",
|
|
83
|
+
"signal_name": "'correction'",
|
|
84
|
+
"src": "'user'",
|
|
85
|
+
"msg": "'Stop hook feedback: the claim has no evidence'",
|
|
86
|
+
"interval_text": "'7 days'",
|
|
87
|
+
"query_vec": (
|
|
88
|
+
"(SELECT list_transform("
|
|
89
|
+
"me.embedding, v -> (v / sqrt(list_sum("
|
|
90
|
+
"list_transform(me.embedding, x -> x::DOUBLE * x))))::FLOAT"
|
|
91
|
+
") FROM message_embeddings me LIMIT 1)"
|
|
92
|
+
),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#: Catalog objects DELIBERATELY without an example, mapped to the reason.
|
|
96
|
+
#: The coverage drift test treats these as documented exclusions; everything
|
|
97
|
+
#: else in the catalogs MUST yield an example. Currently empty — every view
|
|
98
|
+
#: and macro is exampled — but the seam stays so a future exclusion is a
|
|
99
|
+
#: reviewed one-liner instead of a silent coverage hole.
|
|
100
|
+
EXCLUSIONS: dict[str, str] = {}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True, slots=True)
|
|
104
|
+
class ExampleQuery:
|
|
105
|
+
"""One runnable example query, derived from the catalog.
|
|
106
|
+
|
|
107
|
+
Attributes
|
|
108
|
+
----------
|
|
109
|
+
name
|
|
110
|
+
The catalog object (view or macro) the example demonstrates.
|
|
111
|
+
sql
|
|
112
|
+
A complete, runnable statement for ``atif-sql query``.
|
|
113
|
+
description
|
|
114
|
+
Agent-facing prose from :data:`~atif_duck.domain.catalog.DESCRIPTIONS`.
|
|
115
|
+
requires
|
|
116
|
+
``core`` | ``analytics`` | ``vss`` — what must be registered/populated.
|
|
117
|
+
category
|
|
118
|
+
``view`` | ``table-macro`` | ``scalar-macro``.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
name: str
|
|
122
|
+
sql: str
|
|
123
|
+
description: str
|
|
124
|
+
requires: Requires
|
|
125
|
+
category: Category
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
#: Catalog objects that need the embeddings store (``atif-sql embed``).
|
|
129
|
+
_VSS_OBJECTS: frozenset[str] = frozenset({"message_embeddings", "semantic_search"})
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def object_requires(name: str) -> Requires:
|
|
133
|
+
"""What must be registered/populated before catalog object ``name`` binds.
|
|
134
|
+
|
|
135
|
+
``analytics`` for the parquet-gated analytics views and macros, ``vss``
|
|
136
|
+
for the embeddings surface, ``core`` for everything the corpus alone
|
|
137
|
+
registers (the authorship views included). ``atif-sql schema`` prints
|
|
138
|
+
this beside every view and macro, and each derived example carries it.
|
|
139
|
+
"""
|
|
140
|
+
if name in ANALYTICS_VIEW_SCHEMA or name in ANALYTICS_MACRO_SIGNATURES:
|
|
141
|
+
return "analytics"
|
|
142
|
+
return "vss" if name in _VSS_OBJECTS else "core"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _view_sql(name: str) -> str:
|
|
146
|
+
"""Canonical top-N SELECT for one view.
|
|
147
|
+
|
|
148
|
+
When the static :data:`~atif_duck.domain.catalog.VIEW_SCHEMA` (or
|
|
149
|
+
:data:`~atif_duck.domain.catalog.ANALYTICS_VIEW_SCHEMA`) declares a
|
|
150
|
+
timestamp column, the example orders by the FIRST one descending
|
|
151
|
+
(newest-first is the agent's default question); otherwise a plain LIMIT.
|
|
152
|
+
"""
|
|
153
|
+
schema = VIEW_SCHEMA.get(name) or ANALYTICS_VIEW_SCHEMA.get(name, ())
|
|
154
|
+
ts_col = next((col for col, typ in schema if typ.startswith("TIMESTAMP")), None)
|
|
155
|
+
if ts_col is not None:
|
|
156
|
+
# The view name and its timestamp column both come from the static catalog.
|
|
157
|
+
return f"SELECT * FROM {name} ORDER BY {ts_col} DESC LIMIT 10" # noqa: S608 # nosec B608 - view name and column are VIEW_SCHEMA constants
|
|
158
|
+
return f"SELECT * FROM {name} LIMIT 10" # noqa: S608 # nosec B608 - view name is a VIEW_SCHEMA constant
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _macro_sql(name: str, params: tuple[str, ...]) -> str:
|
|
162
|
+
"""Runnable invocation for one macro, args filled from the exemplars."""
|
|
163
|
+
try:
|
|
164
|
+
args = ", ".join(ARG_EXEMPLARS[param] for param in params)
|
|
165
|
+
except KeyError as exc:
|
|
166
|
+
msg = (
|
|
167
|
+
f"macro {name!r} has parameter {exc.args[0]!r} with no entry in "
|
|
168
|
+
"ARG_EXEMPLARS — add an exemplar literal so build_examples() can "
|
|
169
|
+
"derive a runnable example"
|
|
170
|
+
)
|
|
171
|
+
raise KeyError(msg) from exc
|
|
172
|
+
if name in TABLE_MACRO_NAMES:
|
|
173
|
+
# The macro name comes from MACRO_SIGNATURES and args from ARG_EXEMPLARS.
|
|
174
|
+
return f"SELECT * FROM {name}({args}) LIMIT 10" # noqa: S608 # nosec B608 - macro name from MACRO_SIGNATURES, args from ARG_EXEMPLARS
|
|
175
|
+
return f"SELECT {name}({args}) AS {name}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _description(name: str) -> str:
|
|
179
|
+
"""DESCRIPTIONS lookup that fails loud on a missing catalog object."""
|
|
180
|
+
try:
|
|
181
|
+
return DESCRIPTIONS[name]
|
|
182
|
+
except KeyError as exc:
|
|
183
|
+
msg = (
|
|
184
|
+
f"catalog object {name!r} has no entry in "
|
|
185
|
+
"atif_duck.domain.catalog.DESCRIPTIONS — add one (the drift test "
|
|
186
|
+
"in tests/test_examples.py pins this coverage)"
|
|
187
|
+
)
|
|
188
|
+
raise KeyError(msg) from exc
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def build_examples() -> tuple[ExampleQuery, ...]:
|
|
192
|
+
"""Derive the full example inventory from the catalogs.
|
|
193
|
+
|
|
194
|
+
Order: core views, VSS view, analytics views, then core macros, VSS
|
|
195
|
+
macro, analytics macros — stable, so the CLI listing and the JSON array
|
|
196
|
+
are deterministic across runs.
|
|
197
|
+
"""
|
|
198
|
+
examples: list[ExampleQuery] = []
|
|
199
|
+
|
|
200
|
+
for view in VIEW_NAMES:
|
|
201
|
+
if view in EXCLUSIONS:
|
|
202
|
+
continue
|
|
203
|
+
examples.append(
|
|
204
|
+
ExampleQuery(
|
|
205
|
+
name=view,
|
|
206
|
+
sql=_view_sql(view),
|
|
207
|
+
description=_description(view),
|
|
208
|
+
requires=object_requires(view),
|
|
209
|
+
category="view",
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
for view in ANALYTICS_VIEW_NAMES:
|
|
214
|
+
if view in EXCLUSIONS:
|
|
215
|
+
continue
|
|
216
|
+
examples.append(
|
|
217
|
+
ExampleQuery(
|
|
218
|
+
name=view,
|
|
219
|
+
sql=_view_sql(view),
|
|
220
|
+
description=_description(view),
|
|
221
|
+
requires=object_requires(view),
|
|
222
|
+
category="view",
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
for macro, params in MACRO_SIGNATURES.items():
|
|
227
|
+
if macro in EXCLUSIONS:
|
|
228
|
+
continue
|
|
229
|
+
examples.append(
|
|
230
|
+
ExampleQuery(
|
|
231
|
+
name=macro,
|
|
232
|
+
sql=_macro_sql(macro, params),
|
|
233
|
+
description=_description(macro),
|
|
234
|
+
requires=object_requires(macro),
|
|
235
|
+
category="table-macro" if macro in TABLE_MACRO_NAMES else "scalar-macro",
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
for macro, params in ANALYTICS_MACRO_SIGNATURES.items():
|
|
240
|
+
if macro in EXCLUSIONS:
|
|
241
|
+
continue
|
|
242
|
+
examples.append(
|
|
243
|
+
ExampleQuery(
|
|
244
|
+
name=macro,
|
|
245
|
+
sql=_macro_sql(macro, params),
|
|
246
|
+
description=_description(macro),
|
|
247
|
+
requires=object_requires(macro),
|
|
248
|
+
category="table-macro" if macro in TABLE_MACRO_NAMES else "scalar-macro",
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
return tuple(examples)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
__all__ = [
|
|
256
|
+
"ARG_EXEMPLARS",
|
|
257
|
+
"CATEGORY_VALUES",
|
|
258
|
+
"EXCLUSIONS",
|
|
259
|
+
"REQUIRES_VALUES",
|
|
260
|
+
"Category",
|
|
261
|
+
"ExampleQuery",
|
|
262
|
+
"Requires",
|
|
263
|
+
"build_examples",
|
|
264
|
+
"object_requires",
|
|
265
|
+
]
|