| """Base schema types and the ExtractionResult wrapper. |
| |
| Every extraction returns an ExtractionResult[T] where T is the domain schema |
| (Invoice, Receipt, or a filing schema in v2). The wrapper carries the extracted |
| data plus per-field confidence, warnings, and provenance so downstream code |
| never has to reach into the LLM response directly. |
| """ |
| from __future__ import annotations |
|
|
| from typing import Generic, TypeVar |
|
|
| from pydantic import BaseModel, ConfigDict, Field, field_validator |
|
|
| |
| T = TypeVar("T", bound=BaseModel) |
|
|
|
|
| class StrictModel(BaseModel): |
| """Base model with strict config — matches OpenAI structured-outputs requirements.""" |
|
|
| model_config = ConfigDict( |
| extra="forbid", |
| str_strip_whitespace=True, |
| validate_assignment=True, |
| ) |
|
|
|
|
| class FieldConfidence(StrictModel): |
| """Model-reported confidence for a single extracted field.""" |
|
|
| field: str = Field(description="Dotted path to the field, e.g. 'total' or 'line_items[0].price'.") |
| score: float = Field( |
| ge=0.0, |
| le=1.0, |
| description="Model's self-reported confidence for this field (0.0 = uncertain, 1.0 = certain).", |
| ) |
| reasoning: str | None = Field( |
| default=None, |
| description="Brief explanation of why confidence is not 1.0 (only present when score < 0.9).", |
| ) |
|
|
|
|
| class ExtractionWarning(StrictModel): |
| """A warning surfaced during extraction — non-fatal, human-reviewable.""" |
|
|
| field: str | None = Field(default=None, description="Field the warning applies to, if applicable.") |
| message: str = Field(description="Human-readable warning.") |
| severity: str = Field( |
| default="info", |
| description="One of: info, warning, error. 'error' means the field could not be extracted.", |
| ) |
|
|
| @field_validator("severity") |
| @classmethod |
| def _validate_severity(cls, v: str) -> str: |
| if v not in {"info", "warning", "error"}: |
| raise ValueError(f"severity must be info/warning/error, got {v!r}") |
| return v |
|
|
|
|
| class ExtractionResult(StrictModel, Generic[T]): |
| """Wrapper around any extracted domain schema. |
| |
| - `data` is the strongly-typed domain object (Invoice, Receipt, etc.) |
| - `field_confidences` gives per-field confidence |
| - `overall_confidence` is a single 0-1 rollup for quick UI display |
| - `warnings` surfaces low-confidence or missing fields to the caller |
| - `raw_text_snippet` keeps a slice of the source for debugging / traceability |
| """ |
|
|
| document_type: str = Field(description="Identifier of the domain schema used (e.g. 'invoice').") |
| data: T = Field(description="The extracted, schema-validated domain object.") |
| field_confidences: list[FieldConfidence] = Field( |
| default_factory=list, |
| description="Per-field confidence scores. Empty list is acceptable but discouraged.", |
| ) |
| overall_confidence: float = Field( |
| ge=0.0, le=1.0, description="Rollup confidence across all fields (mean of scores)." |
| ) |
| warnings: list[ExtractionWarning] = Field( |
| default_factory=list, description="Non-fatal issues surfaced during extraction." |
| ) |
| raw_text_snippet: str | None = Field( |
| default=None, |
| max_length=2000, |
| description="First 2K chars of source text for debugging. None for pure-image inputs.", |
| ) |
|
|