clairic 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.
- clairic/__init__.py +100 -0
- clairic/_client.py +322 -0
- clairic/_compile.py +196 -0
- clairic/_version.py +1 -0
- clairic/answers.py +374 -0
- clairic/errors.py +99 -0
- clairic/py.typed +0 -0
- clairic/result.py +111 -0
- clairic/specs.py +346 -0
- clairic-0.1.0.dist-info/METADATA +209 -0
- clairic-0.1.0.dist-info/RECORD +14 -0
- clairic-0.1.0.dist-info/WHEEL +4 -0
- clairic-0.1.0.dist-info/licenses/LICENSE +21 -0
- clairic-0.1.0.dist-info/licenses/NOTICE +18 -0
clairic/answers.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Typed answers returned by Clairic, one class per response shape.
|
|
2
|
+
|
|
3
|
+
Declare an extraction model by annotating each field with one of these classes. The class
|
|
4
|
+
chooses the Clairic field type, and after extraction the field holds the answer: the value and
|
|
5
|
+
the metadata Clairic attaches to it.
|
|
6
|
+
|
|
7
|
+
Clairic answers a field with `value = None` when the input does not determine it. Single-value
|
|
8
|
+
answers carry a confidence score and the probability that the value is absent (`p_null`).
|
|
9
|
+
Estimates and forecasts carry a distribution. Typed output constrains the shape of an answer,
|
|
10
|
+
not its correctness: use `confidence`, `p_null` and the percentile range to decide which
|
|
11
|
+
answers to act on and which to review.
|
|
12
|
+
|
|
13
|
+
Source: https://developers.cloudflare.com/workers-ai/concepts/extraction-models/
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import datetime as dt
|
|
19
|
+
from typing import Generic, Literal, NamedTuple, TypeVar
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, ConfigDict
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Answer",
|
|
25
|
+
"DateAnswer",
|
|
26
|
+
"DateEstimateAnswer",
|
|
27
|
+
"DatetimeAnswer",
|
|
28
|
+
"DurationAnswer",
|
|
29
|
+
"DurationUnit",
|
|
30
|
+
"EstimateAnswer",
|
|
31
|
+
"ForecastAnswer",
|
|
32
|
+
"ForecastStep",
|
|
33
|
+
"ListAnswer",
|
|
34
|
+
"NumberAnswer",
|
|
35
|
+
"Span",
|
|
36
|
+
"TextAnswer",
|
|
37
|
+
"TextListAnswer",
|
|
38
|
+
"TimeAnswer",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
N = TypeVar("N", int, float)
|
|
42
|
+
"""Numeric value type of an answer: `int` sends `integer: true` to Clairic."""
|
|
43
|
+
|
|
44
|
+
V = TypeVar("V", int, float, dt.date)
|
|
45
|
+
"""Item type of a number or date list."""
|
|
46
|
+
|
|
47
|
+
DurationUnit = Literal["seconds", "minutes", "hours", "days", "weeks", "months", "years"]
|
|
48
|
+
"""Unit in which Clairic expresses a duration."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Span(NamedTuple):
|
|
52
|
+
"""Location of an extracted text in the input, as end-exclusive character offsets.
|
|
53
|
+
|
|
54
|
+
`state[span.start:span.end]` returns the extracted text.
|
|
55
|
+
|
|
56
|
+
Usage:
|
|
57
|
+
>>> state = "Invoice INV-2291 from Northwind Supply"
|
|
58
|
+
>>> span = Span(8, 16)
|
|
59
|
+
>>> state[span.start : span.end]
|
|
60
|
+
'INV-2291'
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
start: int
|
|
64
|
+
end: int
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Answer(BaseModel):
|
|
68
|
+
"""Base class of every Clairic answer.
|
|
69
|
+
|
|
70
|
+
Answers are immutable. Keys Clairic adds in the future are kept and readable through
|
|
71
|
+
`model_extra`.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
model_config = ConfigDict(frozen=True, extra="allow")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _SingleValueAnswer(Answer):
|
|
78
|
+
"""Answer to a field with a single value: text, number, date, time, datetime, duration.
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
p_null: float
|
|
82
|
+
Probability that the input does not contain or determine the value. `value` is
|
|
83
|
+
`None` when it exceeds the field's `null_threshold` (default 0.5).
|
|
84
|
+
confidence: float
|
|
85
|
+
Probability that `guess` is the correct value, given that a value exists.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
p_null: float
|
|
89
|
+
confidence: float
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TextAnswer(_SingleValueAnswer):
|
|
93
|
+
"""Answer to a `text` field: an exact span of the input.
|
|
94
|
+
|
|
95
|
+
`guess` and `span` describe the best candidate even when `value` is `None`, which lets you
|
|
96
|
+
show a reviewer what Clairic considered before deciding the value was absent.
|
|
97
|
+
|
|
98
|
+
Attributes:
|
|
99
|
+
value: str | None
|
|
100
|
+
The extracted text, or `None` when the input does not contain it.
|
|
101
|
+
guess: str | None
|
|
102
|
+
The best candidate text, reported even when `value` is `None`.
|
|
103
|
+
span: Span | None
|
|
104
|
+
Location of `guess` in the input.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
type: Literal["text"] = "text"
|
|
108
|
+
value: str | None
|
|
109
|
+
guess: str | None = None
|
|
110
|
+
span: Span | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class NumberAnswer(_SingleValueAnswer, Generic[N]):
|
|
114
|
+
"""Answer to a `number` field, read from the input or computed from it.
|
|
115
|
+
|
|
116
|
+
Parametrize with `int` for whole numbers (`NumberAnswer[int]` sends `integer: true`) or with
|
|
117
|
+
`float`. Clairic rounds to the field's `decimals` and returns numbers without trailing
|
|
118
|
+
zeros (`1080` for a two-decimal amount). Multi-step arithmetic is not yet reliable: prefer
|
|
119
|
+
extracting the operands and computing in code.
|
|
120
|
+
|
|
121
|
+
Attributes:
|
|
122
|
+
value: N | None
|
|
123
|
+
The number, or `None` when the input does not determine it.
|
|
124
|
+
guess: N | None
|
|
125
|
+
The best candidate number, reported even when `value` is `None`.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
type: Literal["number"] = "number"
|
|
129
|
+
value: N | None
|
|
130
|
+
guess: N | None = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class DateAnswer(_SingleValueAnswer):
|
|
134
|
+
"""Answer to a `date` field, read from the input or worked out from it.
|
|
135
|
+
|
|
136
|
+
Pass `reference_date` when extracting to anchor dates written without a year and relative
|
|
137
|
+
dates such as "net 30".
|
|
138
|
+
|
|
139
|
+
Attributes:
|
|
140
|
+
value: date | None
|
|
141
|
+
The date, or `None` when the input does not determine it.
|
|
142
|
+
guess: date | None
|
|
143
|
+
The best candidate date, reported even when `value` is `None`.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
type: Literal["date"] = "date"
|
|
147
|
+
value: dt.date | None
|
|
148
|
+
guess: dt.date | None = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TimeAnswer(_SingleValueAnswer):
|
|
152
|
+
"""Answer to a `time` field: a time of day, without time zone.
|
|
153
|
+
|
|
154
|
+
Attributes:
|
|
155
|
+
value: time | None
|
|
156
|
+
The time, or `None` when the input does not determine it.
|
|
157
|
+
guess: time | None
|
|
158
|
+
The best candidate time, reported even when `value` is `None`.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
type: Literal["time"] = "time"
|
|
162
|
+
value: dt.time | None
|
|
163
|
+
guess: dt.time | None = None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class DatetimeAnswer(_SingleValueAnswer):
|
|
167
|
+
"""Answer to a `datetime` field: a date and time of day, without time zone.
|
|
168
|
+
|
|
169
|
+
Attributes:
|
|
170
|
+
value: datetime | None
|
|
171
|
+
The date and time, or `None` when the input does not determine them.
|
|
172
|
+
guess: datetime | None
|
|
173
|
+
The best candidate, reported even when `value` is `None`.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
type: Literal["datetime"] = "datetime"
|
|
177
|
+
value: dt.datetime | None
|
|
178
|
+
guess: dt.datetime | None = None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class DurationAnswer(_SingleValueAnswer, Generic[N]):
|
|
182
|
+
"""Answer to a `duration` field: a length of time expressed in `unit`.
|
|
183
|
+
|
|
184
|
+
Parametrize with `int` for whole units (`DurationAnswer[int]` sends `integer: true`) or with
|
|
185
|
+
`float`. Choose the unit with the `Duration` spec; it defaults to seconds.
|
|
186
|
+
|
|
187
|
+
Attributes:
|
|
188
|
+
value: N | None
|
|
189
|
+
The duration in `unit`, or `None` when the input does not determine it.
|
|
190
|
+
guess: N | None
|
|
191
|
+
The best candidate duration, reported even when `value` is `None`.
|
|
192
|
+
unit: DurationUnit
|
|
193
|
+
The unit of `value` and `guess`.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
type: Literal["duration"] = "duration"
|
|
197
|
+
value: N | None
|
|
198
|
+
guess: N | None = None
|
|
199
|
+
unit: DurationUnit
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class TextListAnswer(Answer):
|
|
203
|
+
"""Answer to a `list` field of text items: every matching span of the input, in order.
|
|
204
|
+
|
|
205
|
+
An empty list means nothing in the input matches.
|
|
206
|
+
|
|
207
|
+
Attributes:
|
|
208
|
+
value: list[str]
|
|
209
|
+
The extracted texts, in input order.
|
|
210
|
+
spans: list[Span]
|
|
211
|
+
Location of each item of `value` in the input.
|
|
212
|
+
confidence: float
|
|
213
|
+
Confidence in the list as a whole.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
type: Literal["list"] = "list"
|
|
217
|
+
value: list[str]
|
|
218
|
+
spans: list[Span]
|
|
219
|
+
confidence: float
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class ListAnswer(Answer, Generic[V]):
|
|
223
|
+
"""Answer to a `list` field of numbers or dates, in input order.
|
|
224
|
+
|
|
225
|
+
Parametrize with `int`, `float` or `date`. For a list of text, use `TextListAnswer`. An
|
|
226
|
+
empty list means nothing in the input matches.
|
|
227
|
+
|
|
228
|
+
Attributes:
|
|
229
|
+
value: list[V]
|
|
230
|
+
The extracted items, in input order.
|
|
231
|
+
confidences: list[float]
|
|
232
|
+
Confidence in each item of `value`.
|
|
233
|
+
confidence: float
|
|
234
|
+
Confidence in the list as a whole.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
type: Literal["list"] = "list"
|
|
238
|
+
value: list[V]
|
|
239
|
+
confidences: list[float]
|
|
240
|
+
confidence: float
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class EstimateAnswer(Answer, Generic[N]):
|
|
244
|
+
"""Answer to a numeric `estimate` field: a distribution over the `[min, max]` range.
|
|
245
|
+
|
|
246
|
+
An estimate predicts a value the input does not state, such as a likely sale price, from the
|
|
247
|
+
evidence the input contains. The 80% interval from `p10` to `p90` is calibrated on
|
|
248
|
+
Cloudflare's measurements, which does not guarantee that a given outcome falls inside it.
|
|
249
|
+
Intervals can be too narrow on unfamiliar data.
|
|
250
|
+
|
|
251
|
+
Attributes:
|
|
252
|
+
value: N | None
|
|
253
|
+
Point estimate: the mean rounded to the field's `step` grid.
|
|
254
|
+
mean: float
|
|
255
|
+
Mean of the distribution.
|
|
256
|
+
median: N
|
|
257
|
+
50th percentile.
|
|
258
|
+
p10: N
|
|
259
|
+
10th percentile, lower bound of the 80% interval.
|
|
260
|
+
p90: N
|
|
261
|
+
90th percentile, upper bound of the 80% interval.
|
|
262
|
+
p_below: float
|
|
263
|
+
Probability mass below `min`.
|
|
264
|
+
p_above: float
|
|
265
|
+
Probability mass above `max`.
|
|
266
|
+
quantiles: list[N] | None
|
|
267
|
+
Percentiles 1 to 99 (`quantiles[9] == p10`), when requested with
|
|
268
|
+
`include_quantiles`.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
type: Literal["estimate"] = "estimate"
|
|
272
|
+
value: N | None
|
|
273
|
+
mean: float
|
|
274
|
+
median: N
|
|
275
|
+
p10: N
|
|
276
|
+
p90: N
|
|
277
|
+
p_below: float
|
|
278
|
+
p_above: float
|
|
279
|
+
quantiles: list[N] | None = None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class DateEstimateAnswer(Answer):
|
|
283
|
+
"""Answer to an `estimate` field with date bounds: a distribution over dates.
|
|
284
|
+
|
|
285
|
+
With a `"month"` or `"year"` step, every date is the first day of its period.
|
|
286
|
+
|
|
287
|
+
Attributes:
|
|
288
|
+
value: date | None
|
|
289
|
+
Point estimate: the mean date rounded to the field's `step` grid.
|
|
290
|
+
mean: date
|
|
291
|
+
Mean of the distribution.
|
|
292
|
+
median: date
|
|
293
|
+
50th percentile.
|
|
294
|
+
p10: date
|
|
295
|
+
10th percentile, lower bound of the 80% interval.
|
|
296
|
+
p90: date
|
|
297
|
+
90th percentile, upper bound of the 80% interval.
|
|
298
|
+
quantiles: list[date] | None
|
|
299
|
+
Percentiles 1 to 99, when requested with `include_quantiles`.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
type: Literal["estimate"] = "estimate"
|
|
303
|
+
value: dt.date | None
|
|
304
|
+
mean: dt.date
|
|
305
|
+
median: dt.date
|
|
306
|
+
p10: dt.date
|
|
307
|
+
p90: dt.date
|
|
308
|
+
quantiles: list[dt.date] | None = None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class ForecastStep(BaseModel):
|
|
312
|
+
"""Estimate for one future step of a forecast series.
|
|
313
|
+
|
|
314
|
+
Attributes:
|
|
315
|
+
value: float
|
|
316
|
+
Point forecast for the step.
|
|
317
|
+
mean: float
|
|
318
|
+
Mean of the step's distribution.
|
|
319
|
+
median: float
|
|
320
|
+
50th percentile.
|
|
321
|
+
p10: float
|
|
322
|
+
10th percentile, lower bound of the 80% interval.
|
|
323
|
+
p90: float
|
|
324
|
+
90th percentile, upper bound of the 80% interval.
|
|
325
|
+
p_below: float
|
|
326
|
+
Probability mass below `min`.
|
|
327
|
+
p_above: float
|
|
328
|
+
Probability mass above `max`.
|
|
329
|
+
quantiles: list[float] | None
|
|
330
|
+
Percentiles 1 to 99, when requested with `include_quantiles`.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
model_config = ConfigDict(frozen=True, extra="allow")
|
|
334
|
+
|
|
335
|
+
value: float
|
|
336
|
+
mean: float
|
|
337
|
+
median: float
|
|
338
|
+
p10: float
|
|
339
|
+
p90: float
|
|
340
|
+
p_below: float
|
|
341
|
+
p_above: float
|
|
342
|
+
quantiles: list[float] | None = None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class ForecastAnswer(Answer):
|
|
346
|
+
"""Answer to a `forecast` field: one estimate per future step of a series.
|
|
347
|
+
|
|
348
|
+
Clairic reads the history as plain text in the input (a CSV export, a pasted table, rows
|
|
349
|
+
mixed with notes) and forecasts `horizon` steps ahead within `[min, max]`. It learns patterns
|
|
350
|
+
from the series only, so events the series does not show, such as holidays, are missed.
|
|
351
|
+
Forecasting is basic and does not replace a dedicated time-series model.
|
|
352
|
+
|
|
353
|
+
The parallel lists hold one entry per step. `steps` holds the same per-step figures together
|
|
354
|
+
with the mean and tail probabilities.
|
|
355
|
+
|
|
356
|
+
Attributes:
|
|
357
|
+
value: list[float]
|
|
358
|
+
Point forecast for each step.
|
|
359
|
+
median: list[float]
|
|
360
|
+
50th percentile for each step.
|
|
361
|
+
p10: list[float]
|
|
362
|
+
10th percentile for each step.
|
|
363
|
+
p90: list[float]
|
|
364
|
+
90th percentile for each step.
|
|
365
|
+
steps: list[ForecastStep]
|
|
366
|
+
Full estimate for each step.
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
type: Literal["forecast"] = "forecast"
|
|
370
|
+
value: list[float]
|
|
371
|
+
median: list[float]
|
|
372
|
+
p10: list[float]
|
|
373
|
+
p90: list[float]
|
|
374
|
+
steps: list[ForecastStep]
|
clairic/errors.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Exceptions and warnings raised by the clairic SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"APIConnectionError",
|
|
7
|
+
"APIError",
|
|
8
|
+
"APITimeoutError",
|
|
9
|
+
"AuthenticationError",
|
|
10
|
+
"ClairicConfigurationError",
|
|
11
|
+
"ClairicError",
|
|
12
|
+
"ClairicTruncationWarning",
|
|
13
|
+
"ContextWindowExceededError",
|
|
14
|
+
"InvalidRequestError",
|
|
15
|
+
"ModelDefinitionError",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ClairicError(Exception):
|
|
20
|
+
"""Base class of every exception raised by the clairic SDK."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ClairicConfigurationError(ClairicError):
|
|
24
|
+
"""Raised when the client is missing its Cloudflare account id or API token."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ModelDefinitionError(ClairicError):
|
|
28
|
+
"""Raised when an extraction model cannot be turned into Clairic fields.
|
|
29
|
+
|
|
30
|
+
Raised before any request is sent. The message names the offending field.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class APIError(ClairicError):
|
|
35
|
+
"""Raised when the Cloudflare API answers with an error.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
message: str
|
|
39
|
+
Error message returned by Cloudflare.
|
|
40
|
+
status_code: int
|
|
41
|
+
HTTP status of the response.
|
|
42
|
+
code: int | None
|
|
43
|
+
Cloudflare error code, such as 3030 for a request the model rejects.
|
|
44
|
+
request_id: str | None
|
|
45
|
+
Workers AI request id (`cf-ai-req-id` header), to quote when contacting Cloudflare.
|
|
46
|
+
`None` when Cloudflare rejects the request before it reaches Workers AI, as with
|
|
47
|
+
authentication errors and malformed bodies.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
message: str,
|
|
53
|
+
*,
|
|
54
|
+
status_code: int,
|
|
55
|
+
code: int | None = None,
|
|
56
|
+
request_id: str | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
super().__init__(message)
|
|
59
|
+
self.message = message
|
|
60
|
+
self.status_code = status_code
|
|
61
|
+
self.code = code
|
|
62
|
+
self.request_id = request_id
|
|
63
|
+
|
|
64
|
+
def __str__(self) -> str:
|
|
65
|
+
return f"{self.message} (HTTP {self.status_code}, code {self.code})"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AuthenticationError(APIError):
|
|
69
|
+
"""Raised on HTTP 401: the API token is missing or invalid, or the account id is unknown."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class InvalidRequestError(APIError):
|
|
73
|
+
"""Raised when Cloudflare rejects the request content.
|
|
74
|
+
|
|
75
|
+
Covers schema violations (HTTP 400, code 5006), field definitions the model rejects
|
|
76
|
+
(HTTP 422, code 3030) and malformed bodies (HTTP 400, code 6003).
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ContextWindowExceededError(APIError):
|
|
81
|
+
"""Raised on HTTP 413: the input is estimated to exceed Clairic's 65,536-token context window.
|
|
82
|
+
|
|
83
|
+
Cloudflare estimates the input size from its length in characters before tokenizing it.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class APIConnectionError(ClairicError):
|
|
88
|
+
"""Raised when the Cloudflare API cannot be reached."""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class APITimeoutError(APIConnectionError):
|
|
92
|
+
"""Raised when the Cloudflare API does not answer within the client timeout."""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ClairicTruncationWarning(UserWarning):
|
|
96
|
+
"""Emitted when Clairic reports `truncated: true`: answers were computed on part of the input.
|
|
97
|
+
|
|
98
|
+
Turn it into an error with `warnings.simplefilter("error", ClairicTruncationWarning)`.
|
|
99
|
+
"""
|
clairic/py.typed
ADDED
|
File without changes
|
clairic/result.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""The outcome of an extraction: answers, token usage and request metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import (
|
|
8
|
+
BaseModel,
|
|
9
|
+
ConfigDict,
|
|
10
|
+
SerializationInfo,
|
|
11
|
+
SerializerFunctionWrapHandler,
|
|
12
|
+
field_serializer,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from clairic.answers import Answer
|
|
16
|
+
|
|
17
|
+
__all__ = ["Result", "Usage"]
|
|
18
|
+
|
|
19
|
+
M = TypeVar("M", bound=BaseModel)
|
|
20
|
+
P = TypeVar("P", bound=BaseModel)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Usage(BaseModel):
|
|
24
|
+
"""Token counts of an extraction. Clairic bills input tokens only.
|
|
25
|
+
|
|
26
|
+
Fields that do not fit next to the input in one 8,192-token pass are split across parallel
|
|
27
|
+
passes, and every pass reads the whole input: `input_tokens` counts the state once per pass.
|
|
28
|
+
Fewer fields, or a shorter input, lower the number of passes.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
state_tokens: int
|
|
32
|
+
Tokens of the input text.
|
|
33
|
+
image_tokens: int
|
|
34
|
+
Tokens of the input images.
|
|
35
|
+
schema_tokens: int
|
|
36
|
+
Tokens of the field definitions.
|
|
37
|
+
input_tokens: int
|
|
38
|
+
Billed input tokens, across all passes.
|
|
39
|
+
output_tokens: int
|
|
40
|
+
Generated tokens, always 0: Clairic answers without generating text.
|
|
41
|
+
passes: int
|
|
42
|
+
Number of parallel passes the fields were split into.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
model_config = ConfigDict(frozen=True, extra="allow")
|
|
46
|
+
|
|
47
|
+
state_tokens: int
|
|
48
|
+
image_tokens: int
|
|
49
|
+
schema_tokens: int
|
|
50
|
+
input_tokens: int
|
|
51
|
+
output_tokens: int
|
|
52
|
+
passes: int
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Result(BaseModel, Generic[M]):
|
|
56
|
+
"""Answers to an extraction, with its token usage and request metadata.
|
|
57
|
+
|
|
58
|
+
A result serializes to JSON and back, so storing it keeps the answers, the usage and the
|
|
59
|
+
request id together:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
stored = res.model_dump_json()
|
|
63
|
+
res = Result[Invoice].model_validate_json(stored)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
data: M
|
|
68
|
+
The extraction model, holding one answer per field.
|
|
69
|
+
usage: Usage
|
|
70
|
+
Token counts of the request.
|
|
71
|
+
truncated: bool
|
|
72
|
+
True when Clairic computed the answers on part of the input only.
|
|
73
|
+
model: str
|
|
74
|
+
Clairic model that answered.
|
|
75
|
+
request_id: str | None
|
|
76
|
+
Workers AI request id, to quote when contacting Cloudflare.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
model_config = ConfigDict(frozen=True)
|
|
80
|
+
|
|
81
|
+
data: M
|
|
82
|
+
usage: Usage
|
|
83
|
+
truncated: bool
|
|
84
|
+
model: str
|
|
85
|
+
request_id: str | None = None
|
|
86
|
+
|
|
87
|
+
@field_serializer("data", mode="wrap")
|
|
88
|
+
def _serialize_data_by_alias(
|
|
89
|
+
self, data: M, handler: SerializerFunctionWrapHandler, info: SerializationInfo
|
|
90
|
+
) -> Any:
|
|
91
|
+
# Answers arrive keyed by Clairic field name (the alias), so dump them the same way.
|
|
92
|
+
return data.model_dump(mode=info.mode, by_alias=True)
|
|
93
|
+
|
|
94
|
+
def values_as(self, target: type[P]) -> P:
|
|
95
|
+
"""Return the answer values as an instance of `target`, a model of plain values.
|
|
96
|
+
|
|
97
|
+
Each field of `target` receives the `value` of the answer with the same attribute name;
|
|
98
|
+
answers without a matching field are left out. Pydantic validates the values, so a
|
|
99
|
+
`None` value in a field of `target` that does not accept `None` raises
|
|
100
|
+
`pydantic.ValidationError`.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
target: type[P]
|
|
104
|
+
Pydantic model with plain value fields, such as `total: float | None`.
|
|
105
|
+
"""
|
|
106
|
+
values: dict[str, Any] = {}
|
|
107
|
+
for attribute in type(self.data).model_fields:
|
|
108
|
+
answer = getattr(self.data, attribute)
|
|
109
|
+
if attribute in target.model_fields and isinstance(answer, Answer):
|
|
110
|
+
values[attribute] = answer.model_dump(include={"value"})["value"]
|
|
111
|
+
return target.model_validate(values)
|