anyformat 1.2.0__tar.gz → 1.3.0__tar.gz
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.
- {anyformat-1.2.0 → anyformat-1.3.0}/PKG-INFO +1 -1
- {anyformat-1.2.0 → anyformat-1.3.0}/pyproject.toml +1 -1
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/sdk/_viz.py +6 -2
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/sdk/cli.py +1 -1
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/workflow/fields.py +1 -1
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/workflow/results.py +60 -12
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/workflow/topology.py +31 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/README.md +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/sdk/__init__.py +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/sdk/_banner.py +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/sdk/_http.py +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/sdk/_source.py +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/sdk/builder.py +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/sdk/client.py +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/sdk/errors.py +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/sdk/handles.py +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/sdk/result.py +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/sdk/views.py +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/workflow/__init__.py +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/workflow/builder.py +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/workflow/checks.py +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/workflow/definition.py +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/workflow/nodes.py +0 -0
- {anyformat-1.2.0 → anyformat-1.3.0}/src/anyformat/workflow/schema.py +0 -0
|
@@ -15,6 +15,8 @@ from anyformat.sdk._source import SourceDocument
|
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from PIL.Image import Image
|
|
18
|
+
from PIL.ImageDraw import ImageDraw
|
|
19
|
+
from PIL.ImageFont import FreeTypeFont, ImageFont
|
|
18
20
|
|
|
19
21
|
_EMERALD = (74, 222, 128) # #4ADE80 high (>=80)
|
|
20
22
|
_AMBER = (251, 191, 36) # #FBBF24 medium (50-79)
|
|
@@ -110,7 +112,9 @@ def _draw_on_page(page: "Image", boxes: list[Box]) -> "Image":
|
|
|
110
112
|
return Image.alpha_composite(base, overlay).convert("RGB")
|
|
111
113
|
|
|
112
114
|
|
|
113
|
-
def _draw_label(
|
|
115
|
+
def _draw_label(
|
|
116
|
+
draw: "ImageDraw", x: float, y: float, text: str, color: tuple[int, int, int], font: "FreeTypeFont | ImageFont"
|
|
117
|
+
) -> None:
|
|
114
118
|
left, top, right, bottom = draw.textbbox((0, 0), text, font=font)
|
|
115
119
|
pad = 2
|
|
116
120
|
tag_w = (right - left) + 2 * pad
|
|
@@ -119,7 +123,7 @@ def _draw_label(draw, x: float, y: float, text: str, color: tuple[int, int, int]
|
|
|
119
123
|
draw.text((x + pad, y + pad), text, fill=(255, 255, 255, 255), font=font)
|
|
120
124
|
|
|
121
125
|
|
|
122
|
-
def _load_font(size: int):
|
|
126
|
+
def _load_font(size: int) -> "FreeTypeFont | ImageFont":
|
|
123
127
|
from PIL import ImageFont
|
|
124
128
|
|
|
125
129
|
return ImageFont.load_default(size=size)
|
|
@@ -71,7 +71,7 @@ class _Base(BaseModel):
|
|
|
71
71
|
|
|
72
72
|
@model_validator(mode="before")
|
|
73
73
|
@classmethod
|
|
74
|
-
def _inject_data_type(cls, data):
|
|
74
|
+
def _inject_data_type(cls, data: object) -> object:
|
|
75
75
|
# Direct Python construction (``StringField(name=..., description=...)``)
|
|
76
76
|
# should not require repeating the discriminator literal — the subclass
|
|
77
77
|
# already pins it. Inject it from the ``Literal[…]`` annotation when
|
|
@@ -5,9 +5,16 @@ This is the canonical response shape — every cookbook recipe and the OpenAPI s
|
|
|
5
5
|
mirror it.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import Any, Generic, Optional, TypeVar, Union
|
|
8
|
+
from typing import Any, Generic, Literal, Optional, TypeVar, Union
|
|
9
9
|
|
|
10
|
-
from pydantic import
|
|
10
|
+
from pydantic import (
|
|
11
|
+
BaseModel,
|
|
12
|
+
Field,
|
|
13
|
+
SerializerFunctionWrapHandler,
|
|
14
|
+
field_validator,
|
|
15
|
+
model_serializer,
|
|
16
|
+
model_validator,
|
|
17
|
+
)
|
|
11
18
|
|
|
12
19
|
# Generic over the extracted ``value`` / ``value_override`` so SDK consumers
|
|
13
20
|
# can parameterise ``ExtractedField[float]`` etc. and stop seeing ``Any``. The
|
|
@@ -30,6 +37,30 @@ class Evidence(BaseModel):
|
|
|
30
37
|
page_number: int = Field(description="1-indexed page number where the snippet was found.")
|
|
31
38
|
|
|
32
39
|
|
|
40
|
+
class ConsolidationCandidate(BaseModel):
|
|
41
|
+
"""One part of a long document's answer for a field whose parts disagreed."""
|
|
42
|
+
|
|
43
|
+
value: Optional[Any] = Field(default=None, description="The value this part of the document gave.")
|
|
44
|
+
value_unit: Optional[str] = Field(default=None, description="The unit of `value`, when it has one.")
|
|
45
|
+
evidence: list[Evidence] = Field(
|
|
46
|
+
default_factory=list, description="Source-text snippets this part cited. Empty when it cited none."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@field_validator("evidence", mode="before")
|
|
50
|
+
@classmethod
|
|
51
|
+
def _coerce_null_evidence(cls, value: Any) -> Any:
|
|
52
|
+
return [] if value is None else value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Consolidation(BaseModel):
|
|
56
|
+
"""Why a field of a long document has no value: its parts gave answers that could not be combined."""
|
|
57
|
+
|
|
58
|
+
status: Literal["unresolved"] = Field(description="Always `unresolved`: no value could be chosen.")
|
|
59
|
+
candidates: list[ConsolidationCandidate] = Field(
|
|
60
|
+
description="Each part's answer, to compare against the document before entering the value."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
33
64
|
class ExtractedField(BaseModel, Generic[ValueT]):
|
|
34
65
|
"""One extracted field's value, confidence, and supporting evidence."""
|
|
35
66
|
|
|
@@ -58,6 +89,20 @@ class ExtractedField(BaseModel, Generic[ValueT]):
|
|
|
58
89
|
default_factory=list,
|
|
59
90
|
description="Source-text snippets the model used to derive this value.",
|
|
60
91
|
)
|
|
92
|
+
value_source: Optional[Literal["document", "lookup"]] = Field(
|
|
93
|
+
default=None,
|
|
94
|
+
description="Where a smart-lookup field's value came from. Omitted for other fields.",
|
|
95
|
+
)
|
|
96
|
+
lookup_check: Optional[Literal["confirmed", "conflict"]] = Field(
|
|
97
|
+
default=None,
|
|
98
|
+
description="Whether the lookup `confirmed` the document value or was in `conflict` with it (the document "
|
|
99
|
+
"value is kept). Omitted when no lookup compared the two.",
|
|
100
|
+
)
|
|
101
|
+
consolidation: Optional[Consolidation] = Field(
|
|
102
|
+
default=None,
|
|
103
|
+
description="Set when a long document's parts gave answers that could not be combined; `value` is then "
|
|
104
|
+
"`null`. Omitted otherwise.",
|
|
105
|
+
)
|
|
61
106
|
|
|
62
107
|
@field_validator("evidence", mode="before")
|
|
63
108
|
@classmethod
|
|
@@ -66,6 +111,15 @@ class ExtractedField(BaseModel, Generic[ValueT]):
|
|
|
66
111
|
# when a field has no extracted value; collapse that to an empty list.
|
|
67
112
|
return [] if value is None else value
|
|
68
113
|
|
|
114
|
+
@model_serializer(mode="wrap")
|
|
115
|
+
def _drop_unset_provenance(self, handler: SerializerFunctionWrapHandler): # noqa: ANN202 — pydantic reads a return annotation as the serialization schema
|
|
116
|
+
# Only lookup or unresolved fields carry these; every other field keeps its shape.
|
|
117
|
+
data = handler(self)
|
|
118
|
+
for key in ("value_source", "lookup_check", "consolidation"):
|
|
119
|
+
if data.get(key) is None:
|
|
120
|
+
data.pop(key, None)
|
|
121
|
+
return data
|
|
122
|
+
|
|
69
123
|
|
|
70
124
|
# A scalar extracted field, or a list of object-field rows — each row is a dict
|
|
71
125
|
# from cell name to ExtractedField (the shape produced by object-type extraction).
|
|
@@ -266,11 +320,9 @@ class ClassificationResult(BaseModel):
|
|
|
266
320
|
|
|
267
321
|
|
|
268
322
|
class Validation(BaseModel):
|
|
269
|
-
"""A configured rule verdict
|
|
323
|
+
"""A configured rule verdict."""
|
|
270
324
|
|
|
271
|
-
rule_id: str = Field(
|
|
272
|
-
description="The configured rule ID, or an `extract.consolidation:` ID for an automatic review warning."
|
|
273
|
-
)
|
|
325
|
+
rule_id: str = Field(description="The configured rule ID.")
|
|
274
326
|
description: str = Field(
|
|
275
327
|
default="",
|
|
276
328
|
description="The rule's natural-language description; empty for a deterministic rule.",
|
|
@@ -313,11 +365,7 @@ class Extraction(BaseModel):
|
|
|
313
365
|
)
|
|
314
366
|
validations: list[Validation] = Field(
|
|
315
367
|
default_factory=list,
|
|
316
|
-
description=
|
|
317
|
-
"Configured validation verdicts and automatic extraction review warnings. "
|
|
318
|
-
"Unresolved chunk fields have a null value and an inconclusive warning with candidate values and evidence, "
|
|
319
|
-
"even without a validate node."
|
|
320
|
-
),
|
|
368
|
+
description="Verdicts of the workflow's validate nodes.",
|
|
321
369
|
)
|
|
322
370
|
|
|
323
371
|
|
|
@@ -484,7 +532,7 @@ class ResultsResponseV3(ResultsResponse):
|
|
|
484
532
|
model_config = {"json_schema_extra": _v3_schema_transform}
|
|
485
533
|
|
|
486
534
|
@model_serializer(mode="wrap")
|
|
487
|
-
def _drop_legacy_collection_id(self, handler):
|
|
535
|
+
def _drop_legacy_collection_id(self, handler: SerializerFunctionWrapHandler): # noqa: ANN202 — pydantic reads a return annotation as the serialization schema
|
|
488
536
|
data = handler(self)
|
|
489
537
|
data.pop("collection_id", None)
|
|
490
538
|
return data
|
|
@@ -618,6 +618,35 @@ class NoParseAsBranchTarget:
|
|
|
618
618
|
]
|
|
619
619
|
|
|
620
620
|
|
|
621
|
+
class NoParseUnderSplitter:
|
|
622
|
+
"""Reject a Parse reachable from a Splitter, however deep. A Parse inside a
|
|
623
|
+
partition re-parses every file of the packet once per partition, not the slice."""
|
|
624
|
+
|
|
625
|
+
name = "no_parse_under_splitter"
|
|
626
|
+
|
|
627
|
+
def check(self, graph: "WorkflowDefinition") -> list[Violation]:
|
|
628
|
+
successors: dict[str, list[str]] = {}
|
|
629
|
+
for edge in graph.edges:
|
|
630
|
+
successors.setdefault(edge.source, []).append(edge.target)
|
|
631
|
+
frontier = [n.id for n in graph.nodes if isinstance(n, SplitterNode)]
|
|
632
|
+
downstream_of_splitter: set[str] = set()
|
|
633
|
+
while frontier:
|
|
634
|
+
for target in successors.get(frontier.pop(), []):
|
|
635
|
+
if target not in downstream_of_splitter:
|
|
636
|
+
downstream_of_splitter.add(target)
|
|
637
|
+
frontier.append(target)
|
|
638
|
+
return [
|
|
639
|
+
Violation(
|
|
640
|
+
rule=self.name,
|
|
641
|
+
message=f"parse node {node.id!r} sits downstream of a splitter; a parse "
|
|
642
|
+
"cannot run inside a partition.",
|
|
643
|
+
node_ids=[node.id],
|
|
644
|
+
)
|
|
645
|
+
for node in graph.nodes
|
|
646
|
+
if isinstance(node, ParseNode) and node.id in downstream_of_splitter
|
|
647
|
+
]
|
|
648
|
+
|
|
649
|
+
|
|
621
650
|
class EveryExtractNodeHasFields:
|
|
622
651
|
"""Belt-and-braces redundancy with ``ExtractionSchema.fields min_length=1``:
|
|
623
652
|
pydantic catches the empty-fields case first at the wire boundary, but
|
|
@@ -839,6 +868,7 @@ RULES: list[TopologyRule] = [
|
|
|
839
868
|
EveryExtractNodeHasFields(),
|
|
840
869
|
EveryIfElseHasTrueBranch(),
|
|
841
870
|
NoParseAsBranchTarget(),
|
|
871
|
+
NoParseUnderSplitter(),
|
|
842
872
|
NoCodeSandboxTemplate(),
|
|
843
873
|
CodeSandboxTemplateFollowsExtract(),
|
|
844
874
|
ValidationRuleIdReferencesExist(),
|
|
@@ -894,6 +924,7 @@ __all__ = [
|
|
|
894
924
|
"ExactlyOneParseNode",
|
|
895
925
|
"NoCycles",
|
|
896
926
|
"NoParseAsBranchTarget",
|
|
927
|
+
"NoParseUnderSplitter",
|
|
897
928
|
"NonBranchingNodeHasAtMostOneOutgoing",
|
|
898
929
|
"RULES",
|
|
899
930
|
"SingleUpstreamNodeHasOneIncoming",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|