Spaces:
Runtime error
Runtime error
| """Schema-constrained extraction types. | |
| These Pydantic models are the contract the LLM extractor must satisfy. The model | |
| may NEVER emit a value outside these constraints — condition ratings are a closed | |
| enum, evidence is mandatory, and unsupported fields are represented explicitly | |
| (``SupportLevel.NOT_FOUND``) rather than interpolated. | |
| """ | |
| from __future__ import annotations | |
| from enum import Enum | |
| from pydantic import BaseModel, Field, field_validator | |
| class ConditionRating(str, Enum): | |
| """RICS Home Survey condition rating — a closed set. | |
| The extractor must map any source rating onto exactly one of these. It may | |
| never invent an intermediate value, upgrade, or downgrade. ``NI`` / ``NA`` | |
| are explicit "no rating present" sentinels so a missing rating is never | |
| silently coerced to a real one. | |
| """ | |
| CR1 = "1" # No repair currently needed. | |
| CR2 = "2" # Defects requiring repair/replacement but not urgent. | |
| CR3 = "3" # Defects that are serious and/or need urgent attention. | |
| NI = "NI" # Not inspected. | |
| NA = "NA" # Not applicable / no rating stated in source. | |
| def coerce(cls, value: object) -> "ConditionRating": | |
| """Map a raw model/source value onto the enum without inventing a rating. | |
| Anything that is not an exact, recognised rating becomes :attr:`NA` | |
| (not a guessed CR1/CR2/CR3). This is deliberate: an unparseable rating | |
| must degrade to "no rating", never to a fabricated severity. | |
| """ | |
| if isinstance(value, cls): | |
| return value | |
| s = str(value or "").strip().upper() | |
| direct = { | |
| "1": cls.CR1, "CR1": cls.CR1, "CONDITION RATING 1": cls.CR1, | |
| "2": cls.CR2, "CR2": cls.CR2, "CONDITION RATING 2": cls.CR2, | |
| "3": cls.CR3, "CR3": cls.CR3, "CONDITION RATING 3": cls.CR3, | |
| "NI": cls.NI, "NOT INSPECTED": cls.NI, | |
| "NA": cls.NA, "N/A": cls.NA, "": cls.NA, "NONE": cls.NA, | |
| } | |
| return direct.get(s, cls.NA) | |
| class SupportLevel(str, Enum): | |
| """How well a claim is backed by cited source spans.""" | |
| SUPPORTED = "supported" # Every token traceable to a cited span. | |
| PARTIAL = "partial" # Core claim supported; minor unverified detail. | |
| NOT_FOUND = "not_found" # No supporting evidence — must be dropped. | |
| class EvidenceSpan(BaseModel): | |
| """A literal span copied from a retrieved source chunk. | |
| ``text`` MUST be a verbatim (or near-verbatim) substring of the chunk | |
| identified by ``chunk_id``. The citation validator rejects spans that do | |
| not actually appear in their cited chunk. | |
| """ | |
| chunk_id: str = Field(..., description="ID of the retrieved chunk this span came from.") | |
| doc_id: str | None = Field(default=None, description="Source document UUID.") | |
| page: int | None = Field(default=None, description="1-based page number when known.") | |
| section_label: str | None = Field(default=None, description="Section/page heading label.") | |
| text: str = Field(..., min_length=1, description="Verbatim span from the cited chunk.") | |
| def _strip(cls, v: str) -> str: | |
| v = v.strip() | |
| if not v: | |
| raise ValueError("evidence span text cannot be blank") | |
| return v | |
| class GroundedClaim(BaseModel): | |
| """An atomic claim and the evidence that supports it.""" | |
| claim: str = Field(..., min_length=1) | |
| supporting_spans: list[EvidenceSpan] = Field(default_factory=list) | |
| support: SupportLevel = Field(default=SupportLevel.NOT_FOUND) | |
| page_refs: list[int] = Field(default_factory=list) | |
| class SurveyFinding(BaseModel): | |
| """One condition finding for a building element, fully evidence-backed. | |
| This is the structured-extraction unit from STEP 4 of the spec. ``finding`` | |
| is descriptive prose derived ONLY from the cited spans; ``condition_rating`` | |
| is a closed enum; ``evidence`` carries the literal spans; ``page_refs`` the | |
| source pages. | |
| """ | |
| section: str = Field(..., description="Domain/section, e.g. 'Roofing', 'Drainage'.") | |
| element: str = Field(..., description="Specific element, e.g. 'Main roof covering'.") | |
| condition_rating: ConditionRating = Field(default=ConditionRating.NA) | |
| finding: str = Field(..., min_length=1, description="Evidence-grounded description.") | |
| evidence: list[EvidenceSpan] = Field(default_factory=list) | |
| page_refs: list[int] = Field(default_factory=list) | |
| support: SupportLevel = Field(default=SupportLevel.NOT_FOUND) | |
| def _coerce_rating(cls, v: object) -> ConditionRating: | |
| return ConditionRating.coerce(v) | |
| class ContradictionKind(str, Enum): | |
| OPERATIONAL = "operational_vs_non_operational" | |
| CONDITION = "satisfactory_vs_defective" | |
| RATING_CONFLICT = "inconsistent_condition_rating" | |
| DUPLICATE = "duplicate_finding" | |
| MUTUALLY_EXCLUSIVE = "mutually_exclusive_statement" | |
| UNSUPPORTED_SUMMARY = "unsupported_summary" | |
| class Contradiction(BaseModel): | |
| """A detected logical inconsistency between two findings (or within one).""" | |
| kind: ContradictionKind | |
| element: str | |
| detail: str | |
| finding_indices: list[int] = Field(default_factory=list) | |
| resolution: str | None = Field( | |
| default=None, | |
| description="Which side was kept and why (evidence-supported version preserved).", | |
| ) | |
| class ClaimType(str, Enum): | |
| """Closed set of atomic claim categories (no free-text types).""" | |
| CONDITION_RATING = "condition_rating" | |
| MEASUREMENT = "measurement" | |
| MATERIAL = "material" | |
| ENTITY = "entity" # named product/model/species/person/org | |
| LOCATION = "location" | |
| DATE = "date" | |
| QUANTITY = "quantity" | |
| OBSERVATION = "observation" # plain factual observation | |
| OTHER = "other" | |
| class ClaimEvidence(BaseModel): | |
| """Exact evidence binding for one atomic claim.""" | |
| text: str = Field(..., min_length=1, description="Verbatim span from the cited chunk.") | |
| page: int = Field(default=0, description="1-based page number; 0 when unknown.") | |
| section: str = Field(default="", description="Section/heading label of the source span.") | |
| chunk_id: str = Field(..., min_length=1, description="ID of the cited source chunk.") | |
| class ClaimVerification(BaseModel): | |
| """Verification outcome for one atomic claim.""" | |
| supported: bool = False | |
| contradiction_detected: bool = False | |
| confidence: float = Field(default=0.0, ge=0.0, le=1.0) | |
| class AtomicClaim(BaseModel): | |
| """A single atomic factual claim with mandatory evidence binding. | |
| This is the forensic unit required by the evidence-comparison contract: | |
| one indivisible fact, the exact span that supports it, and the deterministic | |
| verification result. Unsupported claims are dropped before output. | |
| """ | |
| claim: str = Field(..., min_length=1) | |
| claim_type: ClaimType = Field(default=ClaimType.OTHER) | |
| evidence: ClaimEvidence | |
| verification: ClaimVerification = Field(default_factory=ClaimVerification) | |
| class SectionExtraction(BaseModel): | |
| """The full schema-constrained extraction for one report section.""" | |
| section: str | |
| findings: list[SurveyFinding] = Field(default_factory=list) | |
| contradictions: list[Contradiction] = Field(default_factory=list) | |
| dropped_claims: list[str] = Field( | |
| default_factory=list, | |
| description="Claims removed for lack of supporting evidence (audit trail).", | |
| ) | |
| def confidence(self) -> float: | |
| """Fraction of findings that are fully supported (0.0–1.0).""" | |
| if not self.findings: | |
| return 0.0 | |
| supported = sum(1 for f in self.findings if f.support == SupportLevel.SUPPORTED) | |
| return round(supported / len(self.findings), 3) | |