Coverage for src / quber / playground / answers / shapes.py: 100%
44 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-09-23 22:14 -0400
« prev ^ index » next coverage.py v7.14.0, created at 2026-09-23 22:14 -0400
1"""The defined return values an answer can take.
3A question that names a figure should come back as that figure, not as a
4sentence containing it. These models are the shapes an answer is allowed to
5have; which shape a given question gets is decided in `routing`.
7Every shape carries the ids it was read from. Table cells arrive tagged
8(`<td id="t0-15-1">$2.97</td>`), so a scalar can name the exact cell its
9value came from, and the value it reports can be checked against the text
10printed in that cell — a grounding test no prose answer admits.
11"""
13from __future__ import annotations
15from decimal import Decimal
16from typing import List, Literal, Optional, Union
18from pydantic import BaseModel, Field, computed_field
20from quber.playground.answers.figures import as_number
23class Scalar(BaseModel):
24 """One figure, as the source prints it."""
26 kind: Literal["scalar"] = "scalar"
27 value: str = Field(
28 description=(
29 "The figure exactly as printed in the source, including its currency "
30 "sign, separators, and any trailing sign: '$14.47', '4.6x', '92.6%', "
31 "'(0.02)'. No words, no sentence, no explanation."
32 )
33 )
35 unit: Optional[str] = Field(
36 default=None,
37 description="What the number counts: 'USD per share', 'USD thousands', 'percent', 'x'.",
38 )
39 period: Optional[str] = Field(
40 default=None,
41 description="The period or as-of date the figure belongs to, as printed: 'March 31, 2026'.",
42 )
43 label: Optional[str] = Field(
44 default=None,
45 description="The line item as printed in the source: 'Book Value Per Share of Common Stock'.",
46 )
47 source_id: Optional[str] = Field(
48 default=None,
49 description=(
50 "The id of the single cell or chunk this figure was read from. Must "
51 "appear verbatim in the context."
52 ),
53 )
55 @computed_field # type: ignore[prop-decorator]
56 @property
57 def number(self) -> Optional[Decimal]:
58 """The figure as a plain number, sign applied and separators removed.
60 Derived from `value` rather than asked of the model. The conversion is
61 deterministic, and a model asked for it alongside the figure returns the
62 figure every time and the number only most of the time: one question put
63 four times came back with the figure four times and the number three.
64 A numeric column with unpredictable holes is worse than one computed in
65 a single place and tested there.
66 """
67 return as_number(self.value)
70class Point(BaseModel):
71 """One labelled figure inside a series."""
73 label: str = Field(description="What this point is: a period, a segment, a category.")
74 value: str = Field(description="The figure as printed.")
75 source_id: Optional[str] = None
77 @computed_field # type: ignore[prop-decorator]
78 @property
79 def number(self) -> Optional[Decimal]:
80 """The figure as a number, read from `value` on the same terms a scalar's is."""
81 return as_number(self.value)
84class Series(BaseModel):
85 """One measure across several labels — periods, segments, buckets."""
87 kind: Literal["series"] = "series"
88 measure: str = Field(description="What is being measured across the points.")
89 unit: Optional[str] = None
90 points: List[Point] = Field(min_length=1)
93class Grid(BaseModel):
94 """A rectangle of values, when the question asks for a whole table."""
96 kind: Literal["grid"] = "grid"
97 columns: List[str]
98 rows: List[List[str]] = Field(description="Each row holds one string per column, as printed.")
99 unit: Optional[str] = None
102class Prose(BaseModel):
103 """Free text, for questions that ask for explanation rather than a figure."""
105 kind: Literal["prose"] = "prose"
106 text: str
109class Unanswerable(BaseModel):
110 """The context does not contain the answer.
112 A distinct shape rather than an empty scalar: a caller in code must be able
113 to tell 'not present' from 'zero' without reading English.
114 """
116 kind: Literal["unanswerable"] = "unanswerable"
117 reason: str = Field(description="One sentence: what was missing from the context.")
120Payload = Union[Scalar, Series, Grid, Prose, Unanswerable]
123class Answer(BaseModel):
124 """The single return value. Always this type; the payload varies."""
126 payload: Payload = Field(discriminator="kind")
127 cited_ids: List[str] = Field(
128 default_factory=list,
129 description="Chunk ids and/or table cell ids supporting the payload.",
130 )