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,94 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Domain error taxonomy for atif-embed.
|
|
4
|
+
|
|
5
|
+
Every failure that can reach the CLI must be a :class:`DomainError`: the
|
|
6
|
+
``embed`` command catches only that base class and turns it into a
|
|
7
|
+
classified ``{kind, exit_code, hint}`` envelope. Anything escaping the
|
|
8
|
+
taxonomy surfaces as a raw traceback instead.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DomainError(Exception):
|
|
15
|
+
"""Base class for atif-embed domain errors.
|
|
16
|
+
|
|
17
|
+
``terminal`` distinguishes conditions no retry can clear (the store or
|
|
18
|
+
its config needs an operator) from transient runs the next tick may
|
|
19
|
+
succeed at. The CLI maps terminal errors to their own exit code so an
|
|
20
|
+
unattended lane can stop retrying instead of burning identical ticks —
|
|
21
|
+
the 2026-08-24 schema-stale condition retried 400+ times over 3 days
|
|
22
|
+
with zero escalation because every failure exited alike.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
#: True when retrying without operator action cannot succeed.
|
|
26
|
+
terminal: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EmbeddingProviderMismatch(DomainError): # noqa: N818 — names a store state, not an "*Error"
|
|
30
|
+
"""Raised on a stamped-vs-active ``(model, dim)`` mismatch.
|
|
31
|
+
|
|
32
|
+
The Lance store records the identity of the embedder that wrote it, and it
|
|
33
|
+
differs from the identity of the embedder now asking to read or extend it.
|
|
34
|
+
|
|
35
|
+
Different embedding models produce vectors in incompatible spaces, so
|
|
36
|
+
appending or querying across a provider switch yields numerically valid
|
|
37
|
+
but semantically garbage cosine scores. This error is terminal: the store
|
|
38
|
+
must be dropped and re-embedded under the new provider.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
terminal = True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class EmbeddingStoreSchemaStale(DomainError): # noqa: N818 — names a store state, not an "*Error"
|
|
45
|
+
"""A Lance store's schema requires operator action before embeds can run.
|
|
46
|
+
|
|
47
|
+
ADDITIVE drift no longer raises this: a store missing ``text_hash``
|
|
48
|
+
migrates online via schema evolution + the sentinel staleness path (see
|
|
49
|
+
``infrastructure.lance_store``). The class stays in the taxonomy for
|
|
50
|
+
schema states that genuinely cannot self-heal, and as the terminal
|
|
51
|
+
exemplar the refresh lane's retry suppression keys on.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
terminal = True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class EmbeddingProviderUnavailable(DomainError): # noqa: N818 — names a provider state, not an "*Error"
|
|
58
|
+
"""The embedding backend could not be reached or refused the request.
|
|
59
|
+
|
|
60
|
+
Covers what escapes tenacity's retry budget plus the non-retryable
|
|
61
|
+
botocore failures (expired or denied credentials, a rejected request
|
|
62
|
+
shape). Terminal for this run; the next run's staleness anti-join
|
|
63
|
+
re-picks whatever never got written.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class EmbeddingProviderNotInstalled(DomainError): # noqa: N818 — names an install state, not an "*Error"
|
|
68
|
+
"""The selected provider's libraries are not installed.
|
|
69
|
+
|
|
70
|
+
EmbeddingGemma 2 runs through the optional ``local`` extra (torch,
|
|
71
|
+
transformers, sentence-transformers). Selecting it without the extra
|
|
72
|
+
cannot succeed on a retry: an operator installs the extra or selects
|
|
73
|
+
another provider, so the error is terminal.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
terminal = True
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class EmbeddingResponseInvalid(DomainError): # noqa: N818 — names a response state, not an "*Error"
|
|
80
|
+
"""The backend answered, but not in the shape the adapter can read.
|
|
81
|
+
|
|
82
|
+
Distinct from :class:`EmbeddingProviderUnavailable` because it points at
|
|
83
|
+
a wire-format change rather than at credentials or capacity.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
__all__ = [
|
|
88
|
+
"DomainError",
|
|
89
|
+
"EmbeddingProviderMismatch",
|
|
90
|
+
"EmbeddingProviderNotInstalled",
|
|
91
|
+
"EmbeddingProviderUnavailable",
|
|
92
|
+
"EmbeddingResponseInvalid",
|
|
93
|
+
"EmbeddingStoreSchemaStale",
|
|
94
|
+
]
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Ports for atif-embed: what the embed use case NEEDS from the world.
|
|
4
|
+
|
|
5
|
+
Four seams:
|
|
6
|
+
|
|
7
|
+
* :class:`EmbeddingProvider` — async document embedding (batched,
|
|
8
|
+
float-widened) + sync query embedding, stamped with ``model_id`` /
|
|
9
|
+
``dimension``.
|
|
10
|
+
* :class:`VectorStorePort` — the Lance-backed store surface.
|
|
11
|
+
* :class:`TextRowsPort` — the corpus seam. atif-embed may never import
|
|
12
|
+
atif-duck (import-linter independence contract), so the embeddable text
|
|
13
|
+
rows come through this port; the duckdb implementation in atif-embed's OWN
|
|
14
|
+
infrastructure reads the CONTRACT corpus layout directly (the ConverterPort
|
|
15
|
+
precedent from atif-corpus).
|
|
16
|
+
* :class:`LakeStepsPort` — the corpus's steps as the DuckLake holds them.
|
|
17
|
+
atif-duck owns the lake, so atif-cli implements this port over it and
|
|
18
|
+
hands it to :class:`~atif_embed.infrastructure.lake_text_rows.LakeTextRows`,
|
|
19
|
+
which reads only what changed since the last complete pass.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from typing import TYPE_CHECKING, Protocol, Self
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Iterable, Iterator
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from types import TracebackType
|
|
31
|
+
|
|
32
|
+
import polars as pl
|
|
33
|
+
|
|
34
|
+
from atif_embed.domain.text_stamp import PendingText
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class EmbeddingProvider(Protocol):
|
|
38
|
+
"""Anything that can embed documents and queries into one vector space."""
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def model_id(self) -> str:
|
|
42
|
+
"""Globally unique model identity stamped on every stored row."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def dimension(self) -> int:
|
|
47
|
+
"""Fixed output vector width."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
async def embed_documents(self, texts: list[str]) -> list[list[float] | None]:
|
|
51
|
+
"""Embed corpus documents; one slot per input text, in the same order.
|
|
52
|
+
|
|
53
|
+
A slot is ``None`` when that text could not be embedded. Returning
|
|
54
|
+
per-text outcomes rather than raising is what makes loss bounded: a
|
|
55
|
+
batch that fails terminally must not discard the sibling batches
|
|
56
|
+
whose embeddings were already billed.
|
|
57
|
+
"""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
def embed_query(self, text: str) -> list[float]:
|
|
61
|
+
"""Embed one query string as a float vector of length :attr:`dimension`."""
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class VectorStorePort(Protocol):
|
|
66
|
+
"""The embeddings store surface the backfill writes through."""
|
|
67
|
+
|
|
68
|
+
def table_identity(self) -> tuple[str, int] | None:
|
|
69
|
+
"""Return the store's stamped ``(model, dim)``, or ``None`` when empty."""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
def get_embedded_hashes(self) -> dict[str, str]:
|
|
73
|
+
"""Return ``{uuid: text_hash}`` for every row currently embedded."""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
def delete_uuids(self, uuids: Iterable[str]) -> int:
|
|
77
|
+
"""Remove the rows for ``uuids``; return how many uuids were named."""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
def add_chunk(self, df: pl.DataFrame) -> None:
|
|
81
|
+
"""Append one chunk of embedding rows."""
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
def optimize(self) -> None:
|
|
85
|
+
"""Compact accumulated fragments (best-effort)."""
|
|
86
|
+
...
|
|
87
|
+
|
|
88
|
+
def ensure_index(self, *, metric: str = "cosine") -> None:
|
|
89
|
+
"""Create the vector index if missing (best-effort)."""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TextRowsPort(Protocol):
|
|
94
|
+
"""Anything that can yield the corpus's embeddable text rows.
|
|
95
|
+
|
|
96
|
+
Contract semantics (CONTRACT-V2 §VSS): the embeddable unit is a step's
|
|
97
|
+
flattened text — main chain AND sidechain — of at least 32 characters,
|
|
98
|
+
keyed by the step's PRIMARY uuid (the first ``extra.source_uuids`` entry;
|
|
99
|
+
steps without source uuids are skipped, they cannot join back to the
|
|
100
|
+
``messages``/edges surface).
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def iter_unembedded(
|
|
104
|
+
self,
|
|
105
|
+
corpus_root: Path,
|
|
106
|
+
*,
|
|
107
|
+
embedded: dict[str, str] | None = None,
|
|
108
|
+
limit: int | None = None,
|
|
109
|
+
) -> Iterator[PendingText]:
|
|
110
|
+
"""Yield the texts needing embedding, lazily.
|
|
111
|
+
|
|
112
|
+
``embedded`` is the store's ``{uuid: text_hash}`` map. A uuid absent
|
|
113
|
+
from it is NEW; a uuid present under a different hash is STALE (its
|
|
114
|
+
text changed since it was embedded) and is yielded with
|
|
115
|
+
``replaces_existing=True``.
|
|
116
|
+
|
|
117
|
+
Yielding lazily bounds what the CALLER holds to one write-chunk. It
|
|
118
|
+
says nothing about what an implementation holds internally: whether
|
|
119
|
+
peak resident text is a fraction of the corpus is a property each
|
|
120
|
+
adapter must establish for itself.
|
|
121
|
+
"""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def discovery(self) -> str:
|
|
126
|
+
"""Which path the last :meth:`iter_unembedded` read (``corpus``, ``lake-full``, ...)."""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
def commit(self, *, stored_rows: int) -> None:
|
|
130
|
+
"""Record that every row the last :meth:`iter_unembedded` yielded is now stored.
|
|
131
|
+
|
|
132
|
+
The use case calls it after a run that consumed the whole discovery
|
|
133
|
+
and embedded every row of it, with the store's row count afterwards.
|
|
134
|
+
An adapter with a watermark advances it here (and only when that
|
|
135
|
+
discovery ran to its end, not to a ``--limit``); one without does
|
|
136
|
+
nothing.
|
|
137
|
+
"""
|
|
138
|
+
...
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True, slots=True)
|
|
142
|
+
class LakePosition:
|
|
143
|
+
"""Where one corpus's lake history stands: which history, and how far along it.
|
|
144
|
+
|
|
145
|
+
``lineage`` changes whenever the corpus's rows are loaded from scratch (a
|
|
146
|
+
``lake rebuild``, or the first load of a corpus), because snapshot ids
|
|
147
|
+
from one history mean nothing in the next.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
lineage: str
|
|
151
|
+
snapshot_id: int
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class LakeStepSnapshot(Protocol):
|
|
155
|
+
"""One corpus's steps at one lake snapshot, open until closed."""
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def position(self) -> LakePosition:
|
|
159
|
+
"""The snapshot being read."""
|
|
160
|
+
...
|
|
161
|
+
|
|
162
|
+
def can_read_changes_since(self, since: LakePosition) -> bool:
|
|
163
|
+
"""True when the lake still holds every snapshot after ``since`` in this lineage."""
|
|
164
|
+
...
|
|
165
|
+
|
|
166
|
+
def step_texts(
|
|
167
|
+
self, *, min_chars: int, changed_since: LakePosition | None
|
|
168
|
+
) -> Iterator[tuple[str, str]]:
|
|
169
|
+
"""``(primary uuid, flattened text)`` for every qualifying step, in corpus order.
|
|
170
|
+
|
|
171
|
+
Corpus order is session id, then step id. A qualifying step has a
|
|
172
|
+
primary uuid, a non-NULL text and at least ``min_chars`` characters
|
|
173
|
+
of it. With ``changed_since``, only rows whose uuid appears in a step
|
|
174
|
+
row inserted or deleted after that position are read; those rows
|
|
175
|
+
still come back in corpus order, every occurrence of each uuid, so
|
|
176
|
+
the first-wins rule picks the same text a full read would.
|
|
177
|
+
"""
|
|
178
|
+
...
|
|
179
|
+
|
|
180
|
+
def step_keys(self) -> frozenset[str]:
|
|
181
|
+
"""Every step's primary uuid in the corpus, whatever its text."""
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
def close(self) -> None:
|
|
185
|
+
"""Release the connection."""
|
|
186
|
+
...
|
|
187
|
+
|
|
188
|
+
def __enter__(self) -> Self:
|
|
189
|
+
"""Open for a ``with`` block."""
|
|
190
|
+
...
|
|
191
|
+
|
|
192
|
+
def __exit__(
|
|
193
|
+
self,
|
|
194
|
+
exc_type: type[BaseException] | None,
|
|
195
|
+
exc: BaseException | None,
|
|
196
|
+
tb: TracebackType | None,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Close at the end of the ``with`` block."""
|
|
199
|
+
...
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class LakeStepsPort(Protocol):
|
|
203
|
+
"""Open the lake's view of one corpus's steps."""
|
|
204
|
+
|
|
205
|
+
def open_steps(self, corpus_root: Path) -> LakeStepSnapshot | None:
|
|
206
|
+
"""The corpus's steps at the lake's current snapshot, or ``None`` to fall back.
|
|
207
|
+
|
|
208
|
+
``None`` means no lake the caller should read: none exists, its
|
|
209
|
+
schema is stale, it doesn't hold this corpus, or it holds another
|
|
210
|
+
directory under the same name. The caller then reads the per-session
|
|
211
|
+
artifacts, as it did before the lake existed.
|
|
212
|
+
"""
|
|
213
|
+
...
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
__all__ = [
|
|
217
|
+
"EmbeddingProvider",
|
|
218
|
+
"LakePosition",
|
|
219
|
+
"LakeStepSnapshot",
|
|
220
|
+
"LakeStepsPort",
|
|
221
|
+
"TextRowsPort",
|
|
222
|
+
"VectorStorePort",
|
|
223
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Single-quoted SQL literal escaping for the one predicate that needs it.
|
|
4
|
+
|
|
5
|
+
LanceDB's delete predicate takes a string, not parameters, so ``lance_store``
|
|
6
|
+
escapes the uuids it deletes with this. The DuckDB reader in
|
|
7
|
+
``corpus_text_rows`` no longer needs it: its trajectory paths reach
|
|
8
|
+
``read_json`` as a bound list parameter. Escaping is a correctness requirement
|
|
9
|
+
of the predicate, and it lives in ``domain`` so a second caller would share
|
|
10
|
+
ONE implementation instead of carrying a copy.
|
|
11
|
+
|
|
12
|
+
:data:`SqlFragment` marks text that may be spliced into a statement; a value
|
|
13
|
+
from outside the process is a plain ``str`` until it passes through here.
|
|
14
|
+
|
|
15
|
+
atif-embed may never import atif-duck (the import-linter independence
|
|
16
|
+
contract), so this module is deliberately atif-embed's own rather than a reuse
|
|
17
|
+
of atif-duck's twin.
|
|
18
|
+
|
|
19
|
+
Pure module: stdlib only, no duckdb, no lancedb.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import NewType
|
|
25
|
+
|
|
26
|
+
#: Text that may be spliced into a SQL statement or predicate.
|
|
27
|
+
SqlFragment = NewType("SqlFragment", str)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def sql_literal(value: str) -> SqlFragment:
|
|
31
|
+
"""Escape ``value`` as a single-quoted SQL string literal.
|
|
32
|
+
|
|
33
|
+
Doubles every embedded single quote, per the SQL standard's own escape.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
value
|
|
38
|
+
Value to embed in a statement or predicate.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
SqlFragment
|
|
43
|
+
``value`` wrapped in single quotes, embedded quotes doubled.
|
|
44
|
+
"""
|
|
45
|
+
escaped = value.replace("'", "''")
|
|
46
|
+
return SqlFragment(f"'{escaped}'")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ["SqlFragment", "sql_literal"]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Per-text invariants the store must record: the content stamp and the cap.
|
|
4
|
+
|
|
5
|
+
Two rules, both about not losing information silently.
|
|
6
|
+
|
|
7
|
+
**The stamp.** A step's uuid is stable across re-conversions but its flattened
|
|
8
|
+
text is not: a harbor or converter fix changes the text under the same uuid.
|
|
9
|
+
Keying the store on uuid alone therefore makes the first vector permanent —
|
|
10
|
+
the anti-join sees the uuid, calls it embedded, and semantic search keeps
|
|
11
|
+
ranking against pre-fix text with nothing in the store to reveal it. Every
|
|
12
|
+
row also stamps :func:`text_hash` of the exact text it was built from, so a
|
|
13
|
+
mismatch means STALE and the row is replaced.
|
|
14
|
+
|
|
15
|
+
**The cap.** Texts over :data:`MAX_EMBEDDABLE_CHARS` are sent head-only, so
|
|
16
|
+
their vector cannot match content past the cap. Every row records whether it
|
|
17
|
+
was capped, which turns an otherwise unexplainable search miss into an
|
|
18
|
+
attributable one.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from hashlib import blake2b
|
|
25
|
+
|
|
26
|
+
#: 16 bytes (128 bit) of blake2b, hex-encoded. Collision risk is negligible
|
|
27
|
+
#: at corpus scale and the digest is a third the width of sha256 in a column
|
|
28
|
+
#: read in full on every discovery pass.
|
|
29
|
+
_DIGEST_BYTES = 16
|
|
30
|
+
|
|
31
|
+
#: Characters of a text that reach the embedder. Bedrock's body ceiling is
|
|
32
|
+
#: 20 MB and a full batch is 96 texts, so 50K each leaves headroom. Both the
|
|
33
|
+
#: row's ``truncated`` stamp and the adapter's wire-level clip read this one
|
|
34
|
+
#: constant, so the flag can never disagree with what was actually sent.
|
|
35
|
+
MAX_EMBEDDABLE_CHARS = 50_000
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def text_hash(text: str) -> str:
|
|
39
|
+
"""Return the stable content stamp for one embeddable text."""
|
|
40
|
+
return blake2b(text.encode("utf-8"), digest_size=_DIGEST_BYTES).hexdigest()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def clip_text(text: str) -> tuple[str, bool]:
|
|
44
|
+
"""Return ``(text_to_send, was_truncated)`` for one embeddable text."""
|
|
45
|
+
if len(text) <= MAX_EMBEDDABLE_CHARS:
|
|
46
|
+
return text, False
|
|
47
|
+
return text[:MAX_EMBEDDABLE_CHARS], True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class PendingText:
|
|
52
|
+
"""One text the backfill must embed, with its staleness verdict.
|
|
53
|
+
|
|
54
|
+
``replaces_existing`` is True when the store already holds a row for
|
|
55
|
+
this uuid under a DIFFERENT hash: that row must be deleted before the
|
|
56
|
+
new vector is appended, or the uuid would fan out to two vectors and the
|
|
57
|
+
kNN join would rank against both.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
uuid: str
|
|
61
|
+
text: str
|
|
62
|
+
text_hash: str
|
|
63
|
+
replaces_existing: bool
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def truncated(self) -> bool:
|
|
67
|
+
"""True when this text exceeds the cap and embeds head-only."""
|
|
68
|
+
return len(self.text) > MAX_EMBEDDABLE_CHARS
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
__all__ = ["MAX_EMBEDDABLE_CHARS", "PendingText", "clip_text", "text_hash"]
|