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/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)