44 lines
1.6 KiB
Python
44 lines
1.6 KiB
Python
"""Typed Pydantic IR for a candidate cost-saving measure (B1).
|
|
|
|
Pure module — **no** ``agent_framework`` and no solver. The two structural invariants
|
|
(non-negative quantities; a claimed saving may not exceed the affected items' own total)
|
|
are enforced at construction by Pydantic, so a malformed proposal can never be built. This
|
|
IR is the D7-portable contract both the deterministic validator and the LLM->IR generator
|
|
speak.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pydantic import BaseModel, Field, model_validator
|
|
|
|
|
|
class AffectedItem(BaseModel):
|
|
"""One project cost line a proposal claims to save against."""
|
|
|
|
code: str
|
|
quantity: float = Field(ge=0) # quantities must be >= 0
|
|
unit_cost: float = Field(gt=0)
|
|
|
|
@property
|
|
def total(self) -> float:
|
|
return self.quantity * self.unit_cost
|
|
|
|
|
|
class SavingsProposal(BaseModel):
|
|
"""Typed IR for a candidate cost-saving measure (B1)."""
|
|
|
|
project_id: str
|
|
measure: str
|
|
affected_items: list[AffectedItem] = Field(min_length=1)
|
|
claimed_saving_nok: float = Field(gt=0)
|
|
# code -> (low_unit_cost, high_unit_cost) for the Monte Carlo step; empty = degenerate.
|
|
assumptions: dict[str, tuple[float, float]] = Field(default_factory=dict)
|
|
|
|
@model_validator(mode="after")
|
|
def _claim_within_affected_total(self) -> SavingsProposal:
|
|
total = sum(item.total for item in self.affected_items)
|
|
if self.claimed_saving_nok > total:
|
|
raise ValueError(
|
|
f"claimed saving {self.claimed_saving_nok} exceeds affected items' total {total}"
|
|
)
|
|
return self
|