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,154 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Runtime configuration for atif-embed.
|
|
4
|
+
|
|
5
|
+
Pydantic v2 ``BaseSettings`` populated from env vars prefixed with
|
|
6
|
+
``ATIF_SQL_`` (workspace convention). Two providers share one vector-store
|
|
7
|
+
contract, selected by ``ATIF_SQL_EMBED_PROVIDER``:
|
|
8
|
+
|
|
9
|
+
* ``cohere`` (the default): Cohere Embed v4 on Bedrock via the global CRIS
|
|
10
|
+
profile, 1024-dim Matryoshka, int8 documents, batch 96, concurrency 8.
|
|
11
|
+
* ``gemma``: EmbeddingGemma 2 on this machine, text-only, 768-dim (MRL
|
|
12
|
+
widths 512/256/128 too), through the optional ``local`` extra.
|
|
13
|
+
|
|
14
|
+
Each provider keeps its own default store under the corpus root, so switching
|
|
15
|
+
providers never meets the fail-loud ``(model, dim)`` guard on the other's
|
|
16
|
+
vectors, and both stores can live side by side.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Literal, Self
|
|
23
|
+
|
|
24
|
+
from pydantic import Field, model_validator
|
|
25
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
26
|
+
|
|
27
|
+
#: Matryoshka widths each provider can emit. Cohere Embed v4 takes these four
|
|
28
|
+
#: as ``output_dimension``; EmbeddingGemma 2 is trained for 768 natively and
|
|
29
|
+
#: for truncation to the other three (re-normalized after truncating).
|
|
30
|
+
PROVIDER_DIMENSIONS: dict[str, tuple[int, ...]] = {
|
|
31
|
+
"cohere": (256, 512, 1024, 1536),
|
|
32
|
+
"gemma": (768, 512, 256, 128),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#: Width a provider emits when ``output_dimension`` is unset.
|
|
36
|
+
DEFAULT_DIMENSION: dict[str, int] = {"cohere": 1024, "gemma": 768}
|
|
37
|
+
|
|
38
|
+
#: Store directory under the corpus root, per provider. Cohere keeps the name
|
|
39
|
+
#: every existing store already has.
|
|
40
|
+
STORE_DIRNAME: dict[str, str] = {
|
|
41
|
+
"cohere": "embeddings_lance",
|
|
42
|
+
"gemma": "embeddings_lance_gemma",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class EmbedSettings(BaseSettings):
|
|
47
|
+
"""Env-driven settings for the embedding pipeline."""
|
|
48
|
+
|
|
49
|
+
model_config = SettingsConfigDict(
|
|
50
|
+
env_prefix="ATIF_SQL_",
|
|
51
|
+
env_file=".env",
|
|
52
|
+
extra="ignore",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
#: Which embedder writes and queries the store: Cohere on Bedrock, or
|
|
56
|
+
#: EmbeddingGemma 2 on this machine.
|
|
57
|
+
embed_provider: Literal["cohere", "gemma"] = "cohere"
|
|
58
|
+
#: Cohere Embed v4 global CRIS profile on bedrock-runtime.
|
|
59
|
+
embed_model_id: str = "global.cohere.embed-v4:0"
|
|
60
|
+
#: Matryoshka output width; ``None`` takes the provider's default (1024
|
|
61
|
+
#: for Cohere, 768 for Gemma). An ``int`` checked against the provider's
|
|
62
|
+
#: widths rather than a ``Literal``, because pydantic refuses the string
|
|
63
|
+
#: an environment variable carries for an integer ``Literal``.
|
|
64
|
+
output_dimension: int | None = None
|
|
65
|
+
#: Cohere document storage type. int8 documents + float queries is the
|
|
66
|
+
#: adapter's asymmetry: docs are float-widened on the way out for the
|
|
67
|
+
#: FLOAT[dim] Lance column, queries always embed as float for the distance
|
|
68
|
+
#: math.
|
|
69
|
+
embedding_type: Literal["int8", "float", "uint8", "binary", "ubinary"] = "int8"
|
|
70
|
+
#: Max texts per invoke_model call (Cohere Embed v4 batch ceiling).
|
|
71
|
+
batch_size: int = 96
|
|
72
|
+
#: Concurrent Bedrock batches. 8 × batch 96 ran without throttling in
|
|
73
|
+
#: testing — Cohere's TPM bucket is the binding constraint.
|
|
74
|
+
embed_concurrency: int = 8
|
|
75
|
+
#: AWS region for the bedrock-runtime client.
|
|
76
|
+
region: str = "us-east-1"
|
|
77
|
+
#: Hugging Face repository of the local model, also the identity stamped
|
|
78
|
+
#: on every row it writes.
|
|
79
|
+
gemma_model_id: str = "google/embeddinggemma-2"
|
|
80
|
+
#: Repository commit the local model loads, pinned so a later upload under
|
|
81
|
+
#: the same name cannot change the vectors a store already holds.
|
|
82
|
+
gemma_revision: str = "914f7f89142e33e77833254d9c9b90c3cef7303b"
|
|
83
|
+
#: Where the local model runs; ``auto`` takes CUDA, then Apple MPS, then CPU.
|
|
84
|
+
gemma_device: Literal["auto", "cpu", "cuda", "mps"] = "auto"
|
|
85
|
+
#: Texts per local forward pass. Peak memory grows with it times the
|
|
86
|
+
#: longest text in the batch.
|
|
87
|
+
gemma_batch_size: int = Field(default=32, ge=1)
|
|
88
|
+
#: Local LanceDB dataset URI. ``None`` (the default) resolves per corpus
|
|
89
|
+
#: and per provider via :meth:`resolve_lance_uri` — one vector store per
|
|
90
|
+
#: corpus, so re-pointing the source root never mixes two corpora's
|
|
91
|
+
#: vectors (the corpus-scoping rule).
|
|
92
|
+
lance_uri: Path | None = None
|
|
93
|
+
#: Distance metric for the IVF_HNSW_SQ index.
|
|
94
|
+
hnsw_metric: Literal["cosine", "l2", "dot"] = "cosine"
|
|
95
|
+
|
|
96
|
+
@model_validator(mode="after")
|
|
97
|
+
def _width_fits_provider(self) -> Self:
|
|
98
|
+
"""Refuse a width the selected provider cannot emit."""
|
|
99
|
+
allowed = PROVIDER_DIMENSIONS[self.embed_provider]
|
|
100
|
+
if self.output_dimension is not None and self.output_dimension not in allowed:
|
|
101
|
+
msg = (
|
|
102
|
+
f"ATIF_SQL_OUTPUT_DIMENSION={self.output_dimension} is not a width the "
|
|
103
|
+
f"{self.embed_provider} provider emits; use one of {list(allowed)} "
|
|
104
|
+
f"or leave it unset for {DEFAULT_DIMENSION[self.embed_provider]}"
|
|
105
|
+
)
|
|
106
|
+
raise ValueError(msg)
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def embedding_dim(self) -> int:
|
|
111
|
+
"""The vector width the selected provider writes and queries."""
|
|
112
|
+
if self.output_dimension is None:
|
|
113
|
+
return DEFAULT_DIMENSION[self.embed_provider]
|
|
114
|
+
return int(self.output_dimension)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def active_model_id(self) -> str:
|
|
118
|
+
"""The model identity the selected provider stamps on every row."""
|
|
119
|
+
if self.embed_provider == "gemma":
|
|
120
|
+
return self.gemma_model_id
|
|
121
|
+
return self.embed_model_id
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def active_batch_size(self) -> int:
|
|
125
|
+
"""Texts per provider call: one Bedrock request, or one local forward pass."""
|
|
126
|
+
return self.gemma_batch_size if self.embed_provider == "gemma" else self.batch_size
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def active_concurrency(self) -> int:
|
|
130
|
+
"""Batches in flight at once; the local model runs one at a time."""
|
|
131
|
+
return 1 if self.embed_provider == "gemma" else self.embed_concurrency
|
|
132
|
+
|
|
133
|
+
def resolve_lance_uri(self, corpus_root: Path) -> Path:
|
|
134
|
+
"""Effective Lance dataset directory for ``corpus_root``."""
|
|
135
|
+
if self.lance_uri is not None:
|
|
136
|
+
return self.lance_uri
|
|
137
|
+
return corpus_root / STORE_DIRNAME[self.embed_provider]
|
|
138
|
+
|
|
139
|
+
def expected_embedding_identity(self) -> tuple[str, int]:
|
|
140
|
+
"""Return ``(model_id, dim)`` without building the embedder.
|
|
141
|
+
|
|
142
|
+
Dependency-free identity for the fail-loud store guard: callers that
|
|
143
|
+
only need to BIND the store (e.g. the search path) can enforce the
|
|
144
|
+
provider stamp without importing boto3 or torch.
|
|
145
|
+
"""
|
|
146
|
+
return (self.active_model_id, self.embedding_dim)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
__all__ = [
|
|
150
|
+
"DEFAULT_DIMENSION",
|
|
151
|
+
"PROVIDER_DIMENSIONS",
|
|
152
|
+
"STORE_DIRNAME",
|
|
153
|
+
"EmbedSettings",
|
|
154
|
+
]
|
atif_embed/py.typed
ADDED
|
File without changes
|
atif_models/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""atif-models: model alias registry + structured-output LLM client.
|
|
4
|
+
|
|
5
|
+
Owns CONTRACT-V2.md §Model registry and §Client contract — size aliases
|
|
6
|
+
(small/medium/large) resolve to concrete Bedrock GLOBAL inference-profile
|
|
7
|
+
ids per family (openai default, anthropic escape hatch), and the
|
|
8
|
+
``LlmStructuredProvider`` port turns (system, prompt, pydantic schema)
|
|
9
|
+
into a validated schema instance. NO other atif-sql package hardcodes a
|
|
10
|
+
model id. Hexagonal: ``domain`` (registry, port, schema transform, usage
|
|
11
|
+
accounting — pure) < ``infrastructure`` (bedrock-runtime adapters, env
|
|
12
|
+
settings). Independence contract: only atif-cli and atif-analytics may
|
|
13
|
+
import this package.
|
|
14
|
+
"""
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Pure domain layer for atif-models.
|
|
4
|
+
|
|
5
|
+
The model registry (frozen value objects, no I/O), the
|
|
6
|
+
``LlmStructuredProvider`` port + error taxonomy + usage accounting, and
|
|
7
|
+
the pydantic → OpenAI strict-mode schema transform. No boto3, no env,
|
|
8
|
+
no clock — infrastructure adapts to these shapes.
|
|
9
|
+
"""
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""The structured-output provider port + error taxonomy + usage accounting.
|
|
4
|
+
|
|
5
|
+
The seam is narrow on purpose: a system prompt, a user prompt, and a
|
|
6
|
+
pydantic schema in; a validated instance of that schema out. Everything
|
|
7
|
+
provider-specific (model id, region, reasoning effort, the boto3 client,
|
|
8
|
+
the concurrency limiter) is owned by the adapter and fixed at construction.
|
|
9
|
+
|
|
10
|
+
Error taxonomy follows the atif-converter ``DomainError`` pattern:
|
|
11
|
+
``RefusalError`` is terminal (content filter / model refusal — do not
|
|
12
|
+
retry), ``ProviderUnavailable`` is retryable (the caller's retry queue
|
|
13
|
+
owns the second chance after the adapter's in-process tenacity budget is
|
|
14
|
+
exhausted).
|
|
15
|
+
|
|
16
|
+
Usage accounting: each call yields one frozen :class:`CallUsage`;
|
|
17
|
+
:class:`UsageAccumulator` sums them thread-safely per pipeline. A
|
|
18
|
+
``threading.Lock`` (not an ``anyio.CapacityLimiter``) guards the counters
|
|
19
|
+
because adapters dispatch blocking ``invoke_model`` calls via
|
|
20
|
+
``anyio.to_thread`` — two worker threads can land in
|
|
21
|
+
:meth:`~UsageAccumulator.add` concurrently, and a lock is the right
|
|
22
|
+
primitive for a critical section.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import threading
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from typing import TYPE_CHECKING, Protocol, TypeVar, runtime_checkable
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from pydantic import BaseModel
|
|
33
|
+
|
|
34
|
+
#: A structured-output schema, bound to ``BaseModel`` so the adapter can
|
|
35
|
+
#: both derive the wire schema and ``model_validate`` the response.
|
|
36
|
+
SchemaT = TypeVar("SchemaT", bound="BaseModel")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DomainError(Exception):
|
|
40
|
+
"""Base for every atif-models domain error."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RefusalError(DomainError):
|
|
44
|
+
"""Terminal: the model refused (content filter / refusal finish reason).
|
|
45
|
+
|
|
46
|
+
Never retried — the input itself is the problem. Callers record the
|
|
47
|
+
unit as refused and move on.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ProviderUnavailable(DomainError): # noqa: N818 — names a provider state, not an "*Error"
|
|
52
|
+
"""Retryable: transport/service failure or an unusable response.
|
|
53
|
+
|
|
54
|
+
Raised after the adapter's in-process tenacity budget is exhausted,
|
|
55
|
+
or immediately for non-retryable-but-recoverable shapes (e.g. a
|
|
56
|
+
``length`` finish reason — bump ``max_completion_tokens`` or shrink
|
|
57
|
+
the prompt). Callers enqueue the unit on the retry queue.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class CallUsage:
|
|
63
|
+
"""Token accounting for one structured-output call.
|
|
64
|
+
|
|
65
|
+
``reasoning_tokens`` is the GPT-5.6 ``completion_tokens_details.reasoning_tokens``
|
|
66
|
+
count (a subset of ``output_tokens``, already billed as output);
|
|
67
|
+
``cached_tokens`` is ``prompt_tokens_details.cached_tokens`` (a subset
|
|
68
|
+
of ``input_tokens`` billed at the cached rate). Both are 0 when the
|
|
69
|
+
provider does not report them.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
input_tokens: int = 0
|
|
73
|
+
output_tokens: int = 0
|
|
74
|
+
reasoning_tokens: int = 0
|
|
75
|
+
cached_tokens: int = 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class UsageAccumulator:
|
|
79
|
+
"""Thread-safe per-pipeline accumulation of :class:`CallUsage`.
|
|
80
|
+
|
|
81
|
+
One instance per pipeline run; adapters call :meth:`add` after every
|
|
82
|
+
successful response, and the pipeline reads :meth:`summary` at the
|
|
83
|
+
end for the cost-guard log line.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self) -> None:
|
|
87
|
+
self._lock = threading.Lock()
|
|
88
|
+
self._calls = 0
|
|
89
|
+
self._input_tokens = 0
|
|
90
|
+
self._output_tokens = 0
|
|
91
|
+
self._reasoning_tokens = 0
|
|
92
|
+
self._cached_tokens = 0
|
|
93
|
+
|
|
94
|
+
def add(self, usage: CallUsage) -> None:
|
|
95
|
+
"""Fold one call's usage into the totals (safe from any thread)."""
|
|
96
|
+
with self._lock:
|
|
97
|
+
self._calls += 1
|
|
98
|
+
self._input_tokens += usage.input_tokens
|
|
99
|
+
self._output_tokens += usage.output_tokens
|
|
100
|
+
self._reasoning_tokens += usage.reasoning_tokens
|
|
101
|
+
self._cached_tokens += usage.cached_tokens
|
|
102
|
+
|
|
103
|
+
def summary(self) -> dict[str, int]:
|
|
104
|
+
"""Snapshot of the totals: calls + the four token counters."""
|
|
105
|
+
with self._lock:
|
|
106
|
+
return {
|
|
107
|
+
"calls": self._calls,
|
|
108
|
+
"input_tokens": self._input_tokens,
|
|
109
|
+
"output_tokens": self._output_tokens,
|
|
110
|
+
"reasoning_tokens": self._reasoning_tokens,
|
|
111
|
+
"cached_tokens": self._cached_tokens,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@runtime_checkable
|
|
116
|
+
class LlmStructuredProvider(Protocol):
|
|
117
|
+
"""Port: one structured-output call. One adapter per backend.
|
|
118
|
+
|
|
119
|
+
The seam is deliberately narrow — everything provider-specific is
|
|
120
|
+
fixed at adapter construction.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
async def classify_structured(
|
|
124
|
+
self, *, system: str, prompt: str, schema: type[SchemaT]
|
|
125
|
+
) -> SchemaT:
|
|
126
|
+
"""Run one structured-output call and return a validated ``schema`` instance.
|
|
127
|
+
|
|
128
|
+
``system`` goes in as a ``system``-role message; ``prompt`` is the
|
|
129
|
+
per-call user payload. Raises :class:`RefusalError` (terminal) or
|
|
130
|
+
:class:`ProviderUnavailable` (retryable).
|
|
131
|
+
"""
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
__all__ = [
|
|
136
|
+
"CallUsage",
|
|
137
|
+
"DomainError",
|
|
138
|
+
"LlmStructuredProvider",
|
|
139
|
+
"ProviderUnavailable",
|
|
140
|
+
"RefusalError",
|
|
141
|
+
"SchemaT",
|
|
142
|
+
"UsageAccumulator",
|
|
143
|
+
]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""The model alias registry (CONTRACT-V2 §Model registry).
|
|
4
|
+
|
|
5
|
+
Size aliases (small/medium/large) resolve to concrete Bedrock GLOBAL
|
|
6
|
+
inference-profile ids per family. This module is the ONLY place in the
|
|
7
|
+
atif-sql workspace where a model id is written down — every pipeline
|
|
8
|
+
names a (family, size) pair and lets :func:`resolve` pick the id.
|
|
9
|
+
|
|
10
|
+
Pricing (USD per 1M tokens, on-demand; Bedrock charges OpenAI/Anthropic
|
|
11
|
+
list parity, so these track the vendors' published rates):
|
|
12
|
+
|
|
13
|
+
* openai gpt-5.6, post the 2026-07-30 Bedrock price cut: luna $0.20/$1.20,
|
|
14
|
+
terra $2/$12, sol $5/$30.
|
|
15
|
+
* anthropic: haiku-4-5 $1/$5, sonnet-5 $2/$10, opus-5 $5/$25.
|
|
16
|
+
|
|
17
|
+
Pure domain: frozen value objects, no I/O, no env. Env-driven family /
|
|
18
|
+
size selection lives in :mod:`atif_models.infrastructure.settings`.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Literal
|
|
25
|
+
|
|
26
|
+
Family = Literal["openai", "anthropic"]
|
|
27
|
+
Size = Literal["small", "medium", "large"]
|
|
28
|
+
|
|
29
|
+
#: GPT-5.6 reasoning levels per the Bedrock GA post; the anthropic entries
|
|
30
|
+
#: reuse the same vocabulary.
|
|
31
|
+
ReasoningEffort = Literal["none", "low", "medium", "high", "xhigh", "max"]
|
|
32
|
+
|
|
33
|
+
DEFAULT_FAMILY: Family = "openai"
|
|
34
|
+
DEFAULT_REASONING_EFFORT: ReasoningEffort = "high"
|
|
35
|
+
DEFAULT_MAX_COMPLETION_TOKENS = 32_000
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class ModelSpec:
|
|
40
|
+
"""One concrete model behind a (family, size) alias.
|
|
41
|
+
|
|
42
|
+
``pricing_in`` / ``pricing_out`` are USD per 1M tokens (``None`` when
|
|
43
|
+
unknown — :func:`estimate_cost` tolerates that by returning ``None``).
|
|
44
|
+
``supports_strict_json`` marks native OpenAI strict structured outputs
|
|
45
|
+
(``response_format.json_schema.strict``); the anthropic entries are
|
|
46
|
+
``False`` because Bedrock-Anthropic enforces schemas through
|
|
47
|
+
``output_config``, a different contract.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
family: Family
|
|
51
|
+
size: Size
|
|
52
|
+
model_id: str
|
|
53
|
+
pricing_in: float | None
|
|
54
|
+
pricing_out: float | None
|
|
55
|
+
max_completion_tokens: int = DEFAULT_MAX_COMPLETION_TOKENS
|
|
56
|
+
reasoning_effort: ReasoningEffort = DEFAULT_REASONING_EFFORT
|
|
57
|
+
supports_strict_json: bool = True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
REGISTRY: dict[tuple[Family, Size], ModelSpec] = {
|
|
61
|
+
("openai", "small"): ModelSpec(
|
|
62
|
+
family="openai",
|
|
63
|
+
size="small",
|
|
64
|
+
model_id="global.openai.gpt-5.6-luna",
|
|
65
|
+
pricing_in=0.20,
|
|
66
|
+
pricing_out=1.20,
|
|
67
|
+
),
|
|
68
|
+
("openai", "medium"): ModelSpec(
|
|
69
|
+
family="openai",
|
|
70
|
+
size="medium",
|
|
71
|
+
model_id="global.openai.gpt-5.6-terra",
|
|
72
|
+
pricing_in=2.00,
|
|
73
|
+
pricing_out=12.00,
|
|
74
|
+
),
|
|
75
|
+
("openai", "large"): ModelSpec(
|
|
76
|
+
family="openai",
|
|
77
|
+
size="large",
|
|
78
|
+
model_id="global.openai.gpt-5.6-sol",
|
|
79
|
+
pricing_in=5.00,
|
|
80
|
+
pricing_out=30.00,
|
|
81
|
+
),
|
|
82
|
+
("anthropic", "small"): ModelSpec(
|
|
83
|
+
family="anthropic",
|
|
84
|
+
size="small",
|
|
85
|
+
model_id="global.anthropic.claude-haiku-4-5",
|
|
86
|
+
pricing_in=1.00,
|
|
87
|
+
pricing_out=5.00,
|
|
88
|
+
supports_strict_json=False,
|
|
89
|
+
),
|
|
90
|
+
("anthropic", "medium"): ModelSpec(
|
|
91
|
+
family="anthropic",
|
|
92
|
+
size="medium",
|
|
93
|
+
model_id="global.anthropic.claude-sonnet-5",
|
|
94
|
+
pricing_in=2.00,
|
|
95
|
+
pricing_out=10.00,
|
|
96
|
+
supports_strict_json=False,
|
|
97
|
+
),
|
|
98
|
+
("anthropic", "large"): ModelSpec(
|
|
99
|
+
family="anthropic",
|
|
100
|
+
size="large",
|
|
101
|
+
model_id="global.anthropic.claude-opus-5",
|
|
102
|
+
pricing_in=5.00,
|
|
103
|
+
pricing_out=25.00,
|
|
104
|
+
supports_strict_json=False,
|
|
105
|
+
),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def resolve(size: Size, family: Family = DEFAULT_FAMILY) -> ModelSpec:
|
|
110
|
+
"""Return the :class:`ModelSpec` behind a (family, size) alias.
|
|
111
|
+
|
|
112
|
+
Default family is openai: GPT-5.6 supports native strict structured
|
|
113
|
+
outputs, and it is the only family with a provider adapter. The
|
|
114
|
+
anthropic column records model ids and pricing for a future adapter;
|
|
115
|
+
:data:`atif_models.infrastructure.settings.RUNNABLE_FAMILIES` is the
|
|
116
|
+
gate that keeps it from being selected before one exists.
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
return REGISTRY[(family, size)]
|
|
120
|
+
except KeyError: # pragma: no cover — Literal types make this unreachable from typed code
|
|
121
|
+
msg = f"no model registered for family={family!r} size={size!r}"
|
|
122
|
+
raise KeyError(msg) from None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def estimate_cost(spec: ModelSpec, *, input_tokens: int, output_tokens: int) -> float | None:
|
|
126
|
+
"""USD estimate for one call (or an accumulated pipeline) on ``spec``.
|
|
127
|
+
|
|
128
|
+
Returns ``None`` when either price is unknown — callers must render
|
|
129
|
+
that as "pricing unavailable", never as $0. Cache-read / reasoning
|
|
130
|
+
discounts are deliberately NOT modeled here: reasoning tokens bill as
|
|
131
|
+
output (already inside ``output_tokens`` for the OpenAI usage shape)
|
|
132
|
+
and cached input still bills at a nonzero rate, so this is an upper
|
|
133
|
+
bound suitable for the dry-run cost guard.
|
|
134
|
+
"""
|
|
135
|
+
if spec.pricing_in is None or spec.pricing_out is None:
|
|
136
|
+
return None
|
|
137
|
+
return (input_tokens * spec.pricing_in + output_tokens * spec.pricing_out) / 1_000_000
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
__all__ = [
|
|
141
|
+
"DEFAULT_FAMILY",
|
|
142
|
+
"DEFAULT_MAX_COMPLETION_TOKENS",
|
|
143
|
+
"DEFAULT_REASONING_EFFORT",
|
|
144
|
+
"REGISTRY",
|
|
145
|
+
"Family",
|
|
146
|
+
"ModelSpec",
|
|
147
|
+
"ReasoningEffort",
|
|
148
|
+
"Size",
|
|
149
|
+
"estimate_cost",
|
|
150
|
+
"resolve",
|
|
151
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Pydantic → OpenAI strict-mode JSON Schema transform (CONTRACT-V2 §Client contract).
|
|
4
|
+
|
|
5
|
+
OpenAI's strict structured outputs (``response_format.json_schema.strict``)
|
|
6
|
+
accept a JSON Schema subset with two hard rules this module enforces:
|
|
7
|
+
|
|
8
|
+
* ``additionalProperties: false`` on EVERY object level.
|
|
9
|
+
* EVERY property listed in ``required`` — optionality is expressed by
|
|
10
|
+
making the field's type nullable (``["string", "null"]`` / an ``anyOf``
|
|
11
|
+
null branch), never by omitting it from ``required``.
|
|
12
|
+
|
|
13
|
+
``$defs``/``$ref`` are KEPT: OpenAI strict accepts them, including a
|
|
14
|
+
``$defs``-referenced enum, on all three gpt-5.6 sizes. Ref inlining is a
|
|
15
|
+
per-provider wire-format concern, so it belongs in whichever adapter needs
|
|
16
|
+
it rather than here.
|
|
17
|
+
|
|
18
|
+
``default`` is dropped (it contradicts required-everywhere) and ``title``
|
|
19
|
+
is dropped as noise. Pydantic ``model_validate`` on the response is the
|
|
20
|
+
second gate, so ge/le style constraints still apply even if the model
|
|
21
|
+
ignores them.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import TYPE_CHECKING, Any
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from pydantic import BaseModel
|
|
30
|
+
|
|
31
|
+
#: Keys whose value is a mapping of NAME -> subschema (names are user data,
|
|
32
|
+
#: never schema keywords — a field literally named "default" must survive).
|
|
33
|
+
_MAP_OF_SCHEMAS = ("$defs", "definitions", "properties", "patternProperties")
|
|
34
|
+
#: Keys whose value is a list of subschemas.
|
|
35
|
+
_LIST_OF_SCHEMAS = ("anyOf", "oneOf", "allOf", "prefixItems")
|
|
36
|
+
#: Keys whose value is a single subschema.
|
|
37
|
+
_SINGLE_SCHEMA = ("items", "contains", "propertyNames", "not")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def to_openai_strict(schema: type[BaseModel]) -> dict[str, Any]:
|
|
41
|
+
"""Return ``schema.model_json_schema()`` transformed to OpenAI strict mode.
|
|
42
|
+
|
|
43
|
+
Raises ``ValueError`` for shapes strict mode cannot represent (an
|
|
44
|
+
object with a schema-valued ``additionalProperties``, i.e. a
|
|
45
|
+
``dict[str, X]`` field) — better a loud failure at body-build time
|
|
46
|
+
than a Bedrock 400 mid-pipeline.
|
|
47
|
+
"""
|
|
48
|
+
return _strictify(schema.model_json_schema())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _strictify(node: dict[str, Any]) -> dict[str, Any]:
|
|
52
|
+
"""Recursively apply the strict-mode rules to one schema node."""
|
|
53
|
+
out: dict[str, Any] = {}
|
|
54
|
+
for key, val in node.items():
|
|
55
|
+
if key in ("title", "default"):
|
|
56
|
+
continue
|
|
57
|
+
if key in _MAP_OF_SCHEMAS and isinstance(val, dict):
|
|
58
|
+
out[key] = {name: _strictify(sub) for name, sub in val.items()}
|
|
59
|
+
elif key in _LIST_OF_SCHEMAS and isinstance(val, list):
|
|
60
|
+
out[key] = [_strictify(sub) if isinstance(sub, dict) else sub for sub in val]
|
|
61
|
+
elif key in _SINGLE_SCHEMA and isinstance(val, dict):
|
|
62
|
+
out[key] = _strictify(val)
|
|
63
|
+
elif key == "additionalProperties" and isinstance(val, dict):
|
|
64
|
+
msg = (
|
|
65
|
+
"OpenAI strict mode cannot represent open mappings "
|
|
66
|
+
"(schema-valued additionalProperties, e.g. a dict[str, X] field); "
|
|
67
|
+
"model the keys explicitly or use a list of key/value pairs"
|
|
68
|
+
)
|
|
69
|
+
raise ValueError(msg)
|
|
70
|
+
else:
|
|
71
|
+
out[key] = val
|
|
72
|
+
if out.get("type") == "object" or "properties" in out:
|
|
73
|
+
props: dict[str, Any] = out.get("properties") or {}
|
|
74
|
+
already_required = set(out.get("required") or [])
|
|
75
|
+
for name, sub in props.items():
|
|
76
|
+
if name not in already_required:
|
|
77
|
+
props[name] = _nullable(sub)
|
|
78
|
+
if props:
|
|
79
|
+
out["required"] = list(props)
|
|
80
|
+
out["additionalProperties"] = False
|
|
81
|
+
return out
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _nullable(sub: dict[str, Any]) -> dict[str, Any]:
|
|
85
|
+
"""Rewrite one property subschema so ``null`` is an accepted value."""
|
|
86
|
+
if "$ref" in sub:
|
|
87
|
+
rest = {k: v for k, v in sub.items() if k != "$ref"}
|
|
88
|
+
return {**rest, "anyOf": [{"$ref": sub["$ref"]}, {"type": "null"}]}
|
|
89
|
+
if "anyOf" in sub:
|
|
90
|
+
branches: list[Any] = sub["anyOf"]
|
|
91
|
+
if any(isinstance(b, dict) and b.get("type") == "null" for b in branches):
|
|
92
|
+
return sub
|
|
93
|
+
return {**sub, "anyOf": [*branches, {"type": "null"}]}
|
|
94
|
+
type_val = sub.get("type")
|
|
95
|
+
if isinstance(type_val, str):
|
|
96
|
+
new = {**sub, "type": [type_val, "null"]}
|
|
97
|
+
elif isinstance(type_val, list):
|
|
98
|
+
new = sub if "null" in type_val else {**sub, "type": [*type_val, "null"]}
|
|
99
|
+
else:
|
|
100
|
+
# No type and no ref/anyOf (e.g. a bare enum or empty schema):
|
|
101
|
+
# wrap rather than guess.
|
|
102
|
+
return {"anyOf": [sub, {"type": "null"}]}
|
|
103
|
+
enum_val = new.get("enum")
|
|
104
|
+
if isinstance(enum_val, list) and None not in enum_val:
|
|
105
|
+
new = {**new, "enum": [*enum_val, None]}
|
|
106
|
+
return new
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
__all__ = ["to_openai_strict"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Infrastructure layer for atif-models.
|
|
4
|
+
|
|
5
|
+
bedrock-runtime adapters (OpenAI chat-completions default, Anthropic
|
|
6
|
+
``output_config`` experimental stub) and the ``ATIF_SQL_``-prefixed env
|
|
7
|
+
settings. Everything here imports DOWN into ``domain`` only.
|
|
8
|
+
"""
|