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
atif_duck/domain/lake.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""The DuckLake the corpora are queried through: tables, partitions, schema identity.
|
|
4
|
+
|
|
5
|
+
One lake holds every corpus (both Claude Code corpora and the Codex one). Each
|
|
6
|
+
lake table is the registry's raw reader of the same artifact kind
|
|
7
|
+
(:mod:`atif_duck.domain.raw_readers`) with two identity columns in front,
|
|
8
|
+
``corpus`` (the corpus directory's name) and ``agent`` (the agent the corpus
|
|
9
|
+
holds). The per-session artifacts stay the source of truth: the lake is
|
|
10
|
+
loaded FROM the registry's raw relations over them, so a lake row is by
|
|
11
|
+
construction the row the per-session path would have produced, and the views
|
|
12
|
+
bind over either with no change.
|
|
13
|
+
|
|
14
|
+
Nothing here is declared twice. Every payload column comes from the raw
|
|
15
|
+
reader shapes, which come from the catalog and the columnar contract; the
|
|
16
|
+
only lake-local decisions are the identity columns, the rename rule for a
|
|
17
|
+
source column that collides with one, the derived ``started_at`` the sessions
|
|
18
|
+
table is partitioned by, and the partition specs.
|
|
19
|
+
|
|
20
|
+
Partitioning: every table is partitioned by ``agent`` and ``corpus`` first,
|
|
21
|
+
then by month where a row has a time. ``corpus`` is there because a query is
|
|
22
|
+
scoped to one corpus by default, and a corpus-pure file is what lets that scope
|
|
23
|
+
skip the other corpora's files. It also keeps compaction from merging two
|
|
24
|
+
corpora's rows into one file. ``agent`` is implied by ``corpus`` (one corpus
|
|
25
|
+
holds one agent), so it adds no files, only a readable directory level. The
|
|
26
|
+
two one-row-per-session tables (``loss_reports``, ``session_meta``) have no
|
|
27
|
+
time column and stay at ``agent``/``corpus``: splitting a few thousand small
|
|
28
|
+
rows by month would multiply files without pruning anything a query asks for.
|
|
29
|
+
|
|
30
|
+
Schema identity: the lake records :data:`LAKE_SCHEMA_VERSION`, a digest of
|
|
31
|
+
the rendered table definitions (:func:`lake_schema_digest`), and the columnar
|
|
32
|
+
schema version it was loaded under. Any of the three differing from the
|
|
33
|
+
running code makes the lake STALE: the writer rebuilds it and a reader falls
|
|
34
|
+
back to the per-session path. ``tests/test_lake.py`` pins the digest
|
|
35
|
+
beside the version, so a change to any shape the lake derives from fails until
|
|
36
|
+
someone decides whether it also needs a version bump.
|
|
37
|
+
|
|
38
|
+
Pure constants and string building: no duckdb import, no filesystem access.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import hashlib
|
|
44
|
+
import json
|
|
45
|
+
from dataclasses import dataclass
|
|
46
|
+
|
|
47
|
+
from atif_duck.domain.catalog import VIEW_SCHEMA
|
|
48
|
+
from atif_duck.domain.columnar import COLUMNAR_SCHEMA_VERSION
|
|
49
|
+
from atif_duck.domain.raw_readers import (
|
|
50
|
+
CORPUS_COLUMN,
|
|
51
|
+
EDGE_COLUMNS,
|
|
52
|
+
LOSS_REPORT_COLUMNS,
|
|
53
|
+
LOSS_REPORT_PATH_COLUMN,
|
|
54
|
+
META_COLUMNS,
|
|
55
|
+
RAW_EDGES,
|
|
56
|
+
RAW_LOSS_REPORTS,
|
|
57
|
+
RAW_META,
|
|
58
|
+
RAW_SESSION_EVENTS,
|
|
59
|
+
RAW_STEPS,
|
|
60
|
+
RAW_TOOL_CALLS,
|
|
61
|
+
RAW_TOOL_RESULTS,
|
|
62
|
+
RAW_TRAJECTORIES,
|
|
63
|
+
SESSION_ID_PATH,
|
|
64
|
+
TRAJECTORY_RELATION_COLUMNS,
|
|
65
|
+
)
|
|
66
|
+
from atif_duck.domain.sql_literal import SqlFragment
|
|
67
|
+
|
|
68
|
+
#: Bump when the lake's meaning changes in a way the table definitions do not
|
|
69
|
+
#: show (what a row is loaded from, how a key is derived). A change the
|
|
70
|
+
#: definitions DO show moves :func:`lake_schema_digest` on its own; the pin test
|
|
71
|
+
#: makes the author decide whether it also warrants a bump.
|
|
72
|
+
LAKE_SCHEMA_VERSION: int = 1
|
|
73
|
+
|
|
74
|
+
#: The lake tables whose rows keep their source order. A step's tool calls
|
|
75
|
+
#: and results carry no ordinal, so their order within a step is the order the
|
|
76
|
+
#: writer inserted them in, which DuckLake keeps as ``rowid``. A parallel
|
|
77
|
+
#: partitioned insert interleaves rows from its threads (measured: results
|
|
78
|
+
#: of one step out of order in a few sessions per hundred), so the writer
|
|
79
|
+
#: inserts these tables on one thread. Part of :func:`lake_schema_digest`: a
|
|
80
|
+
#: lake written without the guarantee reads back misordered, so it is stale.
|
|
81
|
+
SOURCE_ORDERED_TABLES: tuple[str, ...] = ("steps", "tool_calls", "tool_results")
|
|
82
|
+
|
|
83
|
+
#: Parquet row group size for every lake file. DuckDB's default (122,880 rows)
|
|
84
|
+
#: puts a whole month of a corpus's tool results in one row group, so reading
|
|
85
|
+
#: one session's results decompressed every other session's too (measured on
|
|
86
|
+
#: the copied corpora: 0.7 s per session). The step-level tables' rows land
|
|
87
|
+
#: in session order, so a small group holds a few sessions and a session read
|
|
88
|
+
#: touches one or two. Part of :func:`lake_schema_digest`, so a lake written in
|
|
89
|
+
#: another layout is rebuilt rather than left slow.
|
|
90
|
+
LAKE_ROW_GROUP_SIZE: int = 4096
|
|
91
|
+
|
|
92
|
+
#: The name the lake is attached under on every connection that reads or
|
|
93
|
+
#: writes it. Not part of the queryable surface: the views are.
|
|
94
|
+
LAKE_ALIAS: str = "atif_lake"
|
|
95
|
+
|
|
96
|
+
#: The metadata database DuckLake attaches beside :data:`LAKE_ALIAS`.
|
|
97
|
+
LAKE_METADATA_ALIAS: str = f"__ducklake_metadata_{LAKE_ALIAS}"
|
|
98
|
+
|
|
99
|
+
#: The identity every lake table starts with.
|
|
100
|
+
AGENT_COLUMN: str = "agent"
|
|
101
|
+
SESSION_KEY: str = "session_id"
|
|
102
|
+
IDENTITY_COLUMNS: tuple[tuple[str, str], ...] = (
|
|
103
|
+
(CORPUS_COLUMN, "VARCHAR"),
|
|
104
|
+
(AGENT_COLUMN, "VARCHAR"),
|
|
105
|
+
(SESSION_KEY, "VARCHAR"),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
#: A source column whose name is one of the identity names is stored under
|
|
109
|
+
#: this prefix (the trajectory's own ``agent`` struct becomes ``src_agent``).
|
|
110
|
+
COLLISION_PREFIX: str = "src_"
|
|
111
|
+
|
|
112
|
+
#: ``lake_info`` keys (the lake's recorded schema identity).
|
|
113
|
+
INFO_SCHEMA_VERSION: str = "lake_schema_version"
|
|
114
|
+
INFO_SCHEMA_DIGEST: str = "lake_schema_digest"
|
|
115
|
+
INFO_COLUMNAR_SCHEMA: str = "columnar_schema_version"
|
|
116
|
+
|
|
117
|
+
#: The two bookkeeping tables. Their shape never changes, so a stale lake can
|
|
118
|
+
#: still say which corpora it holds and a rebuild can find them again.
|
|
119
|
+
LAKE_INFO_TABLE: str = "lake_info"
|
|
120
|
+
CORPORA_TABLE: str = "corpora"
|
|
121
|
+
LAKE_INFO_COLUMNS: tuple[tuple[str, str], ...] = (("key", "VARCHAR"), ("value", "VARCHAR"))
|
|
122
|
+
CORPORA_COLUMNS: tuple[tuple[str, str], ...] = (
|
|
123
|
+
(CORPUS_COLUMN, "VARCHAR"),
|
|
124
|
+
(AGENT_COLUMN, "VARCHAR"),
|
|
125
|
+
("corpus_root", "VARCHAR"),
|
|
126
|
+
("registered_at", "VARCHAR"),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
#: The source for the sessions table: the trajectory rows plus the first
|
|
130
|
+
#: step's time, which the table is partitioned by.
|
|
131
|
+
_SESSIONS_SOURCE: str = (
|
|
132
|
+
f"(SELECT t.*, s.started_at FROM {RAW_TRAJECTORIES} t LEFT JOIN " # noqa: S608 # nosec B608 - module constants only
|
|
133
|
+
f"(SELECT session_id, min(ts) AS started_at FROM {RAW_STEPS} GROUP BY session_id) s "
|
|
134
|
+
f"ON s.session_id = t.{SESSION_ID_PATH})"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
_MONTHLY: tuple[str, ...] = ("year({ts})", "month({ts})")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(frozen=True, slots=True)
|
|
141
|
+
class LakeColumn:
|
|
142
|
+
"""One payload column: its lake name, type, and the source column it copies."""
|
|
143
|
+
|
|
144
|
+
name: str
|
|
145
|
+
sql_type: str
|
|
146
|
+
source: str
|
|
147
|
+
#: Computed at load time rather than copied from the relation, so the
|
|
148
|
+
#: reader view does not select it back (``sessions.started_at``).
|
|
149
|
+
derived: bool = False
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True, slots=True)
|
|
153
|
+
class LakeTable:
|
|
154
|
+
"""One lake table and the raw relation it is loaded from and read back as."""
|
|
155
|
+
|
|
156
|
+
#: Table name inside the lake.
|
|
157
|
+
name: str
|
|
158
|
+
#: The raw reader (a registry relation name) whose shape the table stores.
|
|
159
|
+
relation: str
|
|
160
|
+
#: What the load selects from: the relation itself, or a constant subquery.
|
|
161
|
+
load_source: str
|
|
162
|
+
#: The source column holding the session key (``session_id`` or ``session_id_path``).
|
|
163
|
+
key_source: str
|
|
164
|
+
#: Payload columns after the identity columns, in order.
|
|
165
|
+
payload: tuple[LakeColumn, ...]
|
|
166
|
+
#: Partition expressions, after the identity-derived ``agent, corpus``.
|
|
167
|
+
partition_by: tuple[str, ...]
|
|
168
|
+
#: Source columns the relation exposes that the reader view must rebuild
|
|
169
|
+
#: from the identity (``corpus`` on the trajectories relation).
|
|
170
|
+
relation_extras: tuple[str, ...] = ()
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def columns(self) -> tuple[tuple[str, str], ...]:
|
|
174
|
+
"""Every lake column, identity first."""
|
|
175
|
+
return (*IDENTITY_COLUMNS, *((c.name, c.sql_type) for c in self.payload))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _payload(
|
|
179
|
+
columns: tuple[tuple[str, str], ...], *, key_source: str, skip: tuple[str, ...] = ()
|
|
180
|
+
) -> tuple[LakeColumn, ...]:
|
|
181
|
+
"""Map a relation's columns onto lake payload columns (key and identity renamed)."""
|
|
182
|
+
reserved = {name for name, _ in IDENTITY_COLUMNS}
|
|
183
|
+
out: list[LakeColumn] = []
|
|
184
|
+
for name, sql_type in columns:
|
|
185
|
+
if name == key_source or name in skip:
|
|
186
|
+
continue
|
|
187
|
+
lake_name = f"{COLLISION_PREFIX}{name}" if name in reserved else name
|
|
188
|
+
out.append(LakeColumn(name=lake_name, sql_type=sql_type, source=name))
|
|
189
|
+
return tuple(out)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _monthly(ts: str) -> tuple[str, ...]:
|
|
193
|
+
return tuple(part.format(ts=ts) for part in _MONTHLY)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _view_table(name: str, relation: str, view: str) -> LakeTable:
|
|
197
|
+
return LakeTable(
|
|
198
|
+
name=name,
|
|
199
|
+
relation=relation,
|
|
200
|
+
load_source=relation,
|
|
201
|
+
key_source=SESSION_KEY,
|
|
202
|
+
payload=_payload(VIEW_SCHEMA[view], key_source=SESSION_KEY),
|
|
203
|
+
partition_by=_monthly("ts"),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
#: Every lake table, in load order. The sessions table reads the steps
|
|
208
|
+
#: relation for ``started_at``, which the registry binds before the load runs.
|
|
209
|
+
LAKE_TABLES: tuple[LakeTable, ...] = (
|
|
210
|
+
LakeTable(
|
|
211
|
+
name="sessions",
|
|
212
|
+
relation=RAW_TRAJECTORIES,
|
|
213
|
+
load_source=_SESSIONS_SOURCE,
|
|
214
|
+
key_source=SESSION_ID_PATH,
|
|
215
|
+
payload=(
|
|
216
|
+
*_payload(TRAJECTORY_RELATION_COLUMNS, key_source=SESSION_ID_PATH),
|
|
217
|
+
LakeColumn(name="started_at", sql_type="TIMESTAMP", source="started_at", derived=True),
|
|
218
|
+
),
|
|
219
|
+
partition_by=_monthly("started_at"),
|
|
220
|
+
relation_extras=(CORPUS_COLUMN,),
|
|
221
|
+
),
|
|
222
|
+
_view_table("steps", RAW_STEPS, "steps"),
|
|
223
|
+
_view_table("tool_calls", RAW_TOOL_CALLS, "tool_calls"),
|
|
224
|
+
_view_table("tool_results", RAW_TOOL_RESULTS, "tool_results"),
|
|
225
|
+
_view_table("session_events", RAW_SESSION_EVENTS, "session_events"),
|
|
226
|
+
LakeTable(
|
|
227
|
+
name="edges",
|
|
228
|
+
relation=RAW_EDGES,
|
|
229
|
+
load_source=RAW_EDGES,
|
|
230
|
+
key_source=SESSION_ID_PATH,
|
|
231
|
+
payload=_payload(tuple(EDGE_COLUMNS.items()), key_source=SESSION_ID_PATH),
|
|
232
|
+
partition_by=_monthly("ts"),
|
|
233
|
+
),
|
|
234
|
+
LakeTable(
|
|
235
|
+
name="loss_reports",
|
|
236
|
+
relation=RAW_LOSS_REPORTS,
|
|
237
|
+
load_source=RAW_LOSS_REPORTS,
|
|
238
|
+
key_source=SESSION_ID_PATH,
|
|
239
|
+
payload=_payload(
|
|
240
|
+
(*LOSS_REPORT_COLUMNS.items(), LOSS_REPORT_PATH_COLUMN), key_source=SESSION_ID_PATH
|
|
241
|
+
),
|
|
242
|
+
partition_by=(),
|
|
243
|
+
),
|
|
244
|
+
LakeTable(
|
|
245
|
+
name="session_meta",
|
|
246
|
+
relation=RAW_META,
|
|
247
|
+
load_source=RAW_META,
|
|
248
|
+
key_source=SESSION_ID_PATH,
|
|
249
|
+
payload=_payload(tuple(META_COLUMNS.items()), key_source=SESSION_ID_PATH),
|
|
250
|
+
partition_by=(),
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
LAKE_TABLE_NAMES: tuple[str, ...] = tuple(table.name for table in LAKE_TABLES)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
# Statements. Every one is a module constant built from LAKE_TABLES, so the
|
|
259
|
+
# AST audit in tests/test_sql_text_boundaries.py can prove each placeholder
|
|
260
|
+
# is catalog text; the only values that ever vary (corpus, agent, session
|
|
261
|
+
# ids) are bound parameters.
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
#: ``CREATE TABLE`` for the two bookkeeping tables.
|
|
265
|
+
BOOTSTRAP_CREATE_SQL: tuple[SqlFragment, ...] = (
|
|
266
|
+
SqlFragment(
|
|
267
|
+
f"CREATE TABLE {LAKE_ALIAS}.{LAKE_INFO_TABLE} "
|
|
268
|
+
f"({', '.join(f'{name} {sql_type}' for name, sql_type in LAKE_INFO_COLUMNS)})"
|
|
269
|
+
),
|
|
270
|
+
SqlFragment(
|
|
271
|
+
f"CREATE TABLE {LAKE_ALIAS}.{CORPORA_TABLE} "
|
|
272
|
+
f"({', '.join(f'{name} {sql_type}' for name, sql_type in CORPORA_COLUMNS)})"
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
#: ``CREATE TABLE`` per lake table.
|
|
277
|
+
CREATE_TABLE_SQL: dict[str, SqlFragment] = {
|
|
278
|
+
t.name: SqlFragment(
|
|
279
|
+
f"CREATE TABLE {LAKE_ALIAS}.{t.name} "
|
|
280
|
+
f"({', '.join(f'{name} {sql_type}' for name, sql_type in t.columns)})"
|
|
281
|
+
)
|
|
282
|
+
for t in LAKE_TABLES
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#: ``ALTER TABLE ... SET PARTITIONED BY`` per lake table: agent and corpus
|
|
286
|
+
#: first, then the table's own time partition.
|
|
287
|
+
PARTITION_SQL: dict[str, SqlFragment] = {
|
|
288
|
+
t.name: SqlFragment(
|
|
289
|
+
f"ALTER TABLE {LAKE_ALIAS}.{t.name} SET PARTITIONED BY "
|
|
290
|
+
f"({', '.join((AGENT_COLUMN, CORPUS_COLUMN, *t.partition_by))})"
|
|
291
|
+
)
|
|
292
|
+
for t in LAKE_TABLES
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#: Delete one corpus's rows for a bound list of session ids (params: corpus, ids).
|
|
296
|
+
DELETE_SESSIONS_SQL: dict[str, SqlFragment] = {
|
|
297
|
+
t.name: SqlFragment(
|
|
298
|
+
f"DELETE FROM {LAKE_ALIAS}.{t.name} WHERE {CORPUS_COLUMN} = ? " # noqa: S608 # nosec B608 - constants only; corpus and ids are bound
|
|
299
|
+
f"AND {SESSION_KEY} IN (SELECT unnest(?::VARCHAR[]))"
|
|
300
|
+
)
|
|
301
|
+
for t in LAKE_TABLES
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#: Delete every row of one corpus (param: corpus).
|
|
305
|
+
DELETE_CORPUS_SQL: dict[str, SqlFragment] = {
|
|
306
|
+
t.name: SqlFragment(f"DELETE FROM {LAKE_ALIAS}.{t.name} WHERE {CORPUS_COLUMN} = ?") # noqa: S608 # nosec B608 - constants only; corpus is bound
|
|
307
|
+
for t in LAKE_TABLES
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#: Copy the bound raw relation's rows into the lake (params: corpus, agent).
|
|
311
|
+
#: Every column is cast to its declared type, so a relation that infers a
|
|
312
|
+
#: looser type (an all-NULL column) still lands in the table's shape.
|
|
313
|
+
INSERT_SQL: dict[str, SqlFragment] = {
|
|
314
|
+
t.name: SqlFragment(
|
|
315
|
+
f"INSERT INTO {LAKE_ALIAS}.{t.name} ({', '.join(name for name, _ in t.columns)}) " # noqa: S608 # nosec B608 - constants only; corpus and agent are bound
|
|
316
|
+
f"SELECT CAST(? AS VARCHAR), CAST(? AS VARCHAR), CAST({t.key_source} AS VARCHAR), "
|
|
317
|
+
f"{', '.join(f'CAST({c.source} AS {c.sql_type})' for c in t.payload)} "
|
|
318
|
+
f"FROM {t.load_source}"
|
|
319
|
+
)
|
|
320
|
+
for t in LAKE_TABLES
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
#: Each lake table read back in its raw relation's shape, every corpus. A
|
|
324
|
+
#: reader appends ``WHERE corpus = <literal>`` to scope it.
|
|
325
|
+
READER_SELECT_SQL: dict[str, SqlFragment] = {
|
|
326
|
+
t.name: SqlFragment(
|
|
327
|
+
"SELECT " # noqa: S608 # nosec B608 - constants only
|
|
328
|
+
+ ", ".join(
|
|
329
|
+
(
|
|
330
|
+
*(f"{c.name} AS {c.source}" for c in t.payload if not c.derived),
|
|
331
|
+
f"{SESSION_KEY} AS {t.key_source}",
|
|
332
|
+
*t.relation_extras,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
+ f" FROM {LAKE_ALIAS}.{t.name}"
|
|
336
|
+
)
|
|
337
|
+
for t in LAKE_TABLES
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#: Per-session ``(session_id, rows, content hash)`` over the registry's raw
|
|
341
|
+
#: relation, columns cast as the load casts them. ``sum(hash(...))`` rather
|
|
342
|
+
#: than ``bit_xor``: order-free like an XOR, but two identical rows do not
|
|
343
|
+
#: cancel out.
|
|
344
|
+
ARTIFACT_HASH_SQL: dict[str, SqlFragment] = {
|
|
345
|
+
t.name: SqlFragment(
|
|
346
|
+
f"SELECT CAST({t.key_source} AS VARCHAR) AS session_id, count(*) AS n, " # noqa: S608 # nosec B608 - constants only
|
|
347
|
+
f"sum(CAST(hash({', '.join(f'CAST({c.source} AS {c.sql_type})' for c in t.payload)}) "
|
|
348
|
+
f"AS HUGEINT)) AS h FROM {t.load_source} GROUP BY 1"
|
|
349
|
+
)
|
|
350
|
+
for t in LAKE_TABLES
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#: The same over the lake table for one corpus (param: corpus).
|
|
354
|
+
LAKE_HASH_SQL: dict[str, SqlFragment] = {
|
|
355
|
+
t.name: SqlFragment(
|
|
356
|
+
f"SELECT {SESSION_KEY} AS session_id, count(*) AS n, " # noqa: S608 # nosec B608 - constants only; corpus is bound
|
|
357
|
+
f"sum(CAST(hash({', '.join(c.name for c in t.payload)}) AS HUGEINT)) AS h "
|
|
358
|
+
f"FROM {LAKE_ALIAS}.{t.name} WHERE {CORPUS_COLUMN} = ? GROUP BY 1"
|
|
359
|
+
)
|
|
360
|
+
for t in LAKE_TABLES
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def lake_schema_digest() -> str:
|
|
365
|
+
"""sha256 over every table's name, columns and partition spec, bookkeeping and file layout included."""
|
|
366
|
+
spec = {
|
|
367
|
+
"row_group_size": LAKE_ROW_GROUP_SIZE,
|
|
368
|
+
"source_ordered_tables": SOURCE_ORDERED_TABLES,
|
|
369
|
+
"tables": [
|
|
370
|
+
{"name": t.name, "columns": t.columns, "partition_by": t.partition_by}
|
|
371
|
+
for t in LAKE_TABLES
|
|
372
|
+
],
|
|
373
|
+
LAKE_INFO_TABLE: LAKE_INFO_COLUMNS,
|
|
374
|
+
CORPORA_TABLE: CORPORA_COLUMNS,
|
|
375
|
+
}
|
|
376
|
+
return hashlib.sha256(json.dumps(spec, sort_keys=True).encode("utf-8")).hexdigest()
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def expected_lake_info() -> dict[str, str]:
|
|
380
|
+
"""The ``lake_info`` rows a lake written by this code carries."""
|
|
381
|
+
return {
|
|
382
|
+
INFO_SCHEMA_VERSION: str(LAKE_SCHEMA_VERSION),
|
|
383
|
+
INFO_SCHEMA_DIGEST: lake_schema_digest(),
|
|
384
|
+
INFO_COLUMNAR_SCHEMA: str(COLUMNAR_SCHEMA_VERSION),
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def lake_info_mismatches(recorded: dict[str, str]) -> tuple[str, ...]:
|
|
389
|
+
"""The ``lake_info`` keys whose recorded value differs from this code's (sorted)."""
|
|
390
|
+
expected = expected_lake_info()
|
|
391
|
+
return tuple(sorted(key for key, value in expected.items() if recorded.get(key) != value))
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
__all__ = [
|
|
395
|
+
"AGENT_COLUMN",
|
|
396
|
+
"ARTIFACT_HASH_SQL",
|
|
397
|
+
"BOOTSTRAP_CREATE_SQL",
|
|
398
|
+
"COLLISION_PREFIX",
|
|
399
|
+
"CORPORA_COLUMNS",
|
|
400
|
+
"CORPORA_TABLE",
|
|
401
|
+
"CREATE_TABLE_SQL",
|
|
402
|
+
"DELETE_CORPUS_SQL",
|
|
403
|
+
"DELETE_SESSIONS_SQL",
|
|
404
|
+
"IDENTITY_COLUMNS",
|
|
405
|
+
"INFO_COLUMNAR_SCHEMA",
|
|
406
|
+
"INFO_SCHEMA_DIGEST",
|
|
407
|
+
"INFO_SCHEMA_VERSION",
|
|
408
|
+
"INSERT_SQL",
|
|
409
|
+
"LAKE_ALIAS",
|
|
410
|
+
"LAKE_HASH_SQL",
|
|
411
|
+
"LAKE_INFO_COLUMNS",
|
|
412
|
+
"LAKE_INFO_TABLE",
|
|
413
|
+
"LAKE_METADATA_ALIAS",
|
|
414
|
+
"LAKE_ROW_GROUP_SIZE",
|
|
415
|
+
"LAKE_SCHEMA_VERSION",
|
|
416
|
+
"LAKE_TABLES",
|
|
417
|
+
"LAKE_TABLE_NAMES",
|
|
418
|
+
"PARTITION_SQL",
|
|
419
|
+
"READER_SELECT_SQL",
|
|
420
|
+
"SESSION_KEY",
|
|
421
|
+
"SOURCE_ORDERED_TABLES",
|
|
422
|
+
"LakeColumn",
|
|
423
|
+
"LakeTable",
|
|
424
|
+
"expected_lake_info",
|
|
425
|
+
"lake_info_mismatches",
|
|
426
|
+
"lake_schema_digest",
|
|
427
|
+
]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""The statements that read one corpus's sessions back out of the lake, a batch at a time.
|
|
4
|
+
|
|
5
|
+
The LLM pipelines (atif-analytics) walk a corpus session by session: bounds
|
|
6
|
+
for every session, then the steps of the ones a gate or the renderer needs.
|
|
7
|
+
:class:`atif_duck.infrastructure.lake_sessions.LakeSessionReader` runs these
|
|
8
|
+
statements for them (atif-cli adapts its rows to atif-analytics' session
|
|
9
|
+
port, since the two packages may not import each other).
|
|
10
|
+
|
|
11
|
+
Every statement is a module constant over the lake tables of
|
|
12
|
+
:mod:`atif_duck.domain.lake`, so the AST audit in
|
|
13
|
+
``tests/test_sql_text_boundaries.py`` reads them like any other constant. The
|
|
14
|
+
corpus and the session ids are bound parameters; a batch of ids is one
|
|
15
|
+
``VARCHAR[]`` parameter.
|
|
16
|
+
|
|
17
|
+
Row order. A step's tool calls and results have no ordinal column; their
|
|
18
|
+
order is the order the writer inserted them in, which the lake keeps as
|
|
19
|
+
``rowid`` (DuckLake numbers a data file's rows from its ``row_id_start``, and
|
|
20
|
+
merges keep the numbers). The writer inserts the step-level tables
|
|
21
|
+
(:data:`atif_duck.domain.lake.SOURCE_ORDERED_TABLES`) on one thread so that order is the source
|
|
22
|
+
order; the statements below sort by ``step_id`` and then ``rowid``.
|
|
23
|
+
|
|
24
|
+
Pure constants: no duckdb import, no filesystem access.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from atif_duck.domain.lake import LAKE_ALIAS, SESSION_KEY
|
|
30
|
+
from atif_duck.domain.raw_readers import CORPUS_COLUMN
|
|
31
|
+
from atif_duck.domain.sql_literal import SqlFragment
|
|
32
|
+
|
|
33
|
+
_SCOPE: str = f"{CORPUS_COLUMN} = ? AND {SESSION_KEY} IN (SELECT unnest(?::VARCHAR[]))"
|
|
34
|
+
_ORDER: str = f"ORDER BY {SESSION_KEY}, step_id, rowid"
|
|
35
|
+
|
|
36
|
+
#: ``(session_id, materialized_at, source_mtime_ns)`` for every session of a
|
|
37
|
+
#: corpus (param: corpus). What a session's ``meta.json`` said when the lake
|
|
38
|
+
#: loaded it, so a reader can tell a current session from one the lake holds an
|
|
39
|
+
#: older write of.
|
|
40
|
+
SESSION_FRESHNESS_SQL: SqlFragment = SqlFragment(
|
|
41
|
+
f"SELECT {SESSION_KEY}, materialized_at, source_mtime_ns " # noqa: S608 # nosec B608 - constants only; corpus is bound
|
|
42
|
+
f"FROM {LAKE_ALIAS}.session_meta WHERE {CORPUS_COLUMN} = ?"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
#: ``(session_id, newest record ts)`` for every session of a corpus (param:
|
|
46
|
+
#: corpus): the checkpoint's first bound, from ``edges`` like the file path.
|
|
47
|
+
LAST_EDGE_TS_SQL: SqlFragment = SqlFragment(
|
|
48
|
+
f"SELECT {SESSION_KEY}, max(ts) FROM {LAKE_ALIAS}.edges " # noqa: S608 # nosec B608 - constants only; corpus is bound
|
|
49
|
+
f"WHERE {CORPUS_COLUMN} = ? GROUP BY 1"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
#: Every step of a batch of sessions without its tool payloads (params:
|
|
53
|
+
#: corpus, ids): ``(session_id, step_id, ts, source, message, is_sidechain,
|
|
54
|
+
#: is_compact_summary, source_uuids)``.
|
|
55
|
+
TURNS_SQL: SqlFragment = SqlFragment(
|
|
56
|
+
"SELECT " # noqa: S608 # nosec B608 - constants only; corpus and ids are bound
|
|
57
|
+
f"{SESSION_KEY}, step_id, ts, source, message, is_sidechain, is_compact_summary, "
|
|
58
|
+
f"CAST(source_uuids AS VARCHAR) FROM {LAKE_ALIAS}.steps WHERE {_SCOPE} {_ORDER}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
#: ``(session_id, step_id)`` of every step with an error result in a batch
|
|
62
|
+
#: (params: corpus, ids). The gates need the flag, not the result bodies.
|
|
63
|
+
ERROR_STEPS_SQL: SqlFragment = SqlFragment(
|
|
64
|
+
f"SELECT DISTINCT {SESSION_KEY}, step_id FROM {LAKE_ALIAS}.tool_results " # noqa: S608 # nosec B608 - constants only; corpus and ids are bound
|
|
65
|
+
f"WHERE {_SCOPE} AND is_error"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
#: Every tool call of a batch, in source order (params: corpus, ids):
|
|
69
|
+
#: ``(session_id, step_id, tool_name, tool_input)`` with the input as JSON text.
|
|
70
|
+
TOOL_CALLS_SQL: SqlFragment = SqlFragment(
|
|
71
|
+
f"SELECT {SESSION_KEY}, step_id, tool_name, CAST(tool_input AS VARCHAR) " # noqa: S608 # nosec B608 - constants only; corpus and ids are bound
|
|
72
|
+
f"FROM {LAKE_ALIAS}.tool_calls WHERE {_SCOPE} {_ORDER}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
#: Every tool result of a batch, in source order (params: corpus, ids):
|
|
76
|
+
#: ``(session_id, step_id, tool_use_id, content, is_error)`` with the content
|
|
77
|
+
#: as JSON text.
|
|
78
|
+
TOOL_RESULTS_SQL: SqlFragment = SqlFragment(
|
|
79
|
+
f"SELECT {SESSION_KEY}, step_id, tool_use_id, CAST(content AS VARCHAR), is_error " # noqa: S608 # nosec B608 - constants only; corpus and ids are bound
|
|
80
|
+
f"FROM {LAKE_ALIAS}.tool_results WHERE {_SCOPE} {_ORDER}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
#: The non-empty raw-record uuids of one session (params: corpus, session id).
|
|
84
|
+
EDGE_UUIDS_SQL: SqlFragment = SqlFragment(
|
|
85
|
+
f"SELECT DISTINCT uuid FROM {LAKE_ALIAS}.edges " # noqa: S608 # nosec B608 - constants only; corpus and the id are bound
|
|
86
|
+
f"WHERE {CORPUS_COLUMN} = ? AND {SESSION_KEY} = ? AND uuid IS NOT NULL AND uuid <> ''"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
__all__ = [
|
|
90
|
+
"EDGE_UUIDS_SQL",
|
|
91
|
+
"ERROR_STEPS_SQL",
|
|
92
|
+
"LAST_EDGE_TS_SQL",
|
|
93
|
+
"SESSION_FRESHNESS_SQL",
|
|
94
|
+
"TOOL_CALLS_SQL",
|
|
95
|
+
"TOOL_RESULTS_SQL",
|
|
96
|
+
"TURNS_SQL",
|
|
97
|
+
]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""The raw readers' names and column shapes, shared by the registry and the lake.
|
|
4
|
+
|
|
5
|
+
The registry (:mod:`atif_duck.infrastructure.registry`) binds one relation per
|
|
6
|
+
corpus artifact kind under these names, and every business view reads them
|
|
7
|
+
and nothing else. The lake (:mod:`atif_duck.domain.lake`) derives its tables
|
|
8
|
+
from the same shapes, so a column declared here is declared once for both the
|
|
9
|
+
per-session path and the lake path.
|
|
10
|
+
|
|
11
|
+
The names are deliberately absent from :data:`atif_duck.domain.catalog.VIEW_NAMES`:
|
|
12
|
+
they describe corpus files rather than being queryable business surface.
|
|
13
|
+
|
|
14
|
+
Pure constants: no duckdb import, no filesystem access.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from atif_duck.domain.columnar import META_COLUMNAR_KEY, SESSION_COLUMNS
|
|
20
|
+
|
|
21
|
+
#: The trajectory's top-level members minus ``steps``, plus the two
|
|
22
|
+
#: path-derived keys. ``v_raw_trajectories`` carries these, then ``corpus``.
|
|
23
|
+
RAW_TRAJECTORIES: str = "v_raw_trajectories"
|
|
24
|
+
#: One row per ATIF step, in the ``steps`` view's column shape.
|
|
25
|
+
RAW_STEPS: str = "v_raw_steps"
|
|
26
|
+
#: One row per tool call, in the ``tool_calls`` view's column shape.
|
|
27
|
+
RAW_TOOL_CALLS: str = "v_raw_tool_calls"
|
|
28
|
+
#: One row per observation result, in the ``tool_results`` view's column shape.
|
|
29
|
+
RAW_TOOL_RESULTS: str = "v_raw_tool_results"
|
|
30
|
+
#: One row per kept non-message record, in the ``session_events`` view's shape.
|
|
31
|
+
RAW_SESSION_EVENTS: str = "v_raw_session_events"
|
|
32
|
+
#: One row per RAW transcript record, from ``edges.jsonl``.
|
|
33
|
+
RAW_EDGES: str = "v_raw_edges"
|
|
34
|
+
#: One row per session, from ``loss_report.json``.
|
|
35
|
+
RAW_LOSS_REPORTS: str = "v_raw_loss_reports"
|
|
36
|
+
#: One row per complete session, from ``meta.json``.
|
|
37
|
+
RAW_META: str = "v_raw_meta"
|
|
38
|
+
|
|
39
|
+
#: The column every raw reader except the step-level four keys its session
|
|
40
|
+
#: on: the ``sessions/<id>/`` directory name.
|
|
41
|
+
SESSION_ID_PATH: str = "session_id_path"
|
|
42
|
+
|
|
43
|
+
#: The ``corpus`` column ``v_raw_trajectories`` ends with: the corpus
|
|
44
|
+
#: directory's name, which is how the lake and the ``sessions`` view name a
|
|
45
|
+
#: corpus.
|
|
46
|
+
CORPUS_COLUMN: str = "corpus"
|
|
47
|
+
|
|
48
|
+
#: ``v_raw_trajectories`` columns before ``corpus``, in order: the
|
|
49
|
+
#: trajectory's top-level members minus ``steps``, then the two path-derived
|
|
50
|
+
#: keys.
|
|
51
|
+
TRAJECTORY_RELATION_COLUMNS: tuple[tuple[str, str], ...] = (
|
|
52
|
+
*(column for column in SESSION_COLUMNS if column[0] != SESSION_ID_PATH),
|
|
53
|
+
("trajectory_path", "VARCHAR"),
|
|
54
|
+
(SESSION_ID_PATH, "VARCHAR"),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Explicit projection for ``v_raw_edges``: one line per RAW transcript record
|
|
58
|
+
# per docs/CONTRACT.md. ``parent_uuid`` is declared VARCHAR outright: a root
|
|
59
|
+
# record leaves it null, so inferred typing resolves it as a NULL-vs-string
|
|
60
|
+
# JSON union and every downstream view then needs its own CAST. Declaring the
|
|
61
|
+
# type in the one explicit-columns reader pays that cost once.
|
|
62
|
+
EDGE_COLUMNS: dict[str, str] = {
|
|
63
|
+
"uuid": "VARCHAR",
|
|
64
|
+
"parent_uuid": "VARCHAR",
|
|
65
|
+
"message_id": "VARCHAR",
|
|
66
|
+
"type": "VARCHAR",
|
|
67
|
+
"ts": "TIMESTAMP",
|
|
68
|
+
"is_sidechain": "BOOLEAN",
|
|
69
|
+
"is_compact_summary": "BOOLEAN",
|
|
70
|
+
"source_file": "VARCHAR",
|
|
71
|
+
"tool_use_ids": "JSON",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Explicit projection for ``v_raw_loss_reports``: atif_converter
|
|
75
|
+
# ``LossReport.to_json()`` shape (record_counts / gaps_observed stay JSON —
|
|
76
|
+
# enum-keyed dict and list respectively).
|
|
77
|
+
LOSS_REPORT_COLUMNS: dict[str, str] = {
|
|
78
|
+
"record_counts": "JSON",
|
|
79
|
+
"records_total": "BIGINT",
|
|
80
|
+
"records_converted": "BIGINT",
|
|
81
|
+
# Added with session_events (converter schema 2). A report written before
|
|
82
|
+
# then has no such key and reads as NULL, which the view keeps as NULL:
|
|
83
|
+
# "not measured" rather than "none captured".
|
|
84
|
+
"records_captured": "BIGINT",
|
|
85
|
+
"records_dropped": "BIGINT",
|
|
86
|
+
"gaps_observed": "JSON",
|
|
87
|
+
"subagent_files_found": "BIGINT",
|
|
88
|
+
"subagent_files_convertible": "BIGINT",
|
|
89
|
+
"workflow_subagent_files_found": "BIGINT",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#: The path column ``v_raw_loss_reports`` adds (the ``loss_reports`` view
|
|
93
|
+
#: returns it).
|
|
94
|
+
LOSS_REPORT_PATH_COLUMN: tuple[str, str] = ("report_path", "VARCHAR")
|
|
95
|
+
|
|
96
|
+
# Explicit projection for ``v_raw_meta`` per docs/CONTRACT.md. An explicit
|
|
97
|
+
# ``columns=`` projection nulls a missing key rather than failing, which is
|
|
98
|
+
# what lets a corpus written before a key existed still register.
|
|
99
|
+
META_COLUMNS: dict[str, str] = {
|
|
100
|
+
"session_id": "VARCHAR",
|
|
101
|
+
"source_mtime_ns": "BIGINT",
|
|
102
|
+
"source_files": "JSON",
|
|
103
|
+
"harbor_version": "VARCHAR",
|
|
104
|
+
"converter_version": "VARCHAR",
|
|
105
|
+
"materialized_at": "VARCHAR",
|
|
106
|
+
# Which agent wrote the transcript. Added 2026-09-11 with Codex support, so
|
|
107
|
+
# a session materialized before then has no such key and reads as NULL.
|
|
108
|
+
# Queries read ``sessions.agent`` (from the trajectory) instead; this
|
|
109
|
+
# column is provenance for an operator reading meta.json directly.
|
|
110
|
+
"agent": "VARCHAR",
|
|
111
|
+
# Which columnar schema the session's parquet artifacts were written
|
|
112
|
+
# against. Absent (NULL) on a session materialized before the artifacts
|
|
113
|
+
# existed; that session is read from trajectory.json, which is always
|
|
114
|
+
# correct.
|
|
115
|
+
META_COLUMNAR_KEY: "BIGINT",
|
|
116
|
+
# The converter's output version, and whether the source transcript still
|
|
117
|
+
# exists. NULL on a session written before the key existed.
|
|
118
|
+
"converter_schema": "BIGINT",
|
|
119
|
+
"source_present": "BOOLEAN",
|
|
120
|
+
"source_removed_at": "VARCHAR",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
__all__ = [
|
|
125
|
+
"CORPUS_COLUMN",
|
|
126
|
+
"EDGE_COLUMNS",
|
|
127
|
+
"LOSS_REPORT_COLUMNS",
|
|
128
|
+
"LOSS_REPORT_PATH_COLUMN",
|
|
129
|
+
"META_COLUMNS",
|
|
130
|
+
"RAW_EDGES",
|
|
131
|
+
"RAW_LOSS_REPORTS",
|
|
132
|
+
"RAW_META",
|
|
133
|
+
"RAW_SESSION_EVENTS",
|
|
134
|
+
"RAW_STEPS",
|
|
135
|
+
"RAW_TOOL_CALLS",
|
|
136
|
+
"RAW_TOOL_RESULTS",
|
|
137
|
+
"RAW_TRAJECTORIES",
|
|
138
|
+
"SESSION_ID_PATH",
|
|
139
|
+
"TRAJECTORY_RELATION_COLUMNS",
|
|
140
|
+
]
|