"""SCRIPTED model-client stand-in for the offline test suite. Honesty rule (method-spec §1): this is a scripted stand-in, NOT a model. It replays canned replies (or computes them via a prompt-sensitive script function) deterministically, records every call, and never touches a network or an API key. The ONE genuine model run in the programme is S10. """ from __future__ import annotations from typing import Callable from portfolio_optimiser_claude.loop import ModelReply ScriptFn = Callable[[str, str], ModelReply] class ScriptedClient: """Deterministic ``ModelClient`` stand-in — canned replies, recorded calls. Either ``replies`` (a FIFO of ``ModelReply``) or ``script`` (a function of ``(role, prompt)`` — prompt-sensitive, for load-bearing flip proofs) must be given. Every call is recorded as ``(role, prompt)`` in ``calls``. """ def __init__( self, replies: list[ModelReply] | None = None, script: ScriptFn | None = None, ) -> None: if (replies is None) == (script is None): raise ValueError("give exactly one of 'replies' or 'script'") self._replies = list(replies) if replies is not None else None self._script = script self.calls: list[tuple[str, str]] = [] def complete(self, prompt: str, *, role: str) -> ModelReply: self.calls.append((role, prompt)) if self._script is not None: return self._script(role, prompt) assert self._replies is not None if not self._replies: raise AssertionError("scripted client exhausted: no reply left for this call") return self._replies.pop(0) def prompts(self, role: str) -> list[str]: """The recorded prompts sent to ``role``, in call order.""" return [prompt for r, prompt in self.calls if r == role] def reply(text: str, usage_tokens: int | None = 10) -> ModelReply: """Shorthand for a scripted ``ModelReply`` with a small default usage.""" return ModelReply(text=text, usage_tokens=usage_tokens)