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,448 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Stance-conflict detection pipeline over the materialized corpus.
|
|
4
|
+
|
|
5
|
+
Transcript text is sent to a third-party model provider on this path: a
|
|
6
|
+
session's full rendered body, tool arguments and tool results included,
|
|
7
|
+
leaves this machine.
|
|
8
|
+
|
|
9
|
+
Reads complete session transcripts WITH uuid headers (``include_uuids=True``
|
|
10
|
+
puts each text step's uuid — the FIRST source_uuid, per CONTRACT-V2 — in
|
|
11
|
+
the header as ``[uuid=... role ts]``) and emits one row per detected
|
|
12
|
+
conflict pair, keyed on ``(turn_a_uuid, turn_b_uuid)``. Sessions with no
|
|
13
|
+
conflicts produce ZERO rows. Refused sessions ALSO produce zero pair rows
|
|
14
|
+
(a sentinel pair would poison the uuid-keyed conversation-time joins);
|
|
15
|
+
the refusal instead lands as a durable audit row in the
|
|
16
|
+
``analytics/refusals`` sidecar (``{pipeline, unit_id, reason,
|
|
17
|
+
refused_at}``) so refusal-skips are queryable, not just log lines.
|
|
18
|
+
|
|
19
|
+
Only ``interactive`` sessions are sent (``session_kind``, the rule atif-duck's
|
|
20
|
+
``session_outcomes.kind`` applies): a turn audit has no human turn and a
|
|
21
|
+
one-shot job has one, and a stance conflict between the agent and the user
|
|
22
|
+
needs the user to answer the agent at least once. Both kinds are
|
|
23
|
+
checkpointed as skipped without a call; growth re-admits.
|
|
24
|
+
|
|
25
|
+
The uuid-validity guard validates the model's returned ``turn_*_uuid``
|
|
26
|
+
values against the session's ``edges.jsonl`` uuid set before they land in
|
|
27
|
+
the parquet — a pair naming a uuid that is not a real raw-record uuid can
|
|
28
|
+
never be recovered on a conversation-time axis, so it is dropped at the
|
|
29
|
+
source. The guard FAILS CLOSED: when the uuid universe cannot be read at
|
|
30
|
+
all, every pair from that session is dropped rather than admitted
|
|
31
|
+
unverified, because a wrong uuid in the parquet is indistinguishable from
|
|
32
|
+
a right one to every downstream view.
|
|
33
|
+
|
|
34
|
+
Output schema (per parquet shard)::
|
|
35
|
+
|
|
36
|
+
session_id VARCHAR
|
|
37
|
+
turn_a_uuid VARCHAR (NOT NULL)
|
|
38
|
+
turn_b_uuid VARCHAR (NOT NULL)
|
|
39
|
+
conflict_kind VARCHAR (enum: disagreement|correction|reversal|impasse)
|
|
40
|
+
severity VARCHAR (enum: low|medium|high)
|
|
41
|
+
agent_position VARCHAR
|
|
42
|
+
user_position VARCHAR
|
|
43
|
+
confidence DOUBLE
|
|
44
|
+
detected_at TIMESTAMP
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import asyncio
|
|
50
|
+
import time
|
|
51
|
+
from datetime import UTC, datetime
|
|
52
|
+
from typing import TYPE_CHECKING, Any
|
|
53
|
+
|
|
54
|
+
import polars as pl
|
|
55
|
+
from loguru import logger
|
|
56
|
+
|
|
57
|
+
from atif_analytics.application.prompts import CONFLICTS_SYSTEM_PROMPT
|
|
58
|
+
from atif_analytics.application.use_cases._shared import (
|
|
59
|
+
RunBudget,
|
|
60
|
+
build_cache,
|
|
61
|
+
build_checkpoint,
|
|
62
|
+
build_provider,
|
|
63
|
+
build_retry_queue,
|
|
64
|
+
gather_under_budget,
|
|
65
|
+
pipeline_usage,
|
|
66
|
+
write_chunk_size,
|
|
67
|
+
write_refusal_audit,
|
|
68
|
+
)
|
|
69
|
+
from atif_analytics.domain.costs import estimate_cost_tokens, tokens_for_chars
|
|
70
|
+
from atif_analytics.domain.models import ConflictsResult
|
|
71
|
+
from atif_analytics.infrastructure.corpus_reader import CorpusReader
|
|
72
|
+
from atif_analytics.infrastructure.sqlite_state import checkpointer
|
|
73
|
+
|
|
74
|
+
if TYPE_CHECKING:
|
|
75
|
+
from atif_analytics.infrastructure.parquet_cache import ParquetCache
|
|
76
|
+
from atif_analytics.infrastructure.settings import AnalyticsSettings
|
|
77
|
+
from atif_analytics.infrastructure.sqlite_state.checkpointer import SqliteCheckpoint
|
|
78
|
+
from atif_analytics.infrastructure.sqlite_state.retry_queue import SqliteRetryQueue
|
|
79
|
+
from atif_models.domain.ports import LlmStructuredProvider
|
|
80
|
+
from atif_models.domain.registry import ModelSpec
|
|
81
|
+
|
|
82
|
+
_PARQUET_SCHEMA: dict[str, Any] = {
|
|
83
|
+
"session_id": pl.Utf8,
|
|
84
|
+
"turn_a_uuid": pl.Utf8,
|
|
85
|
+
"turn_b_uuid": pl.Utf8,
|
|
86
|
+
"conflict_kind": pl.Utf8,
|
|
87
|
+
"severity": pl.Utf8,
|
|
88
|
+
"agent_position": pl.Utf8,
|
|
89
|
+
"user_position": pl.Utf8,
|
|
90
|
+
"confidence": pl.Float64,
|
|
91
|
+
"detected_at": pl.Datetime("us", "UTC"),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#: Dry-run OUTPUT budget per session (~400 structured-output tokens). Input
|
|
95
|
+
#: is MEASURED from the rendered uuid-headed transcript (chars/4), never a
|
|
96
|
+
#: static prior: a real transcript runs an order of magnitude past any
|
|
97
|
+
#: fixed guess.
|
|
98
|
+
_AVG_OUT_TOKENS = 400
|
|
99
|
+
|
|
100
|
+
#: The one session kind conflicts spends on.
|
|
101
|
+
_CONFLICT_KIND = "interactive"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _already_done(cache: ParquetCache) -> set[str]:
|
|
105
|
+
"""Session ids already present in the cache."""
|
|
106
|
+
done_df = cache.read_all(columns=["session_id"])
|
|
107
|
+
if done_df is not None and done_df.height > 0:
|
|
108
|
+
return set(done_df["session_id"].to_list())
|
|
109
|
+
return set()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def _conflicts_async(
|
|
113
|
+
settings: AnalyticsSettings,
|
|
114
|
+
*,
|
|
115
|
+
reader: CorpusReader,
|
|
116
|
+
provider: LlmStructuredProvider,
|
|
117
|
+
since_days: int | None,
|
|
118
|
+
limit: int | None,
|
|
119
|
+
cache: ParquetCache,
|
|
120
|
+
checkpoint: SqliteCheckpoint,
|
|
121
|
+
retry: SqliteRetryQueue,
|
|
122
|
+
budget: RunBudget | None,
|
|
123
|
+
) -> int:
|
|
124
|
+
"""Async implementation behind :func:`detect_conflicts`."""
|
|
125
|
+
already = _already_done(cache)
|
|
126
|
+
|
|
127
|
+
bounds = reader.session_bounds(since_days=since_days, limit=limit)
|
|
128
|
+
unchanged_pending, skipped = checkpointer.filter_unchanged(
|
|
129
|
+
((sid, lt, mt) for sid, (lt, mt) in bounds.items()),
|
|
130
|
+
pipeline="conflicts",
|
|
131
|
+
checkpoint_db_path=checkpoint.db_path,
|
|
132
|
+
)
|
|
133
|
+
keep = set(unchanged_pending)
|
|
134
|
+
|
|
135
|
+
retry_ids = set(retry.drain(pipeline="conflicts"))
|
|
136
|
+
if retry_ids:
|
|
137
|
+
logger.info("conflicts: draining {} retry-queue entries", len(retry_ids))
|
|
138
|
+
keep |= retry_ids
|
|
139
|
+
|
|
140
|
+
# Retry queue is the single re-admission gate for failed units (see
|
|
141
|
+
# classify): a live entry blocks dispatch until drain says it's due.
|
|
142
|
+
blocked = retry.blocked_units(pipeline="conflicts")
|
|
143
|
+
if blocked & keep:
|
|
144
|
+
logger.info(
|
|
145
|
+
"conflicts: skipping {} sessions with live retry entries (backoff/attempts cap)",
|
|
146
|
+
len(blocked & keep),
|
|
147
|
+
)
|
|
148
|
+
keep -= blocked
|
|
149
|
+
|
|
150
|
+
# Session ceiling applied DURING the walk, not after: bounds is
|
|
151
|
+
# newest-first and the cap keeps the freshest sessions, so admitting
|
|
152
|
+
# past it only to slice back would render transcripts (up to 800K chars
|
|
153
|
+
# each) for sessions that can never run this tick. On a backlogged
|
|
154
|
+
# corpus that is the difference between 50 rendered strings and
|
|
155
|
+
# thousands held at once.
|
|
156
|
+
max_sessions = settings.llm_max_sessions_per_run
|
|
157
|
+
pending: list[tuple[str, str]] = []
|
|
158
|
+
empty_sids: list[str] = []
|
|
159
|
+
deferred = 0
|
|
160
|
+
for sid in bounds:
|
|
161
|
+
if sid in already and sid not in retry_ids:
|
|
162
|
+
continue
|
|
163
|
+
if sid not in keep:
|
|
164
|
+
continue
|
|
165
|
+
if len(pending) >= max_sessions:
|
|
166
|
+
deferred += 1
|
|
167
|
+
continue
|
|
168
|
+
# Kind gate before rendering (see the module docstring).
|
|
169
|
+
if reader.session_kind(sid) != _CONFLICT_KIND:
|
|
170
|
+
empty_sids.append(sid)
|
|
171
|
+
continue
|
|
172
|
+
text = reader.session_text(sid, include_uuids=True)
|
|
173
|
+
if not text:
|
|
174
|
+
# A session that renders to nothing yields no row and no LLM
|
|
175
|
+
# call, so without a checkpoint it re-renders every tick
|
|
176
|
+
# forever. Stamp it at current bounds; growth re-admits it.
|
|
177
|
+
empty_sids.append(sid)
|
|
178
|
+
continue
|
|
179
|
+
pending.append((sid, text))
|
|
180
|
+
|
|
181
|
+
if empty_sids:
|
|
182
|
+
checkpoint.mark_completed(
|
|
183
|
+
pipeline="conflicts",
|
|
184
|
+
rows=[(sid, *bounds.get(sid, (None, None))) for sid in empty_sids],
|
|
185
|
+
)
|
|
186
|
+
logger.info(
|
|
187
|
+
"conflicts: checkpointed {} non-interactive or empty-transcript sessions as skipped",
|
|
188
|
+
len(empty_sids),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if deferred:
|
|
192
|
+
logger.warning(
|
|
193
|
+
"conflicts: capped at {} sessions (llm_max_sessions_per_run); {} deferred",
|
|
194
|
+
max_sessions,
|
|
195
|
+
deferred,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if not pending:
|
|
199
|
+
logger.info("conflicts: no pending sessions (skipped={} via checkpoint)", skipped)
|
|
200
|
+
return 0
|
|
201
|
+
if skipped:
|
|
202
|
+
logger.info("conflicts: skipped {} sessions via checkpoint", skipped)
|
|
203
|
+
|
|
204
|
+
# Per-session set of real raw-record uuids for the returned-uuid guard.
|
|
205
|
+
# ``None`` means the edges file was unreadable — the guard then has no
|
|
206
|
+
# universe to check against and drops the session's pairs.
|
|
207
|
+
valid_uuids = {sid: reader.edges_uuids(sid) for sid, _ in pending}
|
|
208
|
+
|
|
209
|
+
chunk_size = write_chunk_size(settings.batch_size)
|
|
210
|
+
logger.info("conflicts: {} pending sessions", len(pending))
|
|
211
|
+
|
|
212
|
+
written_pairs = 0
|
|
213
|
+
processed_sessions = 0
|
|
214
|
+
i = 0
|
|
215
|
+
chunk_index = 0
|
|
216
|
+
total_chunks = (len(pending) + chunk_size - 1) // chunk_size
|
|
217
|
+
while i < len(pending):
|
|
218
|
+
chunk_index += 1
|
|
219
|
+
t0 = time.monotonic()
|
|
220
|
+
# Budget-checked sub-batches; ``i`` advances by what was actually
|
|
221
|
+
# SENT so a mid-chunk stop leaves the remainder unstamped. Conflicts
|
|
222
|
+
# runs on the LARGE model over full transcripts, so this is the
|
|
223
|
+
# stage where one unchecked gather overshoots hardest.
|
|
224
|
+
chunk, results = await gather_under_budget(
|
|
225
|
+
pending[i : i + chunk_size],
|
|
226
|
+
make_coro=lambda unit: provider.classify_structured(
|
|
227
|
+
system=CONFLICTS_SYSTEM_PROMPT,
|
|
228
|
+
prompt=unit[1],
|
|
229
|
+
schema=ConflictsResult,
|
|
230
|
+
),
|
|
231
|
+
budget=budget,
|
|
232
|
+
)
|
|
233
|
+
if not chunk:
|
|
234
|
+
logger.warning(
|
|
235
|
+
"conflicts: cost ceiling hit (${:.2f} >= ${:.2f}) — "
|
|
236
|
+
"aborting with {} sessions unstarted (nothing stamped)",
|
|
237
|
+
budget.spent_usd() if budget is not None else 0.0,
|
|
238
|
+
budget.max_cost_usd if budget is not None else 0.0,
|
|
239
|
+
len(pending) - i,
|
|
240
|
+
)
|
|
241
|
+
break
|
|
242
|
+
i += len(chunk)
|
|
243
|
+
now = datetime.now(UTC)
|
|
244
|
+
|
|
245
|
+
rows: list[dict[str, Any]] = []
|
|
246
|
+
errors = 0
|
|
247
|
+
ok_sids: set[str] = set()
|
|
248
|
+
refused: list[tuple[str, str]] = []
|
|
249
|
+
for (sid, _), res in zip(chunk, results, strict=True):
|
|
250
|
+
if isinstance(res, BaseException):
|
|
251
|
+
from atif_models.domain.ports import RefusalError
|
|
252
|
+
|
|
253
|
+
if isinstance(res, RefusalError):
|
|
254
|
+
# Terminal: zero PAIR rows (a sentinel pair would poison
|
|
255
|
+
# the uuid-keyed conversation-time joins — see the
|
|
256
|
+
# refusals sidecar decision in _shared.write_refusal_audit),
|
|
257
|
+
# but the refusal itself lands as a durable audit row.
|
|
258
|
+
logger.info("conflicts: {} refused (terminal) — audit sidecar row", sid)
|
|
259
|
+
ok_sids.add(sid)
|
|
260
|
+
refused.append((sid, str(res)))
|
|
261
|
+
continue
|
|
262
|
+
errors += 1
|
|
263
|
+
logger.warning("conflicts: {} failed (queued for retry): {}", sid, res)
|
|
264
|
+
retry.enqueue(pipeline="conflicts", unit_id=sid, error=str(res))
|
|
265
|
+
continue
|
|
266
|
+
ok_sids.add(sid)
|
|
267
|
+
for c in res.conflicts:
|
|
268
|
+
turn_a = c.turn_a_uuid
|
|
269
|
+
turn_b = c.turn_b_uuid
|
|
270
|
+
if not turn_a or not turn_b or turn_a == turn_b:
|
|
271
|
+
logger.warning(
|
|
272
|
+
"conflicts: {} returned a degenerate pair "
|
|
273
|
+
"(turn_a={!r}, turn_b={!r}) — skipping",
|
|
274
|
+
sid,
|
|
275
|
+
turn_a,
|
|
276
|
+
turn_b,
|
|
277
|
+
)
|
|
278
|
+
continue
|
|
279
|
+
# uuid-validity guard: a pair whose turn uuids are not BOTH
|
|
280
|
+
# real raw-record uuids for this session can never be
|
|
281
|
+
# recovered on a conversation-time axis. FAIL CLOSED — an
|
|
282
|
+
# unreadable edges file (None) yields no universe to check
|
|
283
|
+
# against, and admitting the pair anyway would put an
|
|
284
|
+
# unverified uuid into session_conflicts, where nothing
|
|
285
|
+
# downstream can tell it from a verified one.
|
|
286
|
+
session_uuids = valid_uuids.get(sid)
|
|
287
|
+
if session_uuids is None:
|
|
288
|
+
logger.warning(
|
|
289
|
+
"conflicts: {} has no readable uuid universe — dropping pair "
|
|
290
|
+
"(turn_a={!r}, turn_b={!r}); the validity guard cannot run",
|
|
291
|
+
sid,
|
|
292
|
+
turn_a,
|
|
293
|
+
turn_b,
|
|
294
|
+
)
|
|
295
|
+
continue
|
|
296
|
+
if turn_a not in session_uuids or turn_b not in session_uuids:
|
|
297
|
+
logger.warning(
|
|
298
|
+
"conflicts: {} returned a pair with non-record uuids "
|
|
299
|
+
"(turn_a={!r} valid={}, turn_b={!r} valid={}) — skipping",
|
|
300
|
+
sid,
|
|
301
|
+
turn_a,
|
|
302
|
+
turn_a in session_uuids,
|
|
303
|
+
turn_b,
|
|
304
|
+
turn_b in session_uuids,
|
|
305
|
+
)
|
|
306
|
+
continue
|
|
307
|
+
rows.append(
|
|
308
|
+
{
|
|
309
|
+
"session_id": sid,
|
|
310
|
+
"turn_a_uuid": turn_a,
|
|
311
|
+
"turn_b_uuid": turn_b,
|
|
312
|
+
"conflict_kind": c.conflict_kind,
|
|
313
|
+
"severity": c.severity,
|
|
314
|
+
"agent_position": c.agent_position,
|
|
315
|
+
"user_position": c.user_position,
|
|
316
|
+
"confidence": float(c.confidence),
|
|
317
|
+
"detected_at": now,
|
|
318
|
+
}
|
|
319
|
+
)
|
|
320
|
+
if rows:
|
|
321
|
+
cache.write_part(pl.DataFrame(rows, schema=_PARQUET_SCHEMA))
|
|
322
|
+
if refused:
|
|
323
|
+
write_refusal_audit(
|
|
324
|
+
settings.layout().refusals_dir, pipeline="conflicts", refusals=refused
|
|
325
|
+
)
|
|
326
|
+
if ok_sids:
|
|
327
|
+
checkpoint.mark_completed(
|
|
328
|
+
pipeline="conflicts",
|
|
329
|
+
rows=[(sid, *bounds.get(sid, (None, None))) for sid in ok_sids],
|
|
330
|
+
)
|
|
331
|
+
retry.mark_done(pipeline="conflicts", unit_ids=list(ok_sids))
|
|
332
|
+
written_pairs += len(rows)
|
|
333
|
+
processed_sessions += len(ok_sids)
|
|
334
|
+
logger.info(
|
|
335
|
+
"conflicts chunk {}/{}: {} sessions, {} pairs, {} errors, {:.1f}s",
|
|
336
|
+
chunk_index,
|
|
337
|
+
total_chunks,
|
|
338
|
+
len(ok_sids),
|
|
339
|
+
len(rows),
|
|
340
|
+
errors,
|
|
341
|
+
time.monotonic() - t0,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
logger.info(
|
|
345
|
+
"conflicts: processed {} sessions, wrote {} pair rows", processed_sessions, written_pairs
|
|
346
|
+
)
|
|
347
|
+
return processed_sessions
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def detect_conflicts(
|
|
351
|
+
settings: AnalyticsSettings,
|
|
352
|
+
*,
|
|
353
|
+
since_days: int | None = None,
|
|
354
|
+
limit: int | None = None,
|
|
355
|
+
dry_run: bool = True,
|
|
356
|
+
reader: CorpusReader | None = None,
|
|
357
|
+
provider: LlmStructuredProvider | None = None,
|
|
358
|
+
spec: ModelSpec | None = None,
|
|
359
|
+
cache: ParquetCache | None = None,
|
|
360
|
+
checkpoint: SqliteCheckpoint | None = None,
|
|
361
|
+
retry: SqliteRetryQueue | None = None,
|
|
362
|
+
budget: RunBudget | None = None,
|
|
363
|
+
) -> int | dict[str, Any]:
|
|
364
|
+
"""Detect stance conflicts per session; return the count of sessions processed.
|
|
365
|
+
|
|
366
|
+
The returned int counts SESSIONS, not pair rows: a session that produced
|
|
367
|
+
two conflict pairs and one that produced zero both count as one (and
|
|
368
|
+
both are checkpointed so a rerun is a no-op). ``dry_run`` (the default)
|
|
369
|
+
returns a plan dict instead.
|
|
370
|
+
"""
|
|
371
|
+
layout = settings.layout()
|
|
372
|
+
reader = (
|
|
373
|
+
reader
|
|
374
|
+
if reader is not None
|
|
375
|
+
else CorpusReader(settings.corpus_root, caps=settings.transcript_caps())
|
|
376
|
+
)
|
|
377
|
+
cache = cache if cache is not None else build_cache(layout.conflicts_dir)
|
|
378
|
+
|
|
379
|
+
if provider is None or spec is None:
|
|
380
|
+
provider, spec = build_provider(settings, "conflicts")
|
|
381
|
+
|
|
382
|
+
if dry_run:
|
|
383
|
+
# Measured estimation: input tokens from the actual rendered
|
|
384
|
+
# uuid-headed transcript (chars/4), capped like the real run.
|
|
385
|
+
already = _already_done(cache)
|
|
386
|
+
bounds = reader.session_bounds(since_days=since_days, limit=limit)
|
|
387
|
+
pending = [sid for sid in bounds if sid not in already]
|
|
388
|
+
max_sessions = settings.llm_max_sessions_per_run
|
|
389
|
+
capped: list[str] = []
|
|
390
|
+
skipped_kind = 0
|
|
391
|
+
for sid in pending:
|
|
392
|
+
if len(capped) >= max_sessions:
|
|
393
|
+
break
|
|
394
|
+
if reader.session_kind(sid) != _CONFLICT_KIND:
|
|
395
|
+
skipped_kind += 1
|
|
396
|
+
continue
|
|
397
|
+
capped.append(sid)
|
|
398
|
+
in_tokens = sum(
|
|
399
|
+
tokens_for_chars(len(text)) for text in reader.session_texts(capped, include_uuids=True)
|
|
400
|
+
)
|
|
401
|
+
out_tokens = len(capped) * _AVG_OUT_TOKENS
|
|
402
|
+
pricing = (spec.pricing_in or 0.0, spec.pricing_out or 0.0)
|
|
403
|
+
cost = estimate_cost_tokens(in_tokens, out_tokens, pricing)
|
|
404
|
+
logger.info(
|
|
405
|
+
"conflicts --dry-run: {} sessions ({} would run under the cap), "
|
|
406
|
+
"estimated cost ~${:.2f}",
|
|
407
|
+
len(pending),
|
|
408
|
+
len(capped),
|
|
409
|
+
cost,
|
|
410
|
+
)
|
|
411
|
+
return {
|
|
412
|
+
"pipeline": "conflicts",
|
|
413
|
+
"candidates": len(pending),
|
|
414
|
+
"skipped_not_interactive": skipped_kind,
|
|
415
|
+
"capped_candidates": len(capped),
|
|
416
|
+
"llm_calls": len(capped),
|
|
417
|
+
"estimated_input_tokens": in_tokens,
|
|
418
|
+
"estimated_output_tokens": out_tokens,
|
|
419
|
+
"estimated_cost_usd": round(cost, 4),
|
|
420
|
+
"max_sessions_per_run": max_sessions,
|
|
421
|
+
"max_cost_usd_per_run": settings.llm_max_cost_usd_per_run,
|
|
422
|
+
"model": spec.model_id,
|
|
423
|
+
"since_days": since_days,
|
|
424
|
+
"limit": limit,
|
|
425
|
+
"dry_run": True,
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
checkpoint = checkpoint if checkpoint is not None else build_checkpoint(layout.state_db_path)
|
|
429
|
+
retry = retry if retry is not None else build_retry_queue(layout.state_db_path)
|
|
430
|
+
if budget is not None:
|
|
431
|
+
budget.watch(provider, spec)
|
|
432
|
+
with pipeline_usage(provider, spec, "conflicts"):
|
|
433
|
+
return asyncio.run(
|
|
434
|
+
_conflicts_async(
|
|
435
|
+
settings,
|
|
436
|
+
reader=reader,
|
|
437
|
+
provider=provider,
|
|
438
|
+
since_days=since_days,
|
|
439
|
+
limit=limit,
|
|
440
|
+
cache=cache,
|
|
441
|
+
checkpoint=checkpoint,
|
|
442
|
+
retry=retry,
|
|
443
|
+
budget=budget,
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
__all__ = ["detect_conflicts"]
|