bynn-decisionnode 0.3.1__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.
- bynn_decisionnode-0.3.1.dist-info/METADATA +939 -0
- bynn_decisionnode-0.3.1.dist-info/RECORD +19 -0
- bynn_decisionnode-0.3.1.dist-info/WHEEL +4 -0
- bynn_decisionnode-0.3.1.dist-info/licenses/LICENSE +21 -0
- decisionnode/__init__.py +195 -0
- decisionnode/_base.py +548 -0
- decisionnode/_batches.py +333 -0
- decisionnode/_boosters.py +553 -0
- decisionnode/_client.py +2269 -0
- decisionnode/_errors.py +218 -0
- decisionnode/_images.py +283 -0
- decisionnode/_questions.py +262 -0
- decisionnode/_sessions.py +711 -0
- decisionnode/_types.py +764 -0
- decisionnode/_uploads.py +265 -0
- decisionnode/_validate.py +934 -0
- decisionnode/_version.py +5 -0
- decisionnode/_videos.py +197 -0
- decisionnode/py.typed +0 -0
decisionnode/_types.py
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
"""What comes back: decisions, answers, the safety check and the model list.
|
|
2
|
+
|
|
3
|
+
Values are the ones the API sent. Numbers are not rounded or renormalised, so a
|
|
4
|
+
probability such as ``0.8200000000000001`` stays as it is, and a list may sum to
|
|
5
|
+
0.99 or 1.01.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Dict, Iterator, List, Mapping, Optional, Sequence, Union
|
|
12
|
+
|
|
13
|
+
from ._boosters import Evidence, parse_evidence
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Answer",
|
|
17
|
+
"TruthAnswer",
|
|
18
|
+
"ChoiceAnswer",
|
|
19
|
+
"ScoreAnswer",
|
|
20
|
+
"NumberAnswer",
|
|
21
|
+
"PointAnswer",
|
|
22
|
+
"BoxAnswer",
|
|
23
|
+
"UnknownAnswer",
|
|
24
|
+
"AnyAnswer",
|
|
25
|
+
"Cost",
|
|
26
|
+
"Usage",
|
|
27
|
+
"Safety",
|
|
28
|
+
"Decision",
|
|
29
|
+
"Fallback",
|
|
30
|
+
"ModelInfo",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Answer:
|
|
36
|
+
"""Base class of every answer.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
type: The answer's type as sent (``truth``, ``choice``, ``score``, ``number``, or a type
|
|
40
|
+
this version of the SDK does not know).
|
|
41
|
+
raw: The answer object exactly as received.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
type: Optional[str]
|
|
45
|
+
raw: Dict[str, Any] = field(repr=False)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class TruthAnswer(Answer):
|
|
50
|
+
"""The answer to a ``truth`` question.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
truth: The probability that the answer is yes, from 0 to 1.
|
|
54
|
+
|
|
55
|
+
``type`` is the answer's type as sent; a ``truth`` question may be answered under
|
|
56
|
+
the API's other name for the type, which reads the same here.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
truth: float
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class ChoiceAnswer(Answer):
|
|
64
|
+
"""The answer to a ``choice`` question.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
choice: The chosen option, by its name exactly as sent.
|
|
68
|
+
confidence: 0 means no better than a uniform guess, 1 means certain.
|
|
69
|
+
probabilities: One entry per option, keyed by the option name exactly as sent.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
choice: str
|
|
73
|
+
confidence: float
|
|
74
|
+
probabilities: Dict[str, float]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class ScoreAnswer(Answer):
|
|
79
|
+
"""The answer to a ``score`` question.
|
|
80
|
+
|
|
81
|
+
Attributes:
|
|
82
|
+
score: The expected level index, from 0 to the number of levels minus 1.
|
|
83
|
+
confidence: How concentrated the probabilities are around the most likely level.
|
|
84
|
+
probabilities: One entry per level, keyed by the level index as an ``int``.
|
|
85
|
+
legend: The level index mapped to the level exactly as sent.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
score: float
|
|
89
|
+
confidence: float
|
|
90
|
+
probabilities: Dict[int, float]
|
|
91
|
+
legend: Dict[int, Any]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class NumberAnswer(Answer):
|
|
96
|
+
"""The answer to a ``number`` question.
|
|
97
|
+
|
|
98
|
+
Attributes:
|
|
99
|
+
number: The most likely value of the grid, as the API wrote it (an ``int`` for a
|
|
100
|
+
whole value, else a ``float``).
|
|
101
|
+
expected: The probability-weighted mean of the values, to the grid's decimals.
|
|
102
|
+
confidence: How concentrated the probabilities are around the most likely value.
|
|
103
|
+
probabilities: The likely values, keyed by the value exactly as the API wrote it
|
|
104
|
+
(``"7"``, ``"3.5"``, ``"-2.345"``), in ascending order.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
number: Union[int, float]
|
|
108
|
+
expected: Union[int, float]
|
|
109
|
+
confidence: float
|
|
110
|
+
probabilities: Dict[str, float]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class PointAnswer(Answer):
|
|
115
|
+
"""The answer to a ``point`` question.
|
|
116
|
+
|
|
117
|
+
Attributes:
|
|
118
|
+
x: The spot's distance from the image's left edge.
|
|
119
|
+
y: The spot's distance from the image's top edge. Both are whole pixels, or
|
|
120
|
+
fractions of the width and height when the question asked for
|
|
121
|
+
``normalized`` units.
|
|
122
|
+
present: The probability that what the instructions describe is in the image at
|
|
123
|
+
all. Under a low ``present`` the spot is only the best guess.
|
|
124
|
+
confidence: The probability that the spot lies on the thing, if it is there.
|
|
125
|
+
image: The id of the image looked in.
|
|
126
|
+
width: That image's width in pixels.
|
|
127
|
+
height: That image's height in pixels.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
x: Union[int, float]
|
|
131
|
+
y: Union[int, float]
|
|
132
|
+
present: float
|
|
133
|
+
confidence: float
|
|
134
|
+
image: str
|
|
135
|
+
width: int
|
|
136
|
+
height: int
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass(frozen=True)
|
|
140
|
+
class BoxAnswer(Answer):
|
|
141
|
+
"""The answer to a ``box`` question.
|
|
142
|
+
|
|
143
|
+
Attributes:
|
|
144
|
+
x1: The left edge of the box.
|
|
145
|
+
y1: The top edge.
|
|
146
|
+
x2: The right edge.
|
|
147
|
+
y2: The bottom edge. In the same units as a point.
|
|
148
|
+
present: The probability that what the instructions describe is in the image at
|
|
149
|
+
all.
|
|
150
|
+
confidence: The probability that the box overlaps the true box by at least half
|
|
151
|
+
(intersection over union of 0.5 or more), if the thing is there.
|
|
152
|
+
image: The id of the image looked in.
|
|
153
|
+
width: That image's width in pixels.
|
|
154
|
+
height: That image's height in pixels.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
x1: Union[int, float]
|
|
158
|
+
y1: Union[int, float]
|
|
159
|
+
x2: Union[int, float]
|
|
160
|
+
y2: Union[int, float]
|
|
161
|
+
present: float
|
|
162
|
+
confidence: float
|
|
163
|
+
image: str
|
|
164
|
+
width: int
|
|
165
|
+
height: int
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(frozen=True)
|
|
169
|
+
class PointPlace:
|
|
170
|
+
"""One point of a :class:`PointsAnswer`.
|
|
171
|
+
|
|
172
|
+
Attributes:
|
|
173
|
+
x: The spot's distance from the image's left edge.
|
|
174
|
+
y: Its distance from the image's top edge, in the question's units.
|
|
175
|
+
confidence: The probability that this point lies on one of the things asked for.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
x: Union[int, float]
|
|
179
|
+
y: Union[int, float]
|
|
180
|
+
confidence: Union[int, float]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(frozen=True)
|
|
184
|
+
class BoxPlace:
|
|
185
|
+
"""One box of a :class:`BoxesAnswer`.
|
|
186
|
+
|
|
187
|
+
Attributes:
|
|
188
|
+
x1: The left edge of the box.
|
|
189
|
+
y1: The top edge.
|
|
190
|
+
x2: The right edge.
|
|
191
|
+
y2: The bottom edge, in the question's units.
|
|
192
|
+
confidence: The probability that this box is right.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
x1: Union[int, float]
|
|
196
|
+
y1: Union[int, float]
|
|
197
|
+
x2: Union[int, float]
|
|
198
|
+
y2: Union[int, float]
|
|
199
|
+
confidence: Union[int, float]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass(frozen=True)
|
|
203
|
+
class PointsAnswer(Answer):
|
|
204
|
+
"""The answer to a ``point`` question asked with ``max``: up to ``max`` points.
|
|
205
|
+
|
|
206
|
+
Attributes:
|
|
207
|
+
count: How many points follow, 0 to ``max``.
|
|
208
|
+
present: The probability that at least one such thing is in the image.
|
|
209
|
+
complete: The probability that nothing was left out, that the true number is
|
|
210
|
+
not above ``max``. Low when the picture holds more than you asked for.
|
|
211
|
+
points: The points, left to right by their centre, ties from the top, each with
|
|
212
|
+
its own ``confidence``; an empty list at 0.
|
|
213
|
+
image: The id of the image looked in.
|
|
214
|
+
width: That image's width in pixels.
|
|
215
|
+
height: That image's height in pixels.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
count: int
|
|
219
|
+
present: Union[int, float]
|
|
220
|
+
complete: Union[int, float]
|
|
221
|
+
points: List[PointPlace]
|
|
222
|
+
image: str
|
|
223
|
+
width: int
|
|
224
|
+
height: int
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(frozen=True)
|
|
228
|
+
class BoxesAnswer(Answer):
|
|
229
|
+
"""The answer to a ``box`` question asked with ``max``: up to ``max`` boxes.
|
|
230
|
+
|
|
231
|
+
Attributes:
|
|
232
|
+
count: How many boxes follow, 0 to ``max``.
|
|
233
|
+
present: The probability that at least one such thing is in the image.
|
|
234
|
+
complete: The probability that nothing was left out, that the true number is
|
|
235
|
+
not above ``max``. Low when the picture holds more than you asked for.
|
|
236
|
+
boxes: The boxes, left to right by their centre, ties from the top, each with its
|
|
237
|
+
own ``confidence``; an empty list at 0.
|
|
238
|
+
image: The id of the image looked in.
|
|
239
|
+
width: That image's width in pixels.
|
|
240
|
+
height: That image's height in pixels.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
count: int
|
|
244
|
+
present: Union[int, float]
|
|
245
|
+
complete: Union[int, float]
|
|
246
|
+
boxes: List[BoxPlace]
|
|
247
|
+
image: str
|
|
248
|
+
width: int
|
|
249
|
+
height: int
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@dataclass(frozen=True)
|
|
253
|
+
class UnknownAnswer(Answer):
|
|
254
|
+
"""An answer of a type this version of the SDK does not know, or of a known type whose
|
|
255
|
+
fields are missing or of the wrong type. Read its fields from ``raw``."""
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
AnyAnswer = Union[
|
|
259
|
+
TruthAnswer,
|
|
260
|
+
ChoiceAnswer,
|
|
261
|
+
ScoreAnswer,
|
|
262
|
+
NumberAnswer,
|
|
263
|
+
PointAnswer,
|
|
264
|
+
BoxAnswer,
|
|
265
|
+
PointsAnswer,
|
|
266
|
+
BoxesAnswer,
|
|
267
|
+
UnknownAnswer,
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass(frozen=True)
|
|
272
|
+
class Cost:
|
|
273
|
+
"""What an answer cost, in USD, as the server sent it.
|
|
274
|
+
|
|
275
|
+
The amounts are the server's, never recomputed: ``usd`` is not summed from the parts,
|
|
276
|
+
since the server rounds each amount on its own. An amount of the wrong type is ``None``.
|
|
277
|
+
|
|
278
|
+
Attributes:
|
|
279
|
+
usd: The answer's whole cost.
|
|
280
|
+
model_usd: The model's share.
|
|
281
|
+
boosters_usd: The boosters' share.
|
|
282
|
+
free_tier: Whether the answer was drawn from the free tier.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
usd: Optional[Union[int, float]] = None
|
|
286
|
+
model_usd: Optional[Union[int, float]] = None
|
|
287
|
+
boosters_usd: Optional[Union[int, float]] = None
|
|
288
|
+
free_tier: Optional[bool] = None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _amount(value: Any) -> Optional[Union[int, float]]:
|
|
292
|
+
return value if isinstance(value, (int, float)) and not isinstance(value, bool) else None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def parse_cost(value: Any) -> Optional[Cost]:
|
|
296
|
+
"""``usage.cost`` as sent, or ``None`` when the usage has none (never zeros)."""
|
|
297
|
+
if not isinstance(value, Mapping):
|
|
298
|
+
return None
|
|
299
|
+
free = value.get("free_tier")
|
|
300
|
+
return Cost(
|
|
301
|
+
usd=_amount(value.get("usd")),
|
|
302
|
+
model_usd=_amount(value.get("model_usd")),
|
|
303
|
+
boosters_usd=_amount(value.get("boosters_usd")),
|
|
304
|
+
free_tier=free if isinstance(free, bool) else None,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@dataclass(frozen=True)
|
|
309
|
+
class Usage:
|
|
310
|
+
"""Token counts of one decision; for a request with videos also the frames and the
|
|
311
|
+
seconds the server read, and for a request with boosters the readings' tokens and the
|
|
312
|
+
booster calls billed (``None`` otherwise). ``cost`` is what the answer cost, as sent
|
|
313
|
+
(``None`` when the server sent none)."""
|
|
314
|
+
|
|
315
|
+
input_tokens: Optional[int] = None
|
|
316
|
+
output_tokens: Optional[int] = None
|
|
317
|
+
# Only a request with videos carries these.
|
|
318
|
+
video_frames: Optional[int] = None
|
|
319
|
+
video_seconds: Optional[Union[int, float]] = None
|
|
320
|
+
# Only a request with boosters carries these: the share of input_tokens the readings
|
|
321
|
+
# take, and the booster calls billed.
|
|
322
|
+
booster_tokens: Optional[int] = None
|
|
323
|
+
boosters: Optional[int] = None
|
|
324
|
+
# What the answer cost, as the server sent it.
|
|
325
|
+
cost: Optional[Cost] = None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@dataclass(frozen=True)
|
|
329
|
+
class Safety:
|
|
330
|
+
"""What the hosted API's safety check found for a request.
|
|
331
|
+
|
|
332
|
+
Attributes:
|
|
333
|
+
checked: Whether the check ran.
|
|
334
|
+
harm: The probability for the ``harm`` category.
|
|
335
|
+
self_harm: The probability for the ``self_harm`` category.
|
|
336
|
+
flagged: The categories above the key's thresholds (a refused category
|
|
337
|
+
included), in the order the server listed them.
|
|
338
|
+
reason: Why the check did not run, when ``checked`` is false.
|
|
339
|
+
categories: Every category the server reported, with its probability.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
checked: bool
|
|
343
|
+
harm: Optional[float] = None
|
|
344
|
+
self_harm: Optional[float] = None
|
|
345
|
+
flagged: List[str] = field(default_factory=list)
|
|
346
|
+
reason: Optional[str] = None
|
|
347
|
+
categories: Dict[str, Optional[float]] = field(default_factory=dict)
|
|
348
|
+
|
|
349
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
350
|
+
"""The result in the shape of the response body's ``safety`` field."""
|
|
351
|
+
if not self.checked:
|
|
352
|
+
return {"checked": False, "reason": self.reason}
|
|
353
|
+
out: Dict[str, Any] = {"checked": True, "harm": self.harm, "self_harm": self.self_harm}
|
|
354
|
+
for name, value in self.categories.items():
|
|
355
|
+
out.setdefault(name, value)
|
|
356
|
+
out["flagged"] = list(self.flagged)
|
|
357
|
+
return out
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@dataclass(frozen=True)
|
|
361
|
+
class Fallback:
|
|
362
|
+
"""A fallback answer: the model asked for had no worker ready at once, so another
|
|
363
|
+
model answered.
|
|
364
|
+
|
|
365
|
+
Attributes:
|
|
366
|
+
served_model: The model that answered, from ``x-decisionnode-fallback``.
|
|
367
|
+
requested_model: The model the request asked for, from
|
|
368
|
+
``x-decisionnode-requested-model``: the family id for a family id or an
|
|
369
|
+
older id, the pinned id for a pinned id.
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
served_model: str
|
|
373
|
+
requested_model: Optional[str] = None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@dataclass(frozen=True)
|
|
377
|
+
class Decision:
|
|
378
|
+
"""The result of one ``decide`` call.
|
|
379
|
+
|
|
380
|
+
Read an answer by its question id: ``decision["route"]`` or ``decision.answers["route"]``.
|
|
381
|
+
|
|
382
|
+
Attributes:
|
|
383
|
+
answers: The answers, by question id.
|
|
384
|
+
model: The model version that answered; for a fine-tuned model its pinned name
|
|
385
|
+
(``decisionnode-moe@acme/fraud:3``).
|
|
386
|
+
usage: Token counts.
|
|
387
|
+
request_id: The ``x-request-id`` response header.
|
|
388
|
+
latency_ms: The server's own time for the request, from ``x-decisionnode-latency-ms``.
|
|
389
|
+
safety: What the safety check found, or ``None`` when the server sent nothing
|
|
390
|
+
(a self-hosted server, for example).
|
|
391
|
+
raw: The parsed response body.
|
|
392
|
+
warnings: What the server says it changed when it read the request's videos (a
|
|
393
|
+
lower rate or resolution), as sent; empty when it sent none.
|
|
394
|
+
evidence: What each booster of the request read: booster name to
|
|
395
|
+
:class:`Evidence`, in the request's order; ``None`` when the request named none.
|
|
396
|
+
fallback: On a fallback answer, the model that answered and the model asked for;
|
|
397
|
+
``None`` on an ordinary answer. ``model`` is the model that answered.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
answers: Dict[str, AnyAnswer]
|
|
401
|
+
model: Optional[str]
|
|
402
|
+
usage: Usage
|
|
403
|
+
request_id: Optional[str]
|
|
404
|
+
latency_ms: Optional[Union[int, float]]
|
|
405
|
+
safety: Optional[Safety]
|
|
406
|
+
raw: Dict[str, Any] = field(repr=False)
|
|
407
|
+
warnings: List[str] = field(default_factory=list)
|
|
408
|
+
evidence: Optional[Dict[str, Evidence]] = None
|
|
409
|
+
fallback: Optional[Fallback] = None
|
|
410
|
+
|
|
411
|
+
def __getitem__(self, question_id: str) -> Any:
|
|
412
|
+
# Typed as Any so that ``decision["route"].choice`` reads naturally under a type
|
|
413
|
+
# checker; ``answers`` keeps the precise union for code that narrows it.
|
|
414
|
+
return self.answers[question_id]
|
|
415
|
+
|
|
416
|
+
def __contains__(self, question_id: object) -> bool:
|
|
417
|
+
return question_id in self.answers
|
|
418
|
+
|
|
419
|
+
def __iter__(self) -> Iterator[str]:
|
|
420
|
+
return iter(self.answers)
|
|
421
|
+
|
|
422
|
+
def __len__(self) -> int:
|
|
423
|
+
return len(self.answers)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@dataclass(frozen=True)
|
|
427
|
+
class ModelInfo:
|
|
428
|
+
"""One entry of the model list returned by ``models()``.
|
|
429
|
+
|
|
430
|
+
Attributes:
|
|
431
|
+
name: The model id to send, such as ``decisionnode-moe``.
|
|
432
|
+
description: What the model is.
|
|
433
|
+
release_date: ``YYYY-MM-DD``.
|
|
434
|
+
limits: Every other field of the entry: the limits the model publishes, such
|
|
435
|
+
as the largest request in tokens.
|
|
436
|
+
raw: The entry exactly as received.
|
|
437
|
+
fine_tuned: ``True`` for a fine-tuned model of the key's workspace; ``None`` on
|
|
438
|
+
the catalogue's own entries.
|
|
439
|
+
base: A fine-tuned model's base model.
|
|
440
|
+
adapter_version: A fine-tuned model's version number; on its plain name, the
|
|
441
|
+
version the name resolves to.
|
|
442
|
+
status: A fine-tuned model's status: ``deployed`` (answers decisions) or ``ready``
|
|
443
|
+
(trained, not deployed); any other value as received.
|
|
444
|
+
"""
|
|
445
|
+
|
|
446
|
+
name: str
|
|
447
|
+
description: Optional[str]
|
|
448
|
+
release_date: Optional[str]
|
|
449
|
+
limits: Dict[str, Any]
|
|
450
|
+
raw: Dict[str, Any] = field(repr=False)
|
|
451
|
+
fine_tuned: Optional[bool] = None
|
|
452
|
+
base: Optional[str] = None
|
|
453
|
+
adapter_version: Optional[int] = None
|
|
454
|
+
status: Optional[str] = None
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# --------------------------------------------------------------------------- parsing
|
|
458
|
+
|
|
459
|
+
# The API's older wire name for the truth type, still accepted for a question sent raw. Its answers read as TruthAnswer.
|
|
460
|
+
LEGACY_TRUTH = "noul"
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class _Malformed(Exception):
|
|
464
|
+
"""A known answer type whose fields are missing or of the wrong type."""
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _number(raw: Mapping[str, Any], key: str) -> Union[int, float]:
|
|
468
|
+
value = raw.get(key)
|
|
469
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
470
|
+
raise _Malformed(key)
|
|
471
|
+
return value
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _string(raw: Mapping[str, Any], key: str) -> str:
|
|
475
|
+
value = raw.get(key)
|
|
476
|
+
if not isinstance(value, str):
|
|
477
|
+
raise _Malformed(key)
|
|
478
|
+
return value
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _places(raw: Mapping[str, Any], key: str, fields: Sequence[str]) -> List[Dict[str, Union[int, float]]]:
|
|
482
|
+
"""The list of a several-places answer: each entry an object of numbers."""
|
|
483
|
+
value = raw.get(key)
|
|
484
|
+
if not isinstance(value, list):
|
|
485
|
+
raise _Malformed(key)
|
|
486
|
+
out = []
|
|
487
|
+
for entry in value:
|
|
488
|
+
if not isinstance(entry, Mapping):
|
|
489
|
+
raise _Malformed(key)
|
|
490
|
+
out.append({name: _number(entry, name) for name in fields})
|
|
491
|
+
return out
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _count(raw: Mapping[str, Any], key: str) -> int:
|
|
495
|
+
value = raw.get(key)
|
|
496
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
497
|
+
raise _Malformed(key)
|
|
498
|
+
return value
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _mapping(raw: Mapping[str, Any], key: str) -> Mapping[str, Any]:
|
|
502
|
+
value = raw.get(key)
|
|
503
|
+
if not isinstance(value, Mapping):
|
|
504
|
+
raise _Malformed(key)
|
|
505
|
+
return value
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _number_values(values: Mapping[str, Any]) -> Dict[str, Union[int, float]]:
|
|
509
|
+
out: Dict[str, Union[int, float]] = {}
|
|
510
|
+
for name, value in values.items():
|
|
511
|
+
if not isinstance(name, str) or isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
512
|
+
raise _Malformed(name)
|
|
513
|
+
out[name] = value
|
|
514
|
+
return out
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _index_keys(values: Mapping[str, Any]) -> Dict[int, Any]:
|
|
518
|
+
# Level indexes are sent as the strings "0" to "K-1".
|
|
519
|
+
out: Dict[int, Any] = {}
|
|
520
|
+
for key, value in values.items():
|
|
521
|
+
if not isinstance(key, str) or not (key == "0" or (key.isdigit() and key.isascii() and key[0] != "0")):
|
|
522
|
+
raise _Malformed(key)
|
|
523
|
+
out[int(key)] = value
|
|
524
|
+
return out
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def parse_answer(raw: Any) -> AnyAnswer:
|
|
528
|
+
"""One answer. A known type with a missing or wrongly typed field is returned as an
|
|
529
|
+
:class:`UnknownAnswer` holding its raw fields, so one odd answer does not fail the decision."""
|
|
530
|
+
if not isinstance(raw, Mapping):
|
|
531
|
+
return UnknownAnswer(type=None, raw={"value": raw})
|
|
532
|
+
raw = dict(raw)
|
|
533
|
+
kind = raw.get("type")
|
|
534
|
+
try:
|
|
535
|
+
if kind == "truth":
|
|
536
|
+
return TruthAnswer(type=kind, raw=raw, truth=_number(raw, "truth"))
|
|
537
|
+
if kind == LEGACY_TRUTH:
|
|
538
|
+
return TruthAnswer(type=kind, raw=raw, truth=_number(raw, LEGACY_TRUTH))
|
|
539
|
+
if kind == "number":
|
|
540
|
+
return NumberAnswer(
|
|
541
|
+
type=kind,
|
|
542
|
+
raw=raw,
|
|
543
|
+
number=_number(raw, "number"),
|
|
544
|
+
expected=_number(raw, "expected"),
|
|
545
|
+
confidence=_number(raw, "confidence"),
|
|
546
|
+
probabilities=_number_values(_mapping(raw, "probabilities")),
|
|
547
|
+
)
|
|
548
|
+
if kind == "point" and ("points" in raw or "boxes" in raw):
|
|
549
|
+
# Several places: told by the answer's own list, not by the request.
|
|
550
|
+
return PointsAnswer(
|
|
551
|
+
type=kind,
|
|
552
|
+
raw=raw,
|
|
553
|
+
count=_count(raw, "count"),
|
|
554
|
+
present=_number(raw, "present"),
|
|
555
|
+
complete=_number(raw, "complete"),
|
|
556
|
+
points=[PointPlace(**p) for p in _places(raw, "points", ("x", "y", "confidence"))],
|
|
557
|
+
image=_string(raw, "image"),
|
|
558
|
+
width=_number(raw, "width"), # type: ignore[arg-type]
|
|
559
|
+
height=_number(raw, "height"), # type: ignore[arg-type]
|
|
560
|
+
)
|
|
561
|
+
if kind == "box" and ("points" in raw or "boxes" in raw):
|
|
562
|
+
return BoxesAnswer(
|
|
563
|
+
type=kind,
|
|
564
|
+
raw=raw,
|
|
565
|
+
count=_count(raw, "count"),
|
|
566
|
+
present=_number(raw, "present"),
|
|
567
|
+
complete=_number(raw, "complete"),
|
|
568
|
+
boxes=[BoxPlace(**b) for b in _places(raw, "boxes", ("x1", "y1", "x2", "y2", "confidence"))],
|
|
569
|
+
image=_string(raw, "image"),
|
|
570
|
+
width=_number(raw, "width"), # type: ignore[arg-type]
|
|
571
|
+
height=_number(raw, "height"), # type: ignore[arg-type]
|
|
572
|
+
)
|
|
573
|
+
if kind == "point":
|
|
574
|
+
return PointAnswer(
|
|
575
|
+
type=kind,
|
|
576
|
+
raw=raw,
|
|
577
|
+
x=_number(raw, "x"),
|
|
578
|
+
y=_number(raw, "y"),
|
|
579
|
+
present=_number(raw, "present"),
|
|
580
|
+
confidence=_number(raw, "confidence"),
|
|
581
|
+
image=_string(raw, "image"),
|
|
582
|
+
width=_number(raw, "width"), # type: ignore[arg-type]
|
|
583
|
+
height=_number(raw, "height"), # type: ignore[arg-type]
|
|
584
|
+
)
|
|
585
|
+
if kind == "box":
|
|
586
|
+
return BoxAnswer(
|
|
587
|
+
type=kind,
|
|
588
|
+
raw=raw,
|
|
589
|
+
x1=_number(raw, "x1"),
|
|
590
|
+
y1=_number(raw, "y1"),
|
|
591
|
+
x2=_number(raw, "x2"),
|
|
592
|
+
y2=_number(raw, "y2"),
|
|
593
|
+
present=_number(raw, "present"),
|
|
594
|
+
confidence=_number(raw, "confidence"),
|
|
595
|
+
image=_string(raw, "image"),
|
|
596
|
+
width=_number(raw, "width"), # type: ignore[arg-type]
|
|
597
|
+
height=_number(raw, "height"), # type: ignore[arg-type]
|
|
598
|
+
)
|
|
599
|
+
if kind == "choice":
|
|
600
|
+
return ChoiceAnswer(
|
|
601
|
+
type=kind,
|
|
602
|
+
raw=raw,
|
|
603
|
+
choice=_string(raw, "choice"),
|
|
604
|
+
confidence=_number(raw, "confidence"),
|
|
605
|
+
probabilities=_number_values(_mapping(raw, "probabilities")),
|
|
606
|
+
)
|
|
607
|
+
if kind == "score":
|
|
608
|
+
probabilities = _index_keys(_mapping(raw, "probabilities"))
|
|
609
|
+
for value in probabilities.values():
|
|
610
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
611
|
+
raise _Malformed("probabilities")
|
|
612
|
+
return ScoreAnswer(
|
|
613
|
+
type=kind,
|
|
614
|
+
raw=raw,
|
|
615
|
+
score=_number(raw, "score"),
|
|
616
|
+
confidence=_number(raw, "confidence"),
|
|
617
|
+
probabilities=probabilities,
|
|
618
|
+
legend=_index_keys(_mapping(raw, "legend")),
|
|
619
|
+
)
|
|
620
|
+
except _Malformed:
|
|
621
|
+
pass
|
|
622
|
+
return UnknownAnswer(type=kind if isinstance(kind, str) else None, raw=raw)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def parse_decision(body: Any, headers: Mapping[str, str]) -> Decision:
|
|
626
|
+
if not isinstance(body, Mapping):
|
|
627
|
+
body = {}
|
|
628
|
+
answers = body.get("answers")
|
|
629
|
+
usage = body.get("usage") if isinstance(body.get("usage"), Mapping) else {}
|
|
630
|
+
warnings = body.get("warnings")
|
|
631
|
+
return Decision(
|
|
632
|
+
answers={str(k): parse_answer(v) for k, v in answers.items()} if isinstance(answers, Mapping) else {},
|
|
633
|
+
model=body.get("model"),
|
|
634
|
+
usage=Usage(
|
|
635
|
+
input_tokens=usage.get("input_tokens"),
|
|
636
|
+
output_tokens=usage.get("output_tokens"),
|
|
637
|
+
video_frames=usage.get("video_frames"),
|
|
638
|
+
video_seconds=usage.get("video_seconds"),
|
|
639
|
+
booster_tokens=usage.get("booster_tokens"),
|
|
640
|
+
boosters=usage.get("boosters"),
|
|
641
|
+
cost=parse_cost(usage.get("cost")),
|
|
642
|
+
),
|
|
643
|
+
request_id=headers.get("x-request-id"),
|
|
644
|
+
latency_ms=parse_latency(headers.get("x-decisionnode-latency-ms")),
|
|
645
|
+
safety=parse_safety(headers.get("x-decisionnode-safety")),
|
|
646
|
+
raw=dict(body),
|
|
647
|
+
warnings=[w for w in warnings if isinstance(w, str)] if isinstance(warnings, list) else [],
|
|
648
|
+
evidence=parse_evidence(body.get("evidence")),
|
|
649
|
+
fallback=parse_fallback(headers),
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def parse_fallback(headers: Mapping[str, str]) -> Optional[Fallback]:
|
|
654
|
+
"""The fallback headers of an answer, or ``None`` on an ordinary answer."""
|
|
655
|
+
served = headers.get("x-decisionnode-fallback")
|
|
656
|
+
if served is None:
|
|
657
|
+
return None
|
|
658
|
+
return Fallback(served_model=served, requested_model=headers.get("x-decisionnode-requested-model"))
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def parse_latency(value: Optional[str]) -> Optional[Union[int, float]]:
|
|
662
|
+
if value is None:
|
|
663
|
+
return None
|
|
664
|
+
try:
|
|
665
|
+
number = float(value.strip())
|
|
666
|
+
except ValueError:
|
|
667
|
+
return None
|
|
668
|
+
if number != number or number in (float("inf"), float("-inf")):
|
|
669
|
+
return None
|
|
670
|
+
return int(number) if number.is_integer() else number
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def parse_model(raw: Any) -> ModelInfo:
|
|
674
|
+
entry = dict(raw) if isinstance(raw, Mapping) else {}
|
|
675
|
+
fine_tuned, base, version, status = (entry.get(k) for k in ("fine_tuned", "base", "adapter_version", "status"))
|
|
676
|
+
return ModelInfo(
|
|
677
|
+
name=entry.get("name", ""),
|
|
678
|
+
description=entry.get("description"),
|
|
679
|
+
release_date=entry.get("release_date"),
|
|
680
|
+
limits={k: v for k, v in entry.items() if k not in ("name", "description", "release_date")},
|
|
681
|
+
raw=entry,
|
|
682
|
+
fine_tuned=fine_tuned if isinstance(fine_tuned, bool) else None,
|
|
683
|
+
base=base if isinstance(base, str) else None,
|
|
684
|
+
adapter_version=version if isinstance(version, int) and not isinstance(version, bool) else None,
|
|
685
|
+
status=status if isinstance(status, str) else None,
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _split_outside_quotes(text: str, sep: str) -> List[str]:
|
|
690
|
+
parts, current, quoted, escaped = [], [], False, False
|
|
691
|
+
for ch in text:
|
|
692
|
+
if escaped:
|
|
693
|
+
current.append(ch)
|
|
694
|
+
escaped = False
|
|
695
|
+
elif ch == "\\" and quoted:
|
|
696
|
+
current.append(ch)
|
|
697
|
+
escaped = True
|
|
698
|
+
elif ch == '"':
|
|
699
|
+
current.append(ch)
|
|
700
|
+
quoted = not quoted
|
|
701
|
+
elif ch == sep and not quoted:
|
|
702
|
+
parts.append("".join(current))
|
|
703
|
+
current = []
|
|
704
|
+
else:
|
|
705
|
+
current.append(ch)
|
|
706
|
+
parts.append("".join(current))
|
|
707
|
+
return parts
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _unquote(value: str) -> str:
|
|
711
|
+
value = value.strip()
|
|
712
|
+
if len(value) >= 2 and value[0] == '"' and value[-1] == '"':
|
|
713
|
+
inner = value[1:-1]
|
|
714
|
+
out, escaped = [], False
|
|
715
|
+
for ch in inner:
|
|
716
|
+
if escaped:
|
|
717
|
+
out.append(ch)
|
|
718
|
+
escaped = False
|
|
719
|
+
elif ch == "\\":
|
|
720
|
+
escaped = True
|
|
721
|
+
else:
|
|
722
|
+
out.append(ch)
|
|
723
|
+
return "".join(out)
|
|
724
|
+
return value
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def parse_safety(value: Optional[str]) -> Optional[Safety]:
|
|
728
|
+
"""Parse the ``x-decisionnode-safety`` header.
|
|
729
|
+
|
|
730
|
+
The header is a list of ``name=value`` members with optional ``;parameters``:
|
|
731
|
+
``harm=0.031, self_harm=0.004``, ``harm=0.912;flagged, self_harm=0.004``, or
|
|
732
|
+
``unchecked;reason="no-model-question"`` when the check did not run.
|
|
733
|
+
"""
|
|
734
|
+
if value is None:
|
|
735
|
+
return None
|
|
736
|
+
value = value.strip()
|
|
737
|
+
if not value:
|
|
738
|
+
return None
|
|
739
|
+
members = [m.strip() for m in _split_outside_quotes(value, ",") if m.strip()]
|
|
740
|
+
categories: Dict[str, Optional[float]] = {}
|
|
741
|
+
flagged: List[str] = []
|
|
742
|
+
for member in members:
|
|
743
|
+
head, *params = [p.strip() for p in _split_outside_quotes(member, ";")]
|
|
744
|
+
name, has_value, number = head.partition("=")
|
|
745
|
+
name = name.strip()
|
|
746
|
+
param_map: Dict[str, str] = {}
|
|
747
|
+
for param in params:
|
|
748
|
+
key, _, pvalue = param.partition("=")
|
|
749
|
+
param_map[key.strip()] = _unquote(pvalue) if pvalue else "?1"
|
|
750
|
+
if name == "unchecked" and not has_value:
|
|
751
|
+
return Safety(checked=False, reason=param_map.get("reason"))
|
|
752
|
+
try:
|
|
753
|
+
categories[name] = float(number) if has_value else None
|
|
754
|
+
except ValueError:
|
|
755
|
+
categories[name] = None
|
|
756
|
+
if any(param_map.get(flag, "?0") != "?0" for flag in ("flagged", "blocked")):
|
|
757
|
+
flagged.append(name)
|
|
758
|
+
return Safety(
|
|
759
|
+
checked=True,
|
|
760
|
+
harm=categories.get("harm"),
|
|
761
|
+
self_harm=categories.get("self_harm"),
|
|
762
|
+
flagged=flagged,
|
|
763
|
+
categories=categories,
|
|
764
|
+
)
|