portfolio-optimiser/src/portfolio_optimiser/ir.py

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