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.
Files changed (101) hide show
  1. langparse/__init__.py +55 -0
  2. langparse/autoparser.py +25 -0
  3. langparse/chunkers/__init__.py +12 -0
  4. langparse/chunkers/blocks.py +151 -0
  5. langparse/chunkers/profiles.py +53 -0
  6. langparse/chunkers/registry.py +38 -0
  7. langparse/chunkers/semantic.py +242 -0
  8. langparse/chunkers/text.py +96 -0
  9. langparse/chunkers/workbook.py +942 -0
  10. langparse/cli.py +329 -0
  11. langparse/config.py +169 -0
  12. langparse/core/__init__.py +0 -0
  13. langparse/core/chunker.py +16 -0
  14. langparse/core/engine.py +37 -0
  15. langparse/core/parser.py +35 -0
  16. langparse/core/rendering.py +49 -0
  17. langparse/engines/__init__.py +1 -0
  18. langparse/engines/pdf/__init__.py +1 -0
  19. langparse/engines/pdf/deepdoc/__init__.py +55 -0
  20. langparse/engines/pdf/deepdoc/layout_recognizer.py +235 -0
  21. langparse/engines/pdf/deepdoc/model_loader.py +101 -0
  22. langparse/engines/pdf/deepdoc/ocr.py +641 -0
  23. langparse/engines/pdf/deepdoc/operators.py +684 -0
  24. langparse/engines/pdf/deepdoc/pdf_parser.py +1894 -0
  25. langparse/engines/pdf/deepdoc/postprocess.py +339 -0
  26. langparse/engines/pdf/deepdoc/recognizer.py +418 -0
  27. langparse/engines/pdf/deepdoc/rendering.py +210 -0
  28. langparse/engines/pdf/deepdoc/table_structure_recognizer.py +559 -0
  29. langparse/engines/pdf/deepdoc/tokenizer.py +30 -0
  30. langparse/engines/pdf/deepdoc/utils.py +36 -0
  31. langparse/engines/pdf/deepdoc_engine.py +164 -0
  32. langparse/engines/pdf/mineru.py +259 -0
  33. langparse/engines/pdf/mineru_client.py +318 -0
  34. langparse/engines/pdf/mineru_service.py +225 -0
  35. langparse/engines/pdf/ocr.py +101 -0
  36. langparse/engines/pdf/other.py +20 -0
  37. langparse/engines/pdf/simple.py +134 -0
  38. langparse/engines/pdf/vision_llm.py +27 -0
  39. langparse/errors.py +70 -0
  40. langparse/logging.py +27 -0
  41. langparse/metrics.py +129 -0
  42. langparse/parsers/__init__.py +0 -0
  43. langparse/parsers/docx_parser.py +114 -0
  44. langparse/parsers/excel_parser.py +220 -0
  45. langparse/parsers/markdown_parser.py +34 -0
  46. langparse/parsers/pdf_parser.py +31 -0
  47. langparse/parsers/registry.py +48 -0
  48. langparse/parsers/sniff.py +72 -0
  49. langparse/progress.py +77 -0
  50. langparse/py.typed +0 -0
  51. langparse/services/__init__.py +11 -0
  52. langparse/services/batch_service.py +339 -0
  53. langparse/services/benchmark_service.py +202 -0
  54. langparse/services/fidelity.py +154 -0
  55. langparse/services/output_paths.py +86 -0
  56. langparse/services/parse_service.py +523 -0
  57. langparse/services/quality.py +65 -0
  58. langparse/services/workbook_ambiguity_benchmark.py +563 -0
  59. langparse/services/workbook_quality_benchmark.py +230 -0
  60. langparse/types.py +97 -0
  61. langparse/workbooks/__init__.py +103 -0
  62. langparse/workbooks/adapters.py +474 -0
  63. langparse/workbooks/assembly.py +993 -0
  64. langparse/workbooks/blocks.py +209 -0
  65. langparse/workbooks/bundle-v1.schema.json +71 -0
  66. langparse/workbooks/bundle.py +341 -0
  67. langparse/workbooks/classification.py +393 -0
  68. langparse/workbooks/continuation.py +577 -0
  69. langparse/workbooks/evaluation/__init__.py +45 -0
  70. langparse/workbooks/evaluation/evaluator.py +381 -0
  71. langparse/workbooks/evaluation/schema.py +419 -0
  72. langparse/workbooks/labels.py +14 -0
  73. langparse/workbooks/lineage.py +117 -0
  74. langparse/workbooks/modeling/__init__.py +52 -0
  75. langparse/workbooks/modeling/cache.py +20 -0
  76. langparse/workbooks/modeling/config.py +87 -0
  77. langparse/workbooks/modeling/contract.py +628 -0
  78. langparse/workbooks/modeling/disambiguation.py +800 -0
  79. langparse/workbooks/modeling/openai_adapter.py +192 -0
  80. langparse/workbooks/modeling/policy.py +79 -0
  81. langparse/workbooks/modeling/ports.py +44 -0
  82. langparse/workbooks/modeling/pricing.py +17 -0
  83. langparse/workbooks/modeling/types.py +251 -0
  84. langparse/workbooks/objects.py +229 -0
  85. langparse/workbooks/quality/__init__.py +23 -0
  86. langparse/workbooks/quality/bundle.py +53 -0
  87. langparse/workbooks/quality/evaluator.py +266 -0
  88. langparse/workbooks/quality/facts.py +142 -0
  89. langparse/workbooks/quality/schema.py +462 -0
  90. langparse/workbooks/reference_types.py +73 -0
  91. langparse/workbooks/references.py +178 -0
  92. langparse/workbooks/regions.py +932 -0
  93. langparse/workbooks/rendering.py +222 -0
  94. langparse/workbooks/tables.py +477 -0
  95. langparse/workbooks/types.py +257 -0
  96. langparse-0.1.0.dist-info/METADATA +790 -0
  97. langparse-0.1.0.dist-info/RECORD +101 -0
  98. langparse-0.1.0.dist-info/WHEEL +5 -0
  99. langparse-0.1.0.dist-info/entry_points.txt +2 -0
  100. langparse-0.1.0.dist-info/licenses/LICENSE +192 -0
  101. langparse-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ from .config import WorkbookModelConfig, resolve_workbook_model_config
4
+ from .ports import WorkbookModelResponseError, WorkbookStructureModelAdapter
5
+ from .types import ModelIdentity, ProviderReply, WorkbookModelRequest
6
+
7
+ _DECISION_COMMON_PROPERTIES = {
8
+ "case_id": {"type": "string"},
9
+ "status": {"type": "string"},
10
+ "confidence": {"type": "number", "minimum": 0, "maximum": 1},
11
+ "reason_codes": {"type": "array", "items": {"type": "string"}},
12
+ }
13
+
14
+ _WORKBOOK_RESPONSE_SCHEMA = {
15
+ "type": "object",
16
+ "additionalProperties": False,
17
+ "required": ["schema_version", "request_checksum", "decisions"],
18
+ "properties": {
19
+ "schema_version": {"type": "integer", "const": 1},
20
+ "request_checksum": {"type": "string"},
21
+ "decisions": {
22
+ "type": "array",
23
+ "minItems": 1,
24
+ "maxItems": 1,
25
+ "items": {
26
+ "anyOf": [
27
+ {
28
+ "type": "object",
29
+ "additionalProperties": False,
30
+ "required": [
31
+ "case_id",
32
+ "status",
33
+ "choice_id",
34
+ "confidence",
35
+ "reason_codes",
36
+ ],
37
+ "properties": {
38
+ **_DECISION_COMMON_PROPERTIES,
39
+ "status": {"type": "string", "const": "selected"},
40
+ "choice_id": {"type": "string"},
41
+ },
42
+ },
43
+ {
44
+ "type": "object",
45
+ "additionalProperties": False,
46
+ "required": ["case_id", "status", "confidence", "reason_codes"],
47
+ "properties": {
48
+ **_DECISION_COMMON_PROPERTIES,
49
+ "status": {"type": "string", "const": "abstained"},
50
+ },
51
+ },
52
+ ]
53
+ },
54
+ },
55
+ },
56
+ }
57
+
58
+
59
+ class OpenAIWorkbookStructureAdapter(WorkbookStructureModelAdapter):
60
+ """Adapter invoking OpenAI Chat Completions API for workbook structure disambiguation."""
61
+
62
+ def __init__(self, config: WorkbookModelConfig) -> None:
63
+ self._config = config
64
+ self._identity = ModelIdentity(
65
+ provider="openai",
66
+ model=config.model,
67
+ revision=None,
68
+ )
69
+ self._client = None
70
+
71
+ @property
72
+ def identity(self) -> ModelIdentity:
73
+ return self._identity
74
+
75
+ @property
76
+ def config(self) -> WorkbookModelConfig:
77
+ return self._config
78
+
79
+ @classmethod
80
+ def from_env(
81
+ cls,
82
+ *,
83
+ cli_model: str | None = None,
84
+ cli_api_key: str | None = None,
85
+ cli_base_url: str | None = None,
86
+ cli_timeout_seconds: float | None = None,
87
+ ) -> OpenAIWorkbookStructureAdapter:
88
+ config = resolve_workbook_model_config(
89
+ cli_model=cli_model,
90
+ cli_api_key=cli_api_key,
91
+ cli_base_url=cli_base_url,
92
+ cli_timeout_seconds=cli_timeout_seconds,
93
+ from_env=True,
94
+ )
95
+ return cls(config)
96
+
97
+ def _get_client(self, timeout_seconds: float):
98
+ import openai
99
+
100
+ if self._client is None:
101
+ self._client = openai.OpenAI(
102
+ api_key=self._config.api_key,
103
+ base_url=self._config.base_url,
104
+ timeout=timeout_seconds,
105
+ )
106
+ return self._client
107
+
108
+ def complete(
109
+ self,
110
+ request: WorkbookModelRequest,
111
+ *,
112
+ timeout_seconds: float,
113
+ ) -> ProviderReply:
114
+ try:
115
+ import openai
116
+
117
+ if openai is None:
118
+ raise ImportError("openai module is None")
119
+ except (ImportError, TypeError) as exc:
120
+ raise ImportError(
121
+ "The 'openai' package is required for model disambiguation. "
122
+ "Install it with 'pip install \"langparse[model]\"'."
123
+ ) from exc
124
+
125
+ effective_timeout = min(timeout_seconds, self._config.timeout_seconds)
126
+ try:
127
+ client = self._get_client(effective_timeout)
128
+ response = client.chat.completions.create(
129
+ model=self._config.model,
130
+ messages=[
131
+ {
132
+ "role": "system",
133
+ "content": (
134
+ "You are an Excel layout disambiguation adjudicator. "
135
+ "Analyze the cell cues, structural feature summary, and registered choice reasons, "
136
+ "then select the registered choice that best explains the region. "
137
+ "Use 'unclassified' only when no registered typed choice is supported by the facts. "
138
+ "Return only one JSON object matching the supplied response contract. "
139
+ "If you choose a registered choice_id, status MUST be exactly 'selected'. "
140
+ "If you do not choose one, status MUST be exactly 'abstained' and choice_id "
141
+ "MUST be omitted. Never use 'resolved', 'chosen', or any other status. "
142
+ "Copy schema_version, request_checksum, and case_id exactly from the request. "
143
+ "choice_id MUST be copied exactly from that case's registered choices."
144
+ ),
145
+ },
146
+ {
147
+ "role": "user",
148
+ "content": request.body.decode("utf-8"),
149
+ },
150
+ ],
151
+ response_format={
152
+ "type": "json_schema",
153
+ "json_schema": {
154
+ "name": "workbook_region_decision",
155
+ "strict": True,
156
+ "schema": _WORKBOOK_RESPONSE_SCHEMA,
157
+ },
158
+ },
159
+ seed=0,
160
+ temperature=0,
161
+ timeout=effective_timeout,
162
+ )
163
+ except Exception as exc:
164
+ if isinstance(exc, ImportError):
165
+ raise
166
+ raise WorkbookModelResponseError(
167
+ f"OpenAI completion failed ({type(exc).__name__})"
168
+ ) from exc
169
+
170
+ if not response.choices:
171
+ raise WorkbookModelResponseError("OpenAI returned an empty choices list")
172
+
173
+ message_content = response.choices[0].message.content or ""
174
+ reply_body = message_content.encode("utf-8")
175
+
176
+ usage_dict: dict[str, int] = {}
177
+ if getattr(response, "usage", None) is not None:
178
+ usage = response.usage
179
+ if getattr(usage, "prompt_tokens", None) is not None:
180
+ usage_dict["prompt_tokens"] = usage.prompt_tokens
181
+ if getattr(usage, "completion_tokens", None) is not None:
182
+ usage_dict["completion_tokens"] = usage.completion_tokens
183
+ if getattr(usage, "total_tokens", None) is not None:
184
+ usage_dict["total_tokens"] = usage.total_tokens
185
+
186
+ provider_request_id = getattr(response, "id", None)
187
+
188
+ return ProviderReply(
189
+ body=reply_body,
190
+ provider_request_id=provider_request_id,
191
+ usage=usage_dict,
192
+ )
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING
5
+
6
+ from .ports import WorkbookModelConfigurationError, WorkbookStructureModelAdapter
7
+ from .types import WorkbookModelMode, WorkbookModelPolicy
8
+
9
+ if TYPE_CHECKING:
10
+ from .disambiguation import WorkbookRegionDisambiguator
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class WorkbookDisambiguation:
15
+ mode: WorkbookModelMode = WorkbookModelMode.OFF
16
+ adapter: WorkbookStructureModelAdapter | None = None
17
+ policy: WorkbookModelPolicy = WorkbookModelPolicy()
18
+ _runtime: WorkbookRegionDisambiguator | None = field(
19
+ init=False,
20
+ default=None,
21
+ repr=False,
22
+ compare=False,
23
+ )
24
+
25
+ def __post_init__(self) -> None:
26
+ mode = WorkbookModelMode(self.mode)
27
+ object.__setattr__(self, "mode", mode)
28
+
29
+ if self.policy is None:
30
+ object.__setattr__(self, "policy", WorkbookModelPolicy())
31
+ elif not isinstance(self.policy, WorkbookModelPolicy):
32
+ raise WorkbookModelConfigurationError("policy must be a WorkbookModelPolicy")
33
+
34
+ if mode is WorkbookModelMode.OFF and self.adapter is not None:
35
+ raise WorkbookModelConfigurationError(
36
+ "off workbook disambiguation cannot carry an adapter"
37
+ )
38
+ if mode is WorkbookModelMode.AUTO and self.adapter is None:
39
+ raise WorkbookModelConfigurationError(
40
+ "auto workbook disambiguation requires an adapter"
41
+ )
42
+ if mode is WorkbookModelMode.REQUIRED and self.adapter is None:
43
+ raise WorkbookModelConfigurationError(
44
+ "required workbook disambiguation requires an adapter"
45
+ )
46
+ if mode is not WorkbookModelMode.OFF:
47
+ from .disambiguation import WorkbookRegionDisambiguator
48
+
49
+ object.__setattr__(self, "_runtime", WorkbookRegionDisambiguator())
50
+
51
+ @classmethod
52
+ def off(cls) -> WorkbookDisambiguation:
53
+ return cls(mode=WorkbookModelMode.OFF)
54
+
55
+ @classmethod
56
+ def auto(
57
+ cls,
58
+ adapter: WorkbookStructureModelAdapter,
59
+ *,
60
+ policy: WorkbookModelPolicy | None = None,
61
+ ) -> WorkbookDisambiguation:
62
+ return cls(
63
+ mode=WorkbookModelMode.AUTO,
64
+ adapter=adapter,
65
+ policy=WorkbookModelPolicy() if policy is None else policy,
66
+ )
67
+
68
+ @classmethod
69
+ def required(
70
+ cls,
71
+ adapter: WorkbookStructureModelAdapter,
72
+ *,
73
+ policy: WorkbookModelPolicy | None = None,
74
+ ) -> WorkbookDisambiguation:
75
+ return cls(
76
+ mode=WorkbookModelMode.REQUIRED,
77
+ adapter=adapter,
78
+ policy=WorkbookModelPolicy() if policy is None else policy,
79
+ )
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from langparse.types import ParseDiagnostics
6
+
7
+ from .types import ModelIdentity, ProviderReply, WorkbookModelRequest
8
+
9
+
10
+ class WorkbookModelError(Exception):
11
+ """Base error for workbook model disambiguation."""
12
+
13
+
14
+ class WorkbookModelConfigurationError(WorkbookModelError, ValueError):
15
+ """Raised when model disambiguation is configured inconsistently."""
16
+
17
+
18
+ class InvalidRegionAmbiguityCaseError(WorkbookModelError, ValueError):
19
+ """Raised when an ambiguity case does not satisfy its contract."""
20
+
21
+
22
+ class WorkbookModelResponseError(WorkbookModelError, ValueError):
23
+ """Raised when a provider response cannot be accepted."""
24
+
25
+
26
+ class RequiredWorkbookDisambiguationError(WorkbookModelError):
27
+ """Raised when required model disambiguation leaves cases unresolved."""
28
+
29
+ def __init__(self, case_ids: tuple[str, ...], diagnostics: ParseDiagnostics):
30
+ super().__init__(f"Workbook model disambiguation unresolved: {', '.join(case_ids)}")
31
+ self.case_ids = case_ids
32
+ self.diagnostics = diagnostics
33
+
34
+
35
+ class WorkbookStructureModelAdapter(Protocol):
36
+ @property
37
+ def identity(self) -> ModelIdentity: ...
38
+
39
+ def complete(
40
+ self,
41
+ request: WorkbookModelRequest,
42
+ *,
43
+ timeout_seconds: float,
44
+ ) -> ProviderReply: ...
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def calculate_cost_usd(
5
+ prompt_tokens: int,
6
+ completion_tokens: int,
7
+ *,
8
+ input_cost_usd_per_million: float,
9
+ output_cost_usd_per_million: float,
10
+ ) -> float:
11
+ """Calculate observed cost using deployment-supplied, versioned policy rates."""
12
+ if prompt_tokens <= 0 and completion_tokens <= 0:
13
+ return 0.0
14
+
15
+ prompt_cost = (prompt_tokens / 1_000_000.0) * input_cost_usd_per_million
16
+ completion_cost = (completion_tokens / 1_000_000.0) * output_cost_usd_per_million
17
+ return prompt_cost + completion_cost
@@ -0,0 +1,251 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from collections.abc import Mapping
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from numbers import Real
8
+ from types import MappingProxyType
9
+ from typing import Literal, TypeAlias
10
+
11
+ REGION_SCHEMA_VERSION = 1
12
+ REGION_PROMPT_VERSION = "region-choice-v2"
13
+ REGION_RULE_VERSION = "region-rules-v1"
14
+ REGION_VALIDATOR_VERSION = "region-validator-v1"
15
+ REGION_PRIVACY_VERSION = "region-privacy-v1"
16
+
17
+ RegionFeatureScalar: TypeAlias = str | int | float | bool | None
18
+
19
+
20
+ def _is_region_feature_scalar(value: object) -> bool:
21
+ return value is None or type(value) in (str, int, float, bool)
22
+
23
+
24
+ class WorkbookModelMode(str, Enum):
25
+ OFF = "off"
26
+ AUTO = "auto"
27
+ REQUIRED = "required"
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class WorkbookModelPolicy:
32
+ timeout_seconds: float = 20.0
33
+ workbook_timeout_seconds: float = 60.0
34
+ max_attempts: int = 2
35
+ max_cases: int = 8
36
+ max_calls: int = 4
37
+ max_cells_per_case: int = 500
38
+ max_request_bytes: int = 256_000
39
+ max_response_bytes: int = 128_000
40
+ kill_switch: bool = False
41
+ max_tokens_per_workbook: int | None = None
42
+ max_cost_usd_per_workbook: float | None = None
43
+ input_cost_usd_per_million: float | None = None
44
+ output_cost_usd_per_million: float | None = None
45
+ cost_pricing_version: str | None = None
46
+
47
+ def __post_init__(self) -> None:
48
+ if not isinstance(self.kill_switch, bool):
49
+ raise ValueError("kill_switch must be a bool")
50
+ for name in ("timeout_seconds", "workbook_timeout_seconds"):
51
+ value = getattr(self, name)
52
+ if (
53
+ isinstance(value, bool)
54
+ or not isinstance(value, Real)
55
+ or not math.isfinite(value)
56
+ or value <= 0
57
+ ):
58
+ raise ValueError(f"{name} must be a finite positive real")
59
+ for name in (
60
+ "max_attempts",
61
+ "max_cases",
62
+ "max_calls",
63
+ "max_cells_per_case",
64
+ "max_request_bytes",
65
+ "max_response_bytes",
66
+ ):
67
+ value = getattr(self, name)
68
+ if type(value) is not int or value <= 0:
69
+ raise ValueError(f"{name} must be an exact positive integer")
70
+ if self.max_tokens_per_workbook is not None:
71
+ if type(self.max_tokens_per_workbook) is not int or self.max_tokens_per_workbook <= 0:
72
+ raise ValueError("max_tokens_per_workbook must be an exact positive integer")
73
+ if self.max_cost_usd_per_workbook is not None:
74
+ if (
75
+ isinstance(self.max_cost_usd_per_workbook, bool)
76
+ or not isinstance(self.max_cost_usd_per_workbook, Real)
77
+ or not math.isfinite(self.max_cost_usd_per_workbook)
78
+ or self.max_cost_usd_per_workbook <= 0
79
+ ):
80
+ raise ValueError("max_cost_usd_per_workbook must be a finite positive real")
81
+ for name in (
82
+ "input_cost_usd_per_million",
83
+ "output_cost_usd_per_million",
84
+ ):
85
+ value = getattr(self, name)
86
+ if (
87
+ isinstance(value, bool)
88
+ or not isinstance(value, Real)
89
+ or not math.isfinite(value)
90
+ or value < 0
91
+ ):
92
+ raise ValueError(
93
+ "cost pricing requires finite non-negative input and output rates"
94
+ )
95
+ version = self.cost_pricing_version
96
+ if (
97
+ not isinstance(version, str)
98
+ or not 0 < len(version.strip()) <= 64
99
+ or not version.isascii()
100
+ or not all(character.isalnum() or character in "._-" for character in version)
101
+ ):
102
+ raise ValueError("cost_pricing_version must be a non-empty safe ASCII identifier")
103
+ object.__setattr__(self, "cost_pricing_version", version.strip())
104
+ elif any(
105
+ value is not None
106
+ for value in (
107
+ self.input_cost_usd_per_million,
108
+ self.output_cost_usd_per_million,
109
+ self.cost_pricing_version,
110
+ )
111
+ ):
112
+ raise ValueError("cost pricing fields require max_cost_usd_per_workbook")
113
+
114
+
115
+ @dataclass(frozen=True)
116
+ class ModelIdentity:
117
+ provider: str
118
+ model: str
119
+ revision: str | None = None
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class RegionChoice:
124
+ choice_id: str
125
+ kind: Literal["logical_table", "form", "matrix", "text", "unclassified"]
126
+ local_score: float
127
+ reason_codes: tuple[str, ...] = ()
128
+
129
+
130
+ @dataclass(frozen=True)
131
+ class RegionCellCue:
132
+ coordinate: str
133
+ display_text: str
134
+ value_type: str
135
+ style_fingerprint: str
136
+ merge_anchor: str | None
137
+ rowspan: int
138
+ colspan: int
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class RegionAmbiguityCase:
143
+ case_id: str
144
+ sheet_name: str
145
+ sheet_visibility: str
146
+ source_range: str
147
+ fact_digest: str
148
+ cells: tuple[RegionCellCue, ...]
149
+ feature_summary: tuple[tuple[str, RegionFeatureScalar], ...]
150
+ choices: tuple[RegionChoice, ...]
151
+ fallback_choice_id: str
152
+ ambiguity_codes: tuple[str, ...]
153
+
154
+ def __post_init__(self) -> None:
155
+ if not isinstance(self.feature_summary, tuple):
156
+ raise TypeError("feature_summary must be a tuple of (str, scalar) entries")
157
+ for entry in self.feature_summary:
158
+ if not isinstance(entry, tuple) or len(entry) != 2:
159
+ raise TypeError("feature_summary must contain (str, scalar) tuples")
160
+ key, value = entry
161
+ if type(key) is not str or not _is_region_feature_scalar(value):
162
+ raise TypeError("feature_summary entries must contain a string and scalar value")
163
+
164
+
165
+ @dataclass(frozen=True)
166
+ class WorkbookModelRequest:
167
+ schema_version: int
168
+ prompt_version: str
169
+ privacy_version: str
170
+ request_checksum: str
171
+ body: bytes
172
+ case_ids: tuple[str, ...]
173
+ choice_ids_by_case: tuple[tuple[str, tuple[str, ...]], ...]
174
+
175
+
176
+ @dataclass(frozen=True)
177
+ class ProviderReply:
178
+ body: bytes
179
+ provider_request_id: str | None
180
+ usage: Mapping[str, int] = field(default_factory=dict)
181
+
182
+ def __post_init__(self) -> None:
183
+ if not isinstance(self.body, (bytes, bytearray, memoryview)):
184
+ raise TypeError("body must be bytes-like")
185
+ object.__setattr__(self, "body", bytes(self.body))
186
+ if self.provider_request_id is not None and type(self.provider_request_id) is not str:
187
+ raise TypeError("provider_request_id must be a string or None")
188
+ if not isinstance(self.usage, Mapping):
189
+ raise TypeError("usage must be a mapping of string keys to integer values")
190
+ copied_usage: dict[str, int] = {}
191
+ for key, value in self.usage.items():
192
+ if not isinstance(key, str):
193
+ raise TypeError("usage keys must be strings")
194
+ if not isinstance(value, int) or isinstance(value, bool):
195
+ raise TypeError("usage values must be non-bool integers")
196
+ if value < 0:
197
+ raise TypeError("usage values must be non-negative integers")
198
+ copied_usage[key] = value
199
+ object.__setattr__(self, "usage", MappingProxyType(copied_usage))
200
+
201
+
202
+ @dataclass(frozen=True)
203
+ class RegionModelDecision:
204
+ case_id: str
205
+ status: Literal["selected", "abstained"]
206
+ choice_id: str | None
207
+ reported_confidence: float | None
208
+ reason_codes: tuple[str, ...] = ()
209
+
210
+
211
+ @dataclass(frozen=True)
212
+ class ModelCallAudit:
213
+ case_id: str
214
+ source_range: str
215
+ mode: str
216
+ schema_version: int
217
+ prompt_version: str
218
+ rule_version: str
219
+ validator_version: str
220
+ privacy_version: str
221
+ rule_confidence: float
222
+ provider: str | None
223
+ model: str | None
224
+ model_revision: str | None
225
+ request_checksum: str | None
226
+ response_checksum: str | None
227
+ cache_status: str
228
+ attempts: int
229
+ elapsed_ms: int
230
+ request_bytes: int
231
+ response_bytes: int
232
+ outcome: str
233
+ selected_choice_id: str | None
234
+ reported_confidence: float | None
235
+ validation_codes: tuple[str, ...] = ()
236
+ reason_codes: tuple[str, ...] = ()
237
+ error_type: str | None = None
238
+
239
+
240
+ @dataclass(frozen=True)
241
+ class RegionResolution:
242
+ case_id: str
243
+ choice_id: str
244
+ status: Literal["local_fallback", "model_selected", "cache_selected"]
245
+ audit: ModelCallAudit | None = None
246
+
247
+
248
+ @dataclass(frozen=True)
249
+ class RegionResolutionBatch:
250
+ resolutions: tuple[RegionResolution, ...]
251
+ unresolved_case_ids: tuple[str, ...] = ()