diff --git a/spikes/_harness.py b/spikes/_harness.py index 8a32b98..3a63295 100644 --- a/spikes/_harness.py +++ b/spikes/_harness.py @@ -137,8 +137,11 @@ class FakeChatClient(BaseChatClient): reply = self._next_reply() if stream: + async def _agen() -> Any: - yield ChatResponseUpdate(role="assistant", contents=[{"type": "text", "text": reply}]) + yield ChatResponseUpdate( + role="assistant", contents=[{"type": "text", "text": reply}] + ) return self._build_response_stream(_agen()) diff --git a/spikes/a_groupchat.py b/spikes/a_groupchat.py index 88ece12..cee0bb2 100644 --- a/spikes/a_groupchat.py +++ b/spikes/a_groupchat.py @@ -109,7 +109,9 @@ def _caught_flaw(texts: Sequence[str]) -> bool: return any(marker in blob for marker in _FLAW_MARKERS) -async def run_live(*, n_rounds: int = DEFAULT_ROUNDS, write_findings: bool = True) -> dict[str, object]: +async def run_live( + *, n_rounds: int = DEFAULT_ROUNDS, write_findings: bool = True +) -> dict[str, object]: """Gated empirical arm: run maker-checker vs single-agent against a reference project carrying a planted flaw, measure, and write ``findings-a.md``. @@ -127,8 +129,12 @@ async def run_live(*, n_rounds: int = DEFAULT_ROUNDS, write_findings: bool = Tru # --- maker-checker arm (3 roles) --- proposer = Agent(client, "You propose one concrete cost-saving measure.", name="proposer") - critic = Agent(client, "You critique the proposal and flag any constraint violation.", name="critic") - validator = Agent(client, "You give the final valid/invalid decision with a reason.", name="validator") + critic = Agent( + client, "You critique the proposal and flag any constraint violation.", name="critic" + ) + validator = Agent( + client, "You give the final valid/invalid decision with a reason.", name="validator" + ) names = ["proposer", "critic", "validator"] counter = {"n": 0} diff --git a/spikes/b_footguns.py b/spikes/b_footguns.py index 40b0fac..1ada2a8 100644 --- a/spikes/b_footguns.py +++ b/spikes/b_footguns.py @@ -53,7 +53,9 @@ _NEVER_SATISFIED_LEDGER = json.dumps( def _magentic(max_round_count: int | None) -> object: """Build a Magentic workflow whose manager never self-finalizes.""" manager = StandardMagenticManager( - agent=Agent(FakeChatClient(default_reply=_NEVER_SATISFIED_LEDGER), "manager", name="manager"), + agent=Agent( + FakeChatClient(default_reply=_NEVER_SATISFIED_LEDGER), "manager", name="manager" + ), max_round_count=max_round_count, ) worker = Agent(FakeChatClient(default_reply="worker did some work"), "worker", name="worker") @@ -95,9 +97,7 @@ def fresh_workflow() -> tuple[object, tuple[FakeChatClient, FakeChatClient]]: every call, so no state survives between runs.""" c1 = FakeChatClient(default_reply="participant one view") c2 = FakeChatClient(default_reply="participant two view") - workflow = ConcurrentBuilder( - participants=[fake_agent(c1, "w1"), fake_agent(c2, "w2")] - ).build() + workflow = ConcurrentBuilder(participants=[fake_agent(c1, "w1"), fake_agent(c2, "w2")]).build() return workflow, (c1, c2) diff --git a/spikes/c_validator.py b/spikes/c_validator.py index 8348773..de87774 100644 --- a/spikes/c_validator.py +++ b/spikes/c_validator.py @@ -120,11 +120,15 @@ def _solve_max_feasible(items: list[AffectedItem], fraction: float) -> float: prob += pulp.lpSum(xs) <= fraction * sum(it.total for it in items) status = prob.solve(solver) if pulp.LpStatus[status] != "Optimal": - raise CbcUnavailable(f"CBC did not reach an optimal solution (status={pulp.LpStatus[status]})") + raise CbcUnavailable( + f"CBC did not reach an optimal solution (status={pulp.LpStatus[status]})" + ) return float(pulp.value(prob.objective)) -def _monte_carlo(proposal: SavingsProposal, *, fraction: float = MAX_SAVING_FRACTION) -> tuple[float, float, float]: +def _monte_carlo( + proposal: SavingsProposal, *, fraction: float = MAX_SAVING_FRACTION +) -> tuple[float, float, float]: """Vary uncertain unit-costs (seeded) and return (P10, P50, P90) of the feasible saving. Uses the LP's closed-form optimum (= fraction x sum of sampled totals), which is exact here, so we do NOT spawn a CBC subprocess per sample (D6).""" diff --git a/spikes/d_verdictstore.py b/spikes/d_verdictstore.py index 5de339b..f36ccea 100644 --- a/spikes/d_verdictstore.py +++ b/spikes/d_verdictstore.py @@ -63,9 +63,12 @@ def similarity(query: ProposalFeatures, candidate: ProposalFeatures) -> float: """Weighted structural similarity in [0, 1] — text is ignored by design.""" codes = _jaccard(query.affected_codes, candidate.affected_codes) measure = 1.0 if query.measure_type == candidate.measure_type else 0.0 - magnitude = 1.0 if _magnitude_bucket(query.claimed_saving_nok) == _magnitude_bucket( - candidate.claimed_saving_nok - ) else 0.0 + magnitude = ( + 1.0 + if _magnitude_bucket(query.claimed_saving_nok) + == _magnitude_bucket(candidate.claimed_saving_nok) + else 0.0 + ) return _W_CODES * codes + _W_MEASURE * measure + _W_MAGNITUDE * magnitude @@ -106,7 +109,9 @@ class ExpeLContextProvider(ContextProvider): body = "\n".join(f"- [{v.id}] {v.decision}: {v.rationale}" for v in hits) return f"Relevant prior verdicts (ExpeL few-shot):\n{body}" - async def before_run(self, *, agent: object, session: object, context: object, state: dict) -> None: + async def before_run( + self, *, agent: object, session: object, context: object, state: dict + ) -> None: context.extend_instructions([self.format_fewshot()]) # type: ignore[attr-defined] @@ -114,18 +119,95 @@ def seed_store() -> VerdictStore: """Seed 12 synthetic verdicts spanning the reference domain's cost codes, measure types, magnitudes, and decisions (B2 store of 10–20).""" rows = [ - ("V01", {"05.2", "03.1"}, "scope_reduction", 180_000, "approved", "asphalt + base course trimmed within feasible range"), - ("V02", {"05.2"}, "rate_renegotiation", 60_000, "approved", "renegotiated asphalt unit rate"), - ("V03", {"07.4"}, "material_substitution", 120_000, "rejected", "granite kerb substitution unsafe"), - ("V04", {"09.1"}, "scope_reduction", 240_000, "approved", "fewer LED masts on low-traffic stretch"), - ("V05", {"02.3", "03.1"}, "scope_reduction", 350_000, "rejected", "soil replacement is load-bearing, cannot cut"), + ( + "V01", + {"05.2", "03.1"}, + "scope_reduction", + 180_000, + "approved", + "asphalt + base course trimmed within feasible range", + ), + ( + "V02", + {"05.2"}, + "rate_renegotiation", + 60_000, + "approved", + "renegotiated asphalt unit rate", + ), + ( + "V03", + {"07.4"}, + "material_substitution", + 120_000, + "rejected", + "granite kerb substitution unsafe", + ), + ( + "V04", + {"09.1"}, + "scope_reduction", + 240_000, + "approved", + "fewer LED masts on low-traffic stretch", + ), + ( + "V05", + {"02.3", "03.1"}, + "scope_reduction", + 350_000, + "rejected", + "soil replacement is load-bearing, cannot cut", + ), ("V06", {"21.2"}, "rate_renegotiation", 90_000, "approved", "blasting rate renegotiated"), - ("V07", {"22.4"}, "material_substitution", 700_000, "rejected", "fiber shotcrete spec is mandated"), - ("V08", {"88.2"}, "scope_reduction", 150_000, "approved", "concrete repair area re-measured smaller"), - ("V09", {"87.3"}, "material_substitution", 130_000, "approved", "alternative membrane qualified"), - ("V10", {"05.2", "03.1"}, "rate_renegotiation", 200_000, "approved", "combined paving rate discount"), - ("V11", {"01.1"}, "scope_reduction", 95_000, "rejected", "rigging is fixed cost, no scope to cut"), - ("V12", {"31.3"}, "scope_reduction", 110_000, "approved", "drainage length reduced after survey"), + ( + "V07", + {"22.4"}, + "material_substitution", + 700_000, + "rejected", + "fiber shotcrete spec is mandated", + ), + ( + "V08", + {"88.2"}, + "scope_reduction", + 150_000, + "approved", + "concrete repair area re-measured smaller", + ), + ( + "V09", + {"87.3"}, + "material_substitution", + 130_000, + "approved", + "alternative membrane qualified", + ), + ( + "V10", + {"05.2", "03.1"}, + "rate_renegotiation", + 200_000, + "approved", + "combined paving rate discount", + ), + ( + "V11", + {"01.1"}, + "scope_reduction", + 95_000, + "rejected", + "rigging is fixed cost, no scope to cut", + ), + ( + "V12", + {"31.3"}, + "scope_reduction", + 110_000, + "approved", + "drainage length reduced after survey", + ), ] return VerdictStore( verdicts=[ diff --git a/tests/spikes/test_d_verdictstore.py b/tests/spikes/test_d_verdictstore.py index 2f42cc8..4928f39 100644 --- a/tests/spikes/test_d_verdictstore.py +++ b/tests/spikes/test_d_verdictstore.py @@ -73,7 +73,9 @@ def test_retrieve_finds_structural_match_over_text_decoys() -> None: def test_retrieve_is_deterministic() -> None: store, _ = _store_with_true_match_and_decoys() - assert [h.id for h in store.retrieve(_QUERY, k=3)] == [h.id for h in store.retrieve(_QUERY, k=3)] + assert [h.id for h in store.retrieve(_QUERY, k=3)] == [ + h.id for h in store.retrieve(_QUERY, k=3) + ] def test_retrieve_rejects_non_positive_k() -> None: