SPB Git

spb/wp26_uqo

Public
5.2 KB · 118 lines python
1# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca2"""Deterministic mock provider: no network, no keys. Produces schema-valid answers from the packet text.34The mock parses the evidence packet JSON embedded in the user prompt, prices the subject at the5median price-per-m2 of the comparables (or the roll ratio), and perturbs it with a role-and-property6seeded log-normal error so that different "agents" disagree. Critiques are generic but well-formed.7This is for pipeline tests only; it carries no information beyond the deterministic comparables rule.8"""9from __future__ import annotations1011import hashlib12import json13import math14import random15import re16from typing import Any1718from pydantic import BaseModel1920from .base import LLMProvider212223def _seed(*parts: str) -> int:24    return int(hashlib.sha256("|".join(parts).encode()).hexdigest()[:12], 16)252627class MockProvider(LLMProvider):28    name = "mock"2930    def __init__(self, noise_sd: float = 0.12, **kw):31        super().__init__(**kw)32        self.noise_sd = noise_sd3334    async def _raw_complete(self, *, system: str, user: str, schema: type[BaseModel], model: str, effort: str, max_tokens: int) -> tuple[str, dict[str, Any]]:35        packet = _find_packet(user)36        role = _find_role(system)37        name = schema.__name__38        out: dict[str, Any]39        if name == "AgentEstimate":40            out = self._estimate(packet, role, user)41        elif name == "CritiqueSet":42            out = self._critiques(packet, role, user)43        else:44            out = {}45        text = json.dumps(out)46        n_in = max(len(system) + len(user), 1) // 447        n_out = max(len(text), 1) // 448        return text, {"input_tokens": n_in, "output_tokens": n_out, "cached_tokens": 0, "request_id": "mock"}4950    def _estimate(self, packet: dict[str, Any], role: str, user: str) -> dict[str, Any]:51        subj = packet.get("subject", {})52        comps = packet.get("comparables", [])53        tx_id = subj.get("tx_id", "x")54        rng = random.Random(_seed(role, tx_id, "estimate"))55        area = subj.get("floor_area_m2") or None56        roll = subj.get("roll_total_value") or None57        base = None58        ids_used = ["P"]59        ppm2 = [c["price"] / c["floor_area_m2"] for c in comps if c.get("floor_area_m2") and c.get("price")]60        if area and ppm2:61            ppm2.sort()62            base = ppm2[len(ppm2) // 2] * area63            ids_used += [c["evidence_id"] for c in comps[:6]]64        elif roll:65            ratios = [c["assessment_ratio"] for c in comps if c.get("assessment_ratio")]66            ratio = sorted(ratios)[len(ratios) // 2] if ratios else (packet.get("market", {}).get("local_median_assessment_ratio_12m") or 1.2)67            base = roll * ratio68            ids_used += ["M1"]69        elif comps:70            base = sorted(c["price"] for c in comps)[len(comps) // 2]71            ids_used += [c["evidence_id"] for c in comps[:6]]72        else:73            base = 350_000.074        # revision prompts carry the committee's previous estimates: shrink toward their median75        m = re.search(r'"committee_median":\s*([0-9.]+)', user)76        if m:77            base = math.exp(0.5 * math.log(base) + 0.5 * math.log(float(m.group(1))))78        noise = rng.gauss(0, self.noise_sd)79        point = base * math.exp(noise)80        sd = 0.10 + 0.05 * rng.random()81        q = lambda z: point * math.exp(z * sd)  # noqa: E73182        return {83            "point_estimate": round(point, -2),84            "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),85            "evidence_ids_used": ids_used,86            "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]],87            "key_assumptions": [f"mock {role}: median price-per-m2 of comparables applied to subject area"],88            "missing_data": subj.get("missing_fields", []),89            "confidence_rationale": "mock: dispersion of comparables",90            "counterarguments": ["mock: comparables may differ in condition (unobserved)"],91            "reasoning_summary": f"Mock {role} estimate from {len(comps)} comparables.",92        }9394    def _critiques(self, packet: dict[str, Any], role: str, user: str) -> dict[str, Any]:95        targets = re.findall(r'"role":\s*"([a-z_0-9]+)"', user)96        rng = random.Random(_seed(role, packet.get("subject", {}).get("tx_id", "x"), "critique"))97        crits = []98        for t in dict.fromkeys(targets):99            if t == role:100                continue101            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})102        return {"critiques": crits[:11]}103104105def _find_packet(user: str) -> dict[str, Any]:106    m = re.search(r"<evidence_packet>\s*(\{.*?\})\s*</evidence_packet>", user, re.S)107    if not m:108        return {}109    try:110        return json.loads(m.group(1))111    except json.JSONDecodeError:112        return {}113114115def _find_role(system: str) -> str:116    m = re.search(r"ROLE_ID:\s*([a-z_0-9]+)", system)117    return m.group(1) if m else "generalist"118
Files · b780d9effbc0d688f848faf14e38219f6088143b
Menu

Theme