Source code for app.alba_core.models

"""Shared data models for Alba Core.

These dataclasses describe the clean internal shapes used by extraction,
matching, and lead summaries. They deliberately exclude raw PropertyMe owner,
tenant, and token details.
"""

from __future__ import annotations

from dataclasses import asdict, dataclass, field
from datetime import date
from typing import Any


[docs] def clean_text(value: Any) -> str | None: """Return compact display text or None. Empty strings create subtle bugs in matching, so Alba Core normalises them to None as early as possible. """ if value is None: return None text = str(value).strip() return text or None
[docs] @dataclass(slots=True) class RentalRequirements: """Structured version of a renter's request. The extractor fills this from plain English. Fields ending in _min or _max become hard matching rules when present. features and priority are softer ranking hints. """ city: str | None = None suburb_preferences: list[str] = field(default_factory=list) budget_max: float | None = None budget_min: float | None = None bedrooms_min: int | None = None bathrooms_min: int | None = None parking_min: int | None = None property_type: str | None = None move_in_timing: str | None = None priority: str | None = None pets_required: bool | None = None furnished_required: bool | None = None features: list[str] = field(default_factory=list) no_preference: list[str] = field(default_factory=list)
[docs] def merge(self, patch: "RentalRequirements") -> "RentalRequirements": """Merge newly extracted values over an existing conversation state.""" data = asdict(self) for key, value in asdict(patch).items(): if isinstance(value, list): if value: existing = data.get(key) or [] merged = [*existing] for item in value: if item not in merged: merged.append(item) data[key] = merged elif value is not None: data[key] = value return RentalRequirements(**data)
@property def missing_required_fields(self) -> list[str]: """Fields needed before Alba Core can call the search complete.""" missing: list[str] = [] if not self.city and not self.suburb_preferences: missing.append("location") if self.budget_max is None: missing.append("budget") if self.bedrooms_min is None: missing.append("bedrooms") return missing @property def is_search_ready(self) -> bool: """True when the minimum useful matching inputs are known.""" return not self.missing_required_fields
[docs] def to_dict(self) -> dict[str, Any]: """Return a JSON-friendly representation for API responses.""" return asdict(self)
[docs] @dataclass(slots=True) class PropertyRecord: """Privacy-shaped property record used by the matcher. This model contains only fields Alba Core needs. Raw owner, tenant, and token details should never be mapped into this object. """ property_id: str address: str suburb: str | None city: str | None region: str | None postcode: str | None property_type: str | None rent_pw: float | None bedrooms: int | None bathrooms: int | None parking_spaces: int | None availability: str | None available_from: date | None furnishing: str | None pets: str | None smoker: str | None pool: str | None spa: str | None view_type: str | None description: str | None listing_url: str | None = None
[docs] def to_public_dict(self) -> dict[str, Any]: """Serialize for API output, converting date objects to ISO strings.""" data = asdict(self) if self.available_from is not None: data["available_from"] = self.available_from.isoformat() return data
[docs] @dataclass(slots=True) class MatchResult: """A matched property plus score and explanation notes.""" property: PropertyRecord score: float hard_filter_reasons: list[str] = field(default_factory=list) match_notes: list[str] = field(default_factory=list) score_breakdown: dict[str, float] = field(default_factory=dict)
[docs] def to_dict(self) -> dict[str, Any]: """Return the public response shape used by API and smoke tests.""" return { "property": self.property.to_public_dict(), "score": round(self.score, 2), "match_notes": self.match_notes, "score_breakdown": {key: round(value, 2) for key, value in self.score_breakdown.items()}, }