# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca """Deterministic mock provider: no network, no keys. Produces schema-valid answers from the packet text. The mock parses the evidence packet JSON embedded in the user prompt, prices the subject at the median price-per-m2 of the comparables (or the roll ratio), and perturbs it with a role-and-property seeded log-normal error so that different "agents" disagree. Critiques are generic but well-formed. This is for pipeline tests only; it carries no information beyond the deterministic comparables rule. """ from __future__ import annotations import hashlib import json import math import random import re from typing import Any from pydantic import BaseModel from .base import LLMProvider def _seed(*parts: str) -> int: return int(hashlib.sha256("|".join(parts).encode()).hexdigest()[:12], 16) class MockProvider(LLMProvider): name = "mock" def __init__(self, noise_sd: float = 0.12, **kw): super().__init__(**kw) self.noise_sd = noise_sd async def _raw_complete(self, *, system: str, user: str, schema: type[BaseModel], model: str, effort: str, max_tokens: int) -> tuple[str, dict[str, Any]]: packet = _find_packet(user) role = _find_role(system) name = schema.__name__ out: dict[str, Any] if name == "AgentEstimate": out = self._estimate(packet, role, user) elif name == "CritiqueSet": out = self._critiques(packet, role, user) else: out = {} text = json.dumps(out) n_in = max(len(system) + len(user), 1) // 4 n_out = max(len(text), 1) // 4 return text, {"input_tokens": n_in, "output_tokens": n_out, "cached_tokens": 0, "request_id": "mock"} def _estimate(self, packet: dict[str, Any], role: str, user: str) -> dict[str, Any]: subj = packet.get("subject", {}) comps = packet.get("comparables", []) tx_id = subj.get("tx_id", "x") rng = random.Random(_seed(role, tx_id, "estimate")) area = subj.get("floor_area_m2") or None roll = subj.get("roll_total_value") or None base = None ids_used = ["P"] ppm2 = [c["price"] / c["floor_area_m2"] for c in comps if c.get("floor_area_m2") and c.get("price")] if area and ppm2: ppm2.sort() base = ppm2[len(ppm2) // 2] * area ids_used += [c["evidence_id"] for c in comps[:6]] elif roll: ratios = [c["assessment_ratio"] for c in comps if c.get("assessment_ratio")] ratio = sorted(ratios)[len(ratios) // 2] if ratios else (packet.get("market", {}).get("local_median_assessment_ratio_12m") or 1.2) base = roll * ratio ids_used += ["M1"] elif comps: base = sorted(c["price"] for c in comps)[len(comps) // 2] ids_used += [c["evidence_id"] for c in comps[:6]] else: base = 350_000.0 # revision prompts carry the committee's previous estimates: shrink toward their median m = re.search(r'"committee_median":\s*([0-9.]+)', user) if m: base = math.exp(0.5 * math.log(base) + 0.5 * math.log(float(m.group(1)))) noise = rng.gauss(0, self.noise_sd) point = base * math.exp(noise) sd = 0.10 + 0.05 * rng.random() q = lambda z: point * math.exp(z * sd) # noqa: E731 return { "point_estimate": round(point, -2), "q05": round(q(-1.645), -2), "q25": round(q(-0.674), -2), "q50": round(point, -2), "q75": round(q(0.674), -2), "q95": round(q(1.645), -2), "evidence_ids_used": ids_used, "adjustments": [{"comparable_id": c["evidence_id"], "adjusted_price": round(c["price"] * (1 + rng.uniform(-0.05, 0.05)), -2), "reason": "mock time/size adjustment"} for c in comps[:3]], "key_assumptions": [f"mock {role}: median price-per-m2 of comparables applied to subject area"], "missing_data": subj.get("missing_fields", []), "confidence_rationale": "mock: dispersion of comparables", "counterarguments": ["mock: comparables may differ in condition (unobserved)"], "reasoning_summary": f"Mock {role} estimate from {len(comps)} comparables.", } def _critiques(self, packet: dict[str, Any], role: str, user: str) -> dict[str, Any]: targets = re.findall(r'"role":\s*"([a-z_0-9]+)"', user) rng = random.Random(_seed(role, packet.get("subject", {}).get("tx_id", "x"), "critique")) crits = [] for t in dict.fromkeys(targets): if t == role: continue crits.append({"target_role": t, "agree": rng.random() < 0.6, "points": ["mock: check the time adjustment of C01"], "evidence_ids_cited": ["C01"], "suggested_value": None}) return {"critiques": crits[:11]} def _find_packet(user: str) -> dict[str, Any]: m = re.search(r"\s*(\{.*?\})\s*", user, re.S) if not m: return {} try: return json.loads(m.group(1)) except json.JSONDecodeError: return {} def _find_role(system: str) -> str: m = re.search(r"ROLE_ID:\s*([a-z_0-9]+)", system) return m.group(1) if m else "generalist"