langparse 0.1.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.
- langparse/__init__.py +55 -0
- langparse/autoparser.py +25 -0
- langparse/chunkers/__init__.py +12 -0
- langparse/chunkers/blocks.py +151 -0
- langparse/chunkers/profiles.py +53 -0
- langparse/chunkers/registry.py +38 -0
- langparse/chunkers/semantic.py +242 -0
- langparse/chunkers/text.py +96 -0
- langparse/chunkers/workbook.py +942 -0
- langparse/cli.py +329 -0
- langparse/config.py +169 -0
- langparse/core/__init__.py +0 -0
- langparse/core/chunker.py +16 -0
- langparse/core/engine.py +37 -0
- langparse/core/parser.py +35 -0
- langparse/core/rendering.py +49 -0
- langparse/engines/__init__.py +1 -0
- langparse/engines/pdf/__init__.py +1 -0
- langparse/engines/pdf/deepdoc/__init__.py +55 -0
- langparse/engines/pdf/deepdoc/layout_recognizer.py +235 -0
- langparse/engines/pdf/deepdoc/model_loader.py +101 -0
- langparse/engines/pdf/deepdoc/ocr.py +641 -0
- langparse/engines/pdf/deepdoc/operators.py +684 -0
- langparse/engines/pdf/deepdoc/pdf_parser.py +1894 -0
- langparse/engines/pdf/deepdoc/postprocess.py +339 -0
- langparse/engines/pdf/deepdoc/recognizer.py +418 -0
- langparse/engines/pdf/deepdoc/rendering.py +210 -0
- langparse/engines/pdf/deepdoc/table_structure_recognizer.py +559 -0
- langparse/engines/pdf/deepdoc/tokenizer.py +30 -0
- langparse/engines/pdf/deepdoc/utils.py +36 -0
- langparse/engines/pdf/deepdoc_engine.py +164 -0
- langparse/engines/pdf/mineru.py +259 -0
- langparse/engines/pdf/mineru_client.py +318 -0
- langparse/engines/pdf/mineru_service.py +225 -0
- langparse/engines/pdf/ocr.py +101 -0
- langparse/engines/pdf/other.py +20 -0
- langparse/engines/pdf/simple.py +134 -0
- langparse/engines/pdf/vision_llm.py +27 -0
- langparse/errors.py +70 -0
- langparse/logging.py +27 -0
- langparse/metrics.py +129 -0
- langparse/parsers/__init__.py +0 -0
- langparse/parsers/docx_parser.py +114 -0
- langparse/parsers/excel_parser.py +220 -0
- langparse/parsers/markdown_parser.py +34 -0
- langparse/parsers/pdf_parser.py +31 -0
- langparse/parsers/registry.py +48 -0
- langparse/parsers/sniff.py +72 -0
- langparse/progress.py +77 -0
- langparse/py.typed +0 -0
- langparse/services/__init__.py +11 -0
- langparse/services/batch_service.py +339 -0
- langparse/services/benchmark_service.py +202 -0
- langparse/services/fidelity.py +154 -0
- langparse/services/output_paths.py +86 -0
- langparse/services/parse_service.py +523 -0
- langparse/services/quality.py +65 -0
- langparse/services/workbook_ambiguity_benchmark.py +563 -0
- langparse/services/workbook_quality_benchmark.py +230 -0
- langparse/types.py +97 -0
- langparse/workbooks/__init__.py +103 -0
- langparse/workbooks/adapters.py +474 -0
- langparse/workbooks/assembly.py +993 -0
- langparse/workbooks/blocks.py +209 -0
- langparse/workbooks/bundle-v1.schema.json +71 -0
- langparse/workbooks/bundle.py +341 -0
- langparse/workbooks/classification.py +393 -0
- langparse/workbooks/continuation.py +577 -0
- langparse/workbooks/evaluation/__init__.py +45 -0
- langparse/workbooks/evaluation/evaluator.py +381 -0
- langparse/workbooks/evaluation/schema.py +419 -0
- langparse/workbooks/labels.py +14 -0
- langparse/workbooks/lineage.py +117 -0
- langparse/workbooks/modeling/__init__.py +52 -0
- langparse/workbooks/modeling/cache.py +20 -0
- langparse/workbooks/modeling/config.py +87 -0
- langparse/workbooks/modeling/contract.py +628 -0
- langparse/workbooks/modeling/disambiguation.py +800 -0
- langparse/workbooks/modeling/openai_adapter.py +192 -0
- langparse/workbooks/modeling/policy.py +79 -0
- langparse/workbooks/modeling/ports.py +44 -0
- langparse/workbooks/modeling/pricing.py +17 -0
- langparse/workbooks/modeling/types.py +251 -0
- langparse/workbooks/objects.py +229 -0
- langparse/workbooks/quality/__init__.py +23 -0
- langparse/workbooks/quality/bundle.py +53 -0
- langparse/workbooks/quality/evaluator.py +266 -0
- langparse/workbooks/quality/facts.py +142 -0
- langparse/workbooks/quality/schema.py +462 -0
- langparse/workbooks/reference_types.py +73 -0
- langparse/workbooks/references.py +178 -0
- langparse/workbooks/regions.py +932 -0
- langparse/workbooks/rendering.py +222 -0
- langparse/workbooks/tables.py +477 -0
- langparse/workbooks/types.py +257 -0
- langparse-0.1.0.dist-info/METADATA +790 -0
- langparse-0.1.0.dist-info/RECORD +101 -0
- langparse-0.1.0.dist-info/WHEEL +5 -0
- langparse-0.1.0.dist-info/entry_points.txt +2 -0
- langparse-0.1.0.dist-info/licenses/LICENSE +192 -0
- langparse-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
6
|
+
from dataclasses import fields
|
|
7
|
+
from threading import RLock
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from langparse.types import ParseDiagnostics
|
|
11
|
+
|
|
12
|
+
from .cache import MemoryDecisionCache
|
|
13
|
+
from .contract import (
|
|
14
|
+
_copy_provider_reply,
|
|
15
|
+
_validate_case,
|
|
16
|
+
_validate_model_identity,
|
|
17
|
+
build_model_request,
|
|
18
|
+
decode_model_reply,
|
|
19
|
+
response_checksum,
|
|
20
|
+
)
|
|
21
|
+
from .policy import WorkbookDisambiguation
|
|
22
|
+
from .ports import (
|
|
23
|
+
RequiredWorkbookDisambiguationError,
|
|
24
|
+
WorkbookModelResponseError,
|
|
25
|
+
WorkbookStructureModelAdapter,
|
|
26
|
+
)
|
|
27
|
+
from .types import (
|
|
28
|
+
REGION_PRIVACY_VERSION,
|
|
29
|
+
REGION_PROMPT_VERSION,
|
|
30
|
+
REGION_RULE_VERSION,
|
|
31
|
+
REGION_SCHEMA_VERSION,
|
|
32
|
+
REGION_VALIDATOR_VERSION,
|
|
33
|
+
ModelCallAudit,
|
|
34
|
+
ModelIdentity,
|
|
35
|
+
ProviderReply,
|
|
36
|
+
RegionAmbiguityCase,
|
|
37
|
+
RegionModelDecision,
|
|
38
|
+
RegionResolution,
|
|
39
|
+
RegionResolutionBatch,
|
|
40
|
+
WorkbookModelMode,
|
|
41
|
+
WorkbookModelPolicy,
|
|
42
|
+
WorkbookModelRequest,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class WorkbookRegionDisambiguator:
|
|
47
|
+
"""Resolve workbook region ambiguity behind the local model contract."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
cache: MemoryDecisionCache | None = None,
|
|
52
|
+
clock: Callable[[], float] = time.monotonic,
|
|
53
|
+
) -> None:
|
|
54
|
+
self._cache = MemoryDecisionCache() if cache is None else cache
|
|
55
|
+
self._clock = clock
|
|
56
|
+
self._resolve_lock = RLock()
|
|
57
|
+
|
|
58
|
+
def resolve(
|
|
59
|
+
self,
|
|
60
|
+
cases: Iterable[RegionAmbiguityCase],
|
|
61
|
+
configured: WorkbookDisambiguation,
|
|
62
|
+
) -> RegionResolutionBatch:
|
|
63
|
+
with self._resolve_lock:
|
|
64
|
+
return self._resolve(cases, configured)
|
|
65
|
+
|
|
66
|
+
def _resolve(
|
|
67
|
+
self,
|
|
68
|
+
cases: Iterable[RegionAmbiguityCase],
|
|
69
|
+
configured: WorkbookDisambiguation,
|
|
70
|
+
) -> RegionResolutionBatch:
|
|
71
|
+
ordered_cases = tuple(cases)
|
|
72
|
+
if not ordered_cases:
|
|
73
|
+
return RegionResolutionBatch(resolutions=())
|
|
74
|
+
|
|
75
|
+
self._validate_cases(ordered_cases)
|
|
76
|
+
if configured.mode is WorkbookModelMode.OFF:
|
|
77
|
+
return RegionResolutionBatch(
|
|
78
|
+
resolutions=tuple(_fallback_resolution(case) for case in ordered_cases)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
policy = configured.policy
|
|
82
|
+
visible_limit = policy.max_cases
|
|
83
|
+
eligible_visible_cases = tuple(
|
|
84
|
+
case
|
|
85
|
+
for index, case in enumerate(_visible_cases(ordered_cases))
|
|
86
|
+
if index < visible_limit
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
kill_switch_active = policy.kill_switch or os.environ.get("LANGPARSE_DISABLE_MODEL") in (
|
|
90
|
+
"1",
|
|
91
|
+
"true",
|
|
92
|
+
"TRUE",
|
|
93
|
+
"yes",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
identity, identity_error = (
|
|
97
|
+
(None, None)
|
|
98
|
+
if kill_switch_active
|
|
99
|
+
else self._read_identity(configured, eligible_visible_cases)
|
|
100
|
+
)
|
|
101
|
+
adapter = configured.adapter
|
|
102
|
+
workbook_started = self._clock() if eligible_visible_cases else None
|
|
103
|
+
deadline = (
|
|
104
|
+
workbook_started + policy.workbook_timeout_seconds
|
|
105
|
+
if workbook_started is not None
|
|
106
|
+
else None
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
resolutions: list[RegionResolution] = []
|
|
110
|
+
audits: list[ModelCallAudit] = []
|
|
111
|
+
unresolved_case_ids: list[str] = []
|
|
112
|
+
call_budget = [0]
|
|
113
|
+
quota_tracker = {"tokens": 0, "cost_usd": 0.0}
|
|
114
|
+
visible_index = 0
|
|
115
|
+
for case in ordered_cases:
|
|
116
|
+
if case.sheet_visibility != "visible":
|
|
117
|
+
resolution, audit = self._local_failure(
|
|
118
|
+
case,
|
|
119
|
+
configured.mode,
|
|
120
|
+
outcome="hidden_sheet",
|
|
121
|
+
)
|
|
122
|
+
elif kill_switch_active:
|
|
123
|
+
resolution, audit = self._local_failure(
|
|
124
|
+
case,
|
|
125
|
+
configured.mode,
|
|
126
|
+
outcome="kill_switch_activated",
|
|
127
|
+
)
|
|
128
|
+
visible_index += 1
|
|
129
|
+
elif visible_index >= visible_limit:
|
|
130
|
+
resolution, audit = self._local_failure(
|
|
131
|
+
case,
|
|
132
|
+
configured.mode,
|
|
133
|
+
identity=identity,
|
|
134
|
+
outcome="limit_exceeded",
|
|
135
|
+
)
|
|
136
|
+
visible_index += 1
|
|
137
|
+
elif identity_error is not None:
|
|
138
|
+
resolution, audit = self._local_failure(
|
|
139
|
+
case,
|
|
140
|
+
configured.mode,
|
|
141
|
+
outcome="adapter_error",
|
|
142
|
+
error=identity_error,
|
|
143
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
144
|
+
)
|
|
145
|
+
visible_index += 1
|
|
146
|
+
else:
|
|
147
|
+
assert identity is not None
|
|
148
|
+
assert deadline is not None
|
|
149
|
+
assert adapter is not None
|
|
150
|
+
resolution, audit = self._resolve_visible_case(
|
|
151
|
+
case,
|
|
152
|
+
configured.mode,
|
|
153
|
+
adapter,
|
|
154
|
+
identity,
|
|
155
|
+
policy,
|
|
156
|
+
workbook_started,
|
|
157
|
+
deadline,
|
|
158
|
+
call_budget,
|
|
159
|
+
quota_tracker,
|
|
160
|
+
)
|
|
161
|
+
visible_index += 1
|
|
162
|
+
|
|
163
|
+
resolutions.append(resolution)
|
|
164
|
+
audits.append(audit)
|
|
165
|
+
if (
|
|
166
|
+
configured.mode is WorkbookModelMode.REQUIRED
|
|
167
|
+
and resolution.status == "local_fallback"
|
|
168
|
+
):
|
|
169
|
+
unresolved_case_ids.append(case.case_id)
|
|
170
|
+
|
|
171
|
+
if unresolved_case_ids:
|
|
172
|
+
raise RequiredWorkbookDisambiguationError(
|
|
173
|
+
tuple(unresolved_case_ids),
|
|
174
|
+
ParseDiagnostics(
|
|
175
|
+
status="failed",
|
|
176
|
+
model_calls=[_audit_payload(audit) for audit in audits],
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
return RegionResolutionBatch(resolutions=tuple(resolutions))
|
|
180
|
+
|
|
181
|
+
def _validate_cases(self, cases: tuple[RegionAmbiguityCase, ...]) -> None:
|
|
182
|
+
for case in cases:
|
|
183
|
+
_validate_case(case, allow_hidden_sheet=True)
|
|
184
|
+
|
|
185
|
+
def _read_identity(
|
|
186
|
+
self,
|
|
187
|
+
configured: WorkbookDisambiguation,
|
|
188
|
+
eligible_visible_cases: tuple[RegionAmbiguityCase, ...],
|
|
189
|
+
) -> tuple[ModelIdentity | None, Exception | None]:
|
|
190
|
+
if not eligible_visible_cases:
|
|
191
|
+
return None, None
|
|
192
|
+
assert configured.adapter is not None
|
|
193
|
+
try:
|
|
194
|
+
identity = configured.adapter.identity
|
|
195
|
+
_validate_model_identity(identity)
|
|
196
|
+
return identity, None
|
|
197
|
+
except Exception as error:
|
|
198
|
+
return None, error
|
|
199
|
+
|
|
200
|
+
def _resolve_visible_case(
|
|
201
|
+
self,
|
|
202
|
+
case: RegionAmbiguityCase,
|
|
203
|
+
mode: WorkbookModelMode,
|
|
204
|
+
adapter: WorkbookStructureModelAdapter,
|
|
205
|
+
identity: ModelIdentity,
|
|
206
|
+
policy: WorkbookModelPolicy,
|
|
207
|
+
workbook_started: float | None,
|
|
208
|
+
deadline: float,
|
|
209
|
+
call_budget: list[int],
|
|
210
|
+
quota_tracker: dict[str, Any],
|
|
211
|
+
) -> tuple[RegionResolution, ModelCallAudit]:
|
|
212
|
+
if (
|
|
213
|
+
policy.max_tokens_per_workbook is not None
|
|
214
|
+
and quota_tracker["tokens"] >= policy.max_tokens_per_workbook
|
|
215
|
+
):
|
|
216
|
+
return self._local_failure(
|
|
217
|
+
case,
|
|
218
|
+
mode,
|
|
219
|
+
identity=identity,
|
|
220
|
+
outcome="quota_exceeded",
|
|
221
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
222
|
+
)
|
|
223
|
+
if (
|
|
224
|
+
policy.max_cost_usd_per_workbook is not None
|
|
225
|
+
and quota_tracker["cost_usd"] >= policy.max_cost_usd_per_workbook
|
|
226
|
+
):
|
|
227
|
+
return self._local_failure(
|
|
228
|
+
case,
|
|
229
|
+
mode,
|
|
230
|
+
identity=identity,
|
|
231
|
+
outcome="quota_exceeded",
|
|
232
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if len(case.cells) > policy.max_cells_per_case:
|
|
236
|
+
return self._local_failure(
|
|
237
|
+
case,
|
|
238
|
+
mode,
|
|
239
|
+
identity=identity,
|
|
240
|
+
outcome="cell_limit_exceeded",
|
|
241
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
request = build_model_request(case, identity)
|
|
246
|
+
except Exception as error:
|
|
247
|
+
return self._local_failure(
|
|
248
|
+
case,
|
|
249
|
+
mode,
|
|
250
|
+
identity=identity,
|
|
251
|
+
outcome="request_error",
|
|
252
|
+
error=error,
|
|
253
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
254
|
+
)
|
|
255
|
+
request_bytes = len(request.body)
|
|
256
|
+
if request_bytes > policy.max_request_bytes:
|
|
257
|
+
return self._local_failure(
|
|
258
|
+
case,
|
|
259
|
+
mode,
|
|
260
|
+
identity=identity,
|
|
261
|
+
request=request,
|
|
262
|
+
request_bytes=request_bytes,
|
|
263
|
+
outcome="request_too_large",
|
|
264
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
cached_body = self._cache.get(request.request_checksum)
|
|
269
|
+
except Exception as error:
|
|
270
|
+
return self._local_failure(
|
|
271
|
+
case,
|
|
272
|
+
mode,
|
|
273
|
+
identity=identity,
|
|
274
|
+
request=request,
|
|
275
|
+
request_bytes=request_bytes,
|
|
276
|
+
cache_status="error",
|
|
277
|
+
outcome="cache_error",
|
|
278
|
+
error=error,
|
|
279
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
280
|
+
)
|
|
281
|
+
if cached_body is not None:
|
|
282
|
+
return self._resolve_cached(
|
|
283
|
+
case,
|
|
284
|
+
mode,
|
|
285
|
+
identity,
|
|
286
|
+
request,
|
|
287
|
+
cached_body,
|
|
288
|
+
policy,
|
|
289
|
+
workbook_started,
|
|
290
|
+
)
|
|
291
|
+
return self._call_adapter(
|
|
292
|
+
case,
|
|
293
|
+
mode,
|
|
294
|
+
adapter,
|
|
295
|
+
identity,
|
|
296
|
+
request,
|
|
297
|
+
policy,
|
|
298
|
+
workbook_started,
|
|
299
|
+
deadline,
|
|
300
|
+
call_budget,
|
|
301
|
+
quota_tracker,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def _resolve_cached(
|
|
305
|
+
self,
|
|
306
|
+
case: RegionAmbiguityCase,
|
|
307
|
+
mode: WorkbookModelMode,
|
|
308
|
+
identity: ModelIdentity,
|
|
309
|
+
request: WorkbookModelRequest,
|
|
310
|
+
cached_body: bytes,
|
|
311
|
+
policy: WorkbookModelPolicy,
|
|
312
|
+
workbook_started: float | None,
|
|
313
|
+
) -> tuple[RegionResolution, ModelCallAudit]:
|
|
314
|
+
try:
|
|
315
|
+
reply = ProviderReply(body=cached_body, provider_request_id=None)
|
|
316
|
+
decision = decode_model_reply(
|
|
317
|
+
reply,
|
|
318
|
+
request,
|
|
319
|
+
max_response_bytes=policy.max_response_bytes,
|
|
320
|
+
)
|
|
321
|
+
except Exception as error:
|
|
322
|
+
safe_cached_body = cached_body if type(cached_body) is bytes else None
|
|
323
|
+
return self._local_failure(
|
|
324
|
+
case,
|
|
325
|
+
mode,
|
|
326
|
+
identity=identity,
|
|
327
|
+
request=request,
|
|
328
|
+
request_bytes=len(request.body),
|
|
329
|
+
response_body=safe_cached_body,
|
|
330
|
+
cache_status="corrupt",
|
|
331
|
+
outcome="corrupt_cache",
|
|
332
|
+
error=error,
|
|
333
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
334
|
+
)
|
|
335
|
+
return self._decision_result(
|
|
336
|
+
case,
|
|
337
|
+
mode,
|
|
338
|
+
identity,
|
|
339
|
+
request,
|
|
340
|
+
cached_body,
|
|
341
|
+
decision,
|
|
342
|
+
cache_status="hit",
|
|
343
|
+
attempts=0,
|
|
344
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def _call_adapter(
|
|
348
|
+
self,
|
|
349
|
+
case: RegionAmbiguityCase,
|
|
350
|
+
mode: WorkbookModelMode,
|
|
351
|
+
adapter: WorkbookStructureModelAdapter,
|
|
352
|
+
identity: ModelIdentity,
|
|
353
|
+
request: WorkbookModelRequest,
|
|
354
|
+
policy: WorkbookModelPolicy,
|
|
355
|
+
workbook_started: float | None,
|
|
356
|
+
deadline: float,
|
|
357
|
+
call_budget: list[int],
|
|
358
|
+
quota_tracker: dict[str, Any],
|
|
359
|
+
) -> tuple[RegionResolution, ModelCallAudit]:
|
|
360
|
+
attempts = 0
|
|
361
|
+
last_error: Exception | None = None
|
|
362
|
+
last_body: bytes | None = None
|
|
363
|
+
assert mode is not WorkbookModelMode.OFF
|
|
364
|
+
|
|
365
|
+
while attempts < policy.max_attempts:
|
|
366
|
+
if _quota_limit_reached(policy, quota_tracker):
|
|
367
|
+
return self._local_failure(
|
|
368
|
+
case,
|
|
369
|
+
mode,
|
|
370
|
+
identity=identity,
|
|
371
|
+
request=request,
|
|
372
|
+
request_bytes=len(request.body),
|
|
373
|
+
response_body=last_body,
|
|
374
|
+
cache_status="miss",
|
|
375
|
+
attempts=attempts,
|
|
376
|
+
outcome="quota_exceeded",
|
|
377
|
+
error=last_error,
|
|
378
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
379
|
+
)
|
|
380
|
+
remaining = deadline - self._clock()
|
|
381
|
+
if remaining <= 0:
|
|
382
|
+
return self._local_failure(
|
|
383
|
+
case,
|
|
384
|
+
mode,
|
|
385
|
+
identity=identity,
|
|
386
|
+
request=request,
|
|
387
|
+
request_bytes=len(request.body),
|
|
388
|
+
response_body=last_body,
|
|
389
|
+
cache_status="miss",
|
|
390
|
+
attempts=attempts,
|
|
391
|
+
outcome="deadline_exceeded",
|
|
392
|
+
error=last_error,
|
|
393
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
394
|
+
)
|
|
395
|
+
if call_budget[0] >= policy.max_calls:
|
|
396
|
+
return self._local_failure(
|
|
397
|
+
case,
|
|
398
|
+
mode,
|
|
399
|
+
identity=identity,
|
|
400
|
+
request=request,
|
|
401
|
+
request_bytes=len(request.body),
|
|
402
|
+
response_body=last_body,
|
|
403
|
+
cache_status="miss",
|
|
404
|
+
attempts=attempts,
|
|
405
|
+
outcome="limit_exceeded",
|
|
406
|
+
error=last_error,
|
|
407
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
408
|
+
)
|
|
409
|
+
attempts += 1
|
|
410
|
+
call_budget[0] += 1
|
|
411
|
+
try:
|
|
412
|
+
reply = _copy_provider_reply(
|
|
413
|
+
adapter.complete(
|
|
414
|
+
request,
|
|
415
|
+
timeout_seconds=min(policy.timeout_seconds, remaining),
|
|
416
|
+
)
|
|
417
|
+
)
|
|
418
|
+
if self._clock() >= deadline:
|
|
419
|
+
return self._local_failure(
|
|
420
|
+
case,
|
|
421
|
+
mode,
|
|
422
|
+
identity=identity,
|
|
423
|
+
request=request,
|
|
424
|
+
request_bytes=len(request.body),
|
|
425
|
+
cache_status="miss",
|
|
426
|
+
attempts=attempts,
|
|
427
|
+
outcome="deadline_exceeded",
|
|
428
|
+
error=TimeoutError(),
|
|
429
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
430
|
+
)
|
|
431
|
+
last_body = reply.body
|
|
432
|
+
from .pricing import calculate_cost_usd
|
|
433
|
+
|
|
434
|
+
usage = reply.usage
|
|
435
|
+
if _quota_usage_unavailable(policy, usage):
|
|
436
|
+
return self._local_failure(
|
|
437
|
+
case,
|
|
438
|
+
mode,
|
|
439
|
+
identity=identity,
|
|
440
|
+
request=request,
|
|
441
|
+
request_bytes=len(request.body),
|
|
442
|
+
response_body=reply.body,
|
|
443
|
+
cache_status="miss",
|
|
444
|
+
attempts=attempts,
|
|
445
|
+
outcome="quota_unavailable",
|
|
446
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
447
|
+
)
|
|
448
|
+
p_tok = usage.get("prompt_tokens", 0)
|
|
449
|
+
c_tok = usage.get("completion_tokens", 0)
|
|
450
|
+
t_tok = max(usage.get("total_tokens", 0), p_tok + c_tok)
|
|
451
|
+
quota_tracker["tokens"] += t_tok
|
|
452
|
+
if policy.max_cost_usd_per_workbook is not None:
|
|
453
|
+
assert policy.input_cost_usd_per_million is not None
|
|
454
|
+
assert policy.output_cost_usd_per_million is not None
|
|
455
|
+
quota_tracker["cost_usd"] += calculate_cost_usd(
|
|
456
|
+
p_tok,
|
|
457
|
+
c_tok,
|
|
458
|
+
input_cost_usd_per_million=policy.input_cost_usd_per_million,
|
|
459
|
+
output_cost_usd_per_million=policy.output_cost_usd_per_million,
|
|
460
|
+
)
|
|
461
|
+
decision = decode_model_reply(
|
|
462
|
+
reply,
|
|
463
|
+
request,
|
|
464
|
+
max_response_bytes=policy.max_response_bytes,
|
|
465
|
+
)
|
|
466
|
+
except Exception as error:
|
|
467
|
+
last_error = error
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
response_checksum(reply.body)
|
|
472
|
+
except Exception as error:
|
|
473
|
+
return self._local_failure(
|
|
474
|
+
case,
|
|
475
|
+
mode,
|
|
476
|
+
identity=identity,
|
|
477
|
+
request=request,
|
|
478
|
+
request_bytes=len(request.body),
|
|
479
|
+
response_body=reply.body,
|
|
480
|
+
cache_status="miss",
|
|
481
|
+
attempts=attempts,
|
|
482
|
+
outcome="checksum_error",
|
|
483
|
+
error=error,
|
|
484
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
485
|
+
)
|
|
486
|
+
try:
|
|
487
|
+
self._cache.put(request.request_checksum, reply.body)
|
|
488
|
+
except Exception as error:
|
|
489
|
+
return self._local_failure(
|
|
490
|
+
case,
|
|
491
|
+
mode,
|
|
492
|
+
identity=identity,
|
|
493
|
+
request=request,
|
|
494
|
+
request_bytes=len(request.body),
|
|
495
|
+
response_body=reply.body,
|
|
496
|
+
cache_status="error",
|
|
497
|
+
attempts=attempts,
|
|
498
|
+
outcome="cache_error",
|
|
499
|
+
error=error,
|
|
500
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
501
|
+
)
|
|
502
|
+
return self._decision_result(
|
|
503
|
+
case,
|
|
504
|
+
mode,
|
|
505
|
+
identity,
|
|
506
|
+
request,
|
|
507
|
+
reply.body,
|
|
508
|
+
decision,
|
|
509
|
+
cache_status="miss",
|
|
510
|
+
attempts=attempts,
|
|
511
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
outcome = "timeout" if isinstance(last_error, TimeoutError) else "invalid_response"
|
|
515
|
+
if last_error is not None and not _is_response_error(last_error):
|
|
516
|
+
outcome = "timeout" if isinstance(last_error, TimeoutError) else "adapter_error"
|
|
517
|
+
return self._local_failure(
|
|
518
|
+
case,
|
|
519
|
+
mode,
|
|
520
|
+
identity=identity,
|
|
521
|
+
request=request,
|
|
522
|
+
request_bytes=len(request.body),
|
|
523
|
+
response_body=last_body,
|
|
524
|
+
cache_status="miss",
|
|
525
|
+
attempts=attempts,
|
|
526
|
+
outcome=outcome,
|
|
527
|
+
error=last_error,
|
|
528
|
+
elapsed_ms=self._elapsed_ms(workbook_started),
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
def _decision_result(
|
|
532
|
+
self,
|
|
533
|
+
case: RegionAmbiguityCase,
|
|
534
|
+
mode: WorkbookModelMode,
|
|
535
|
+
identity: ModelIdentity,
|
|
536
|
+
request: WorkbookModelRequest,
|
|
537
|
+
response_body: bytes,
|
|
538
|
+
decision: RegionModelDecision,
|
|
539
|
+
*,
|
|
540
|
+
cache_status: str,
|
|
541
|
+
attempts: int,
|
|
542
|
+
elapsed_ms: int,
|
|
543
|
+
) -> tuple[RegionResolution, ModelCallAudit]:
|
|
544
|
+
try:
|
|
545
|
+
response_checksum(response_body)
|
|
546
|
+
except Exception as error:
|
|
547
|
+
return self._local_failure(
|
|
548
|
+
case,
|
|
549
|
+
mode,
|
|
550
|
+
identity=identity,
|
|
551
|
+
request=request,
|
|
552
|
+
request_bytes=len(request.body),
|
|
553
|
+
response_body=response_body,
|
|
554
|
+
cache_status=cache_status,
|
|
555
|
+
attempts=attempts,
|
|
556
|
+
outcome="checksum_error",
|
|
557
|
+
error=error,
|
|
558
|
+
elapsed_ms=elapsed_ms,
|
|
559
|
+
)
|
|
560
|
+
if decision.status == "abstained":
|
|
561
|
+
return self._local_failure(
|
|
562
|
+
case,
|
|
563
|
+
mode,
|
|
564
|
+
identity=identity,
|
|
565
|
+
request=request,
|
|
566
|
+
request_bytes=len(request.body),
|
|
567
|
+
response_body=response_body,
|
|
568
|
+
cache_status=cache_status,
|
|
569
|
+
attempts=attempts,
|
|
570
|
+
outcome="abstained",
|
|
571
|
+
reported_confidence=decision.reported_confidence,
|
|
572
|
+
elapsed_ms=elapsed_ms,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
assert decision.choice_id is not None
|
|
576
|
+
audit = _audit(
|
|
577
|
+
case,
|
|
578
|
+
identity=identity,
|
|
579
|
+
request=request,
|
|
580
|
+
request_bytes=len(request.body),
|
|
581
|
+
response_body=response_body,
|
|
582
|
+
cache_status=cache_status,
|
|
583
|
+
attempts=attempts,
|
|
584
|
+
outcome="selected",
|
|
585
|
+
selected_choice_id=decision.choice_id,
|
|
586
|
+
reported_confidence=decision.reported_confidence,
|
|
587
|
+
elapsed_ms=elapsed_ms,
|
|
588
|
+
mode=mode,
|
|
589
|
+
)
|
|
590
|
+
status = "cache_selected" if cache_status == "hit" else "model_selected"
|
|
591
|
+
return (
|
|
592
|
+
RegionResolution(
|
|
593
|
+
case_id=case.case_id,
|
|
594
|
+
choice_id=decision.choice_id,
|
|
595
|
+
status=status,
|
|
596
|
+
audit=audit,
|
|
597
|
+
),
|
|
598
|
+
audit,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
def _local_failure(
|
|
602
|
+
self,
|
|
603
|
+
case: RegionAmbiguityCase,
|
|
604
|
+
mode: WorkbookModelMode,
|
|
605
|
+
*,
|
|
606
|
+
identity: ModelIdentity | None = None,
|
|
607
|
+
request: WorkbookModelRequest | None = None,
|
|
608
|
+
request_bytes: int = 0,
|
|
609
|
+
response_body: bytes | None = None,
|
|
610
|
+
cache_status: str = "not_checked",
|
|
611
|
+
attempts: int = 0,
|
|
612
|
+
elapsed_ms: int = 0,
|
|
613
|
+
outcome: str,
|
|
614
|
+
selected_choice_id: str | None = None,
|
|
615
|
+
reported_confidence: float | None = None,
|
|
616
|
+
error: Exception | None = None,
|
|
617
|
+
) -> tuple[RegionResolution, ModelCallAudit]:
|
|
618
|
+
audit = _audit(
|
|
619
|
+
case,
|
|
620
|
+
identity=identity,
|
|
621
|
+
request=request,
|
|
622
|
+
request_bytes=request_bytes,
|
|
623
|
+
response_body=response_body,
|
|
624
|
+
cache_status=cache_status,
|
|
625
|
+
attempts=attempts,
|
|
626
|
+
elapsed_ms=elapsed_ms,
|
|
627
|
+
outcome=outcome,
|
|
628
|
+
selected_choice_id=selected_choice_id,
|
|
629
|
+
reported_confidence=reported_confidence,
|
|
630
|
+
error=error,
|
|
631
|
+
mode=mode,
|
|
632
|
+
)
|
|
633
|
+
return _fallback_resolution(case, audit), audit
|
|
634
|
+
|
|
635
|
+
def _elapsed_ms(self, workbook_started: float | None) -> int:
|
|
636
|
+
if workbook_started is None:
|
|
637
|
+
return 0
|
|
638
|
+
return max(0, int(round((self._clock() - workbook_started) * 1000)))
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _visible_cases(
|
|
642
|
+
cases: tuple[RegionAmbiguityCase, ...],
|
|
643
|
+
) -> Iterable[RegionAmbiguityCase]:
|
|
644
|
+
return (case for case in cases if case.sheet_visibility == "visible")
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _fallback_resolution(
|
|
648
|
+
case: RegionAmbiguityCase,
|
|
649
|
+
audit: ModelCallAudit | None = None,
|
|
650
|
+
) -> RegionResolution:
|
|
651
|
+
return RegionResolution(
|
|
652
|
+
case_id=case.case_id,
|
|
653
|
+
choice_id=case.fallback_choice_id,
|
|
654
|
+
status="local_fallback",
|
|
655
|
+
audit=audit,
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _audit(
|
|
660
|
+
case: RegionAmbiguityCase,
|
|
661
|
+
*,
|
|
662
|
+
mode: WorkbookModelMode,
|
|
663
|
+
identity: ModelIdentity | None,
|
|
664
|
+
request: WorkbookModelRequest | None,
|
|
665
|
+
request_bytes: int,
|
|
666
|
+
response_body: bytes | None,
|
|
667
|
+
cache_status: str,
|
|
668
|
+
attempts: int,
|
|
669
|
+
elapsed_ms: int,
|
|
670
|
+
outcome: str,
|
|
671
|
+
selected_choice_id: str | None = None,
|
|
672
|
+
reported_confidence: float | None = None,
|
|
673
|
+
error: Exception | None = None,
|
|
674
|
+
) -> ModelCallAudit:
|
|
675
|
+
fallback_choice = next(
|
|
676
|
+
choice for choice in case.choices if choice.choice_id == case.fallback_choice_id
|
|
677
|
+
)
|
|
678
|
+
provider, provider_redacted = _safe_identity_token(
|
|
679
|
+
identity.provider if identity is not None else None,
|
|
680
|
+
required=identity is not None,
|
|
681
|
+
)
|
|
682
|
+
model, model_redacted = _safe_identity_token(
|
|
683
|
+
identity.model if identity is not None else None,
|
|
684
|
+
required=identity is not None,
|
|
685
|
+
)
|
|
686
|
+
model_revision, revision_redacted = _safe_identity_token(
|
|
687
|
+
identity.revision if identity is not None else None,
|
|
688
|
+
required=False,
|
|
689
|
+
)
|
|
690
|
+
validation_codes = []
|
|
691
|
+
if provider_redacted or model_redacted or revision_redacted:
|
|
692
|
+
validation_codes.append("unsafe_identity_redacted")
|
|
693
|
+
checksum = None
|
|
694
|
+
checksum_error = None
|
|
695
|
+
if response_body is not None:
|
|
696
|
+
try:
|
|
697
|
+
checksum = response_checksum(response_body)
|
|
698
|
+
except Exception as caught:
|
|
699
|
+
checksum_error = caught
|
|
700
|
+
validation_codes.append("response_checksum_error")
|
|
701
|
+
effective_error = error if error is not None else checksum_error
|
|
702
|
+
return ModelCallAudit(
|
|
703
|
+
case_id=case.case_id,
|
|
704
|
+
source_range=case.source_range,
|
|
705
|
+
mode=mode.value,
|
|
706
|
+
schema_version=REGION_SCHEMA_VERSION,
|
|
707
|
+
prompt_version=REGION_PROMPT_VERSION,
|
|
708
|
+
rule_version=REGION_RULE_VERSION,
|
|
709
|
+
validator_version=REGION_VALIDATOR_VERSION,
|
|
710
|
+
privacy_version=REGION_PRIVACY_VERSION,
|
|
711
|
+
rule_confidence=fallback_choice.local_score,
|
|
712
|
+
provider=provider,
|
|
713
|
+
model=model,
|
|
714
|
+
model_revision=model_revision,
|
|
715
|
+
request_checksum=request.request_checksum if request is not None else None,
|
|
716
|
+
response_checksum=checksum,
|
|
717
|
+
cache_status=cache_status,
|
|
718
|
+
attempts=attempts,
|
|
719
|
+
elapsed_ms=elapsed_ms,
|
|
720
|
+
request_bytes=request_bytes,
|
|
721
|
+
response_bytes=len(response_body) if response_body is not None else 0,
|
|
722
|
+
outcome=outcome,
|
|
723
|
+
selected_choice_id=selected_choice_id,
|
|
724
|
+
reported_confidence=reported_confidence,
|
|
725
|
+
validation_codes=tuple(validation_codes),
|
|
726
|
+
reason_codes=(),
|
|
727
|
+
error_type=_safe_error_type(effective_error),
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _audit_payload(audit: ModelCallAudit) -> dict[str, object]:
|
|
732
|
+
return {field.name: getattr(audit, field.name) for field in fields(ModelCallAudit)}
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def _quota_limit_reached(
|
|
736
|
+
policy: WorkbookModelPolicy,
|
|
737
|
+
quota_tracker: dict[str, Any],
|
|
738
|
+
) -> bool:
|
|
739
|
+
return bool(
|
|
740
|
+
(
|
|
741
|
+
policy.max_tokens_per_workbook is not None
|
|
742
|
+
and quota_tracker["tokens"] >= policy.max_tokens_per_workbook
|
|
743
|
+
)
|
|
744
|
+
or (
|
|
745
|
+
policy.max_cost_usd_per_workbook is not None
|
|
746
|
+
and quota_tracker["cost_usd"] >= policy.max_cost_usd_per_workbook
|
|
747
|
+
)
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def _quota_usage_unavailable(
|
|
752
|
+
policy: WorkbookModelPolicy,
|
|
753
|
+
usage: Mapping[str, int],
|
|
754
|
+
) -> bool:
|
|
755
|
+
token_usage_available = any(
|
|
756
|
+
key in usage for key in ("prompt_tokens", "completion_tokens", "total_tokens")
|
|
757
|
+
)
|
|
758
|
+
cost_usage_available = "prompt_tokens" in usage and "completion_tokens" in usage
|
|
759
|
+
return bool(
|
|
760
|
+
(policy.max_tokens_per_workbook is not None and not token_usage_available)
|
|
761
|
+
or (policy.max_cost_usd_per_workbook is not None and not cost_usage_available)
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _is_response_error(error: Exception) -> bool:
|
|
766
|
+
return isinstance(error, WorkbookModelResponseError)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _safe_identity_token(
|
|
770
|
+
value: object,
|
|
771
|
+
*,
|
|
772
|
+
required: bool,
|
|
773
|
+
) -> tuple[str | None, bool]:
|
|
774
|
+
if value is None and not required:
|
|
775
|
+
return None, False
|
|
776
|
+
if (
|
|
777
|
+
isinstance(value, str)
|
|
778
|
+
and 0 < len(value) <= 64
|
|
779
|
+
and value.isascii()
|
|
780
|
+
and all(character.isalnum() or character in "._-/" for character in value)
|
|
781
|
+
):
|
|
782
|
+
return value, False
|
|
783
|
+
return None, True
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _safe_error_type(error: Exception | None) -> str | None:
|
|
787
|
+
if error is None:
|
|
788
|
+
return None
|
|
789
|
+
try:
|
|
790
|
+
name = type(error).__name__
|
|
791
|
+
except Exception:
|
|
792
|
+
return "ExternalError"
|
|
793
|
+
if (
|
|
794
|
+
type(name) is str
|
|
795
|
+
and 0 < len(name) <= 64
|
|
796
|
+
and name.isascii()
|
|
797
|
+
and all(character.isalnum() or character in "._-" for character in name)
|
|
798
|
+
):
|
|
799
|
+
return name
|
|
800
|
+
return "ExternalError"
|