refactor(fase2a): konsolider fire skriptede klienter til én kanonisk (S2.5)
This commit is contained in:
parent
7978bd5f49
commit
620d5cfb83
3 changed files with 136 additions and 108 deletions
|
|
@ -67,21 +67,48 @@ _CHECKER_APPROVE = "Tallene er innenfor feasibelt område og resonnementet holde
|
|||
|
||||
|
||||
class ScriptedChatClient(OpenAIChatCompletionClient):
|
||||
"""A network-free, SCRIPTED chat client: it returns a fixed ``reply`` and records every prompt
|
||||
it receives into a shared ``sink`` (the observation probe). It is a stand-in for a real model —
|
||||
it proves the loop's plumbing, NOT model behaviour.
|
||||
"""The ONE canonical network-free scripted chat client (S2.5 consolidation): a single
|
||||
``_inner_get_response`` body shared by the simulation's fixed-reply client AND conftest's three
|
||||
test doubles (which subclass it). Parametrized by a ``reply_selector`` over
|
||||
``(prompt_blob, role)`` plus an optional ``sink`` recording every prompt — the four
|
||||
previously-divergent ``_inner_get_response`` bodies collapse to this one.
|
||||
|
||||
Subclasses the LAYERED ``OpenAIChatCompletionClient`` (not the minimal ``BaseChatClient``) so the
|
||||
always-attached ``BudgetMiddleware`` is not silently no-op'd (verified). Construction is offline
|
||||
(loopback ``base_url`` + dummy key); ``_inner_get_response`` intercepts before any HTTP."""
|
||||
(loopback ``base_url`` + dummy key); ``_inner_get_response`` intercepts before any HTTP.
|
||||
|
||||
Back-compat constructors are preserved (divergent PUBLIC surfaces the external test call-sites
|
||||
depend on): ``ScriptedChatClient(reply, sink)`` (POSITIONAL — used by ``scripted_factory``) is
|
||||
sugar for a constant selector; the ``call_count`` attribute + ``model``/OTEL ``"synthetic"`` are
|
||||
always present; subclasses pass ``reply_selector=`` / ``default_reply=`` for scripted-list,
|
||||
prompt-scan, or record-only behaviour."""
|
||||
|
||||
OTEL_PROVIDER_NAME = "synthetic"
|
||||
|
||||
def __init__(self, reply: str, sink: list[str], *, tokens_per_reply: int = 8) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
reply: str | None = None,
|
||||
sink: list[str] | None = None,
|
||||
*,
|
||||
reply_selector: Callable[[str, str], str] | None = None,
|
||||
role: str = "",
|
||||
default_reply: str = "ok",
|
||||
tokens_per_reply: int = 8,
|
||||
) -> None:
|
||||
super().__init__(model="synthetic", api_key="synthetic", base_url="http://127.0.0.1:9/v1")
|
||||
self._reply = reply
|
||||
self._sink = sink
|
||||
self._role = role
|
||||
self._default = default_reply
|
||||
# The reply-selector over (prompt_blob, role). A positional ``reply`` is sugar for a constant
|
||||
# selector (scripted_factory back-compat); with neither, the constant is ``default_reply``.
|
||||
if reply_selector is not None:
|
||||
self._select: Callable[[str, str], str] = reply_selector
|
||||
elif reply is not None:
|
||||
self._select = lambda _prompt, _role: reply
|
||||
else:
|
||||
self._select = lambda _prompt, _role: self._default
|
||||
self._tokens = tokens_per_reply
|
||||
self.call_count = 0
|
||||
|
||||
def _inner_get_response(
|
||||
self,
|
||||
|
|
@ -91,7 +118,11 @@ class ScriptedChatClient(OpenAIChatCompletionClient):
|
|||
stream: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]:
|
||||
self._sink.append(" ".join(getattr(m, "text", "") or "" for m in messages))
|
||||
blob = " ".join(getattr(m, "text", "") or "" for m in messages)
|
||||
if self._sink is not None:
|
||||
self._sink.append(blob)
|
||||
self.call_count += 1
|
||||
reply = self._select(blob, self._role)
|
||||
usage = UsageDetails(total_token_count=self._tokens)
|
||||
if stream:
|
||||
|
||||
|
|
@ -99,14 +130,14 @@ class ScriptedChatClient(OpenAIChatCompletionClient):
|
|||
# The framework accepts a {"type": "text", ...} dict here (its types under-specify it).
|
||||
yield ChatResponseUpdate(
|
||||
role="assistant",
|
||||
contents=[{"type": "text", "text": self._reply}], # type: ignore[list-item]
|
||||
contents=[{"type": "text", "text": reply}], # type: ignore[list-item]
|
||||
)
|
||||
|
||||
return self._build_response_stream(_agen())
|
||||
|
||||
async def _coro() -> ChatResponse:
|
||||
return ChatResponse(
|
||||
messages=[Message(role="assistant", contents=[self._reply])],
|
||||
messages=[Message(role="assistant", contents=[reply])],
|
||||
response_id="synthetic",
|
||||
usage_details=usage,
|
||||
)
|
||||
|
|
@ -121,7 +152,7 @@ def scripted_factory(replies: dict[str, str], sink: list[str]) -> Callable[[str]
|
|||
per-turn counter); the shared ``sink`` spans the debate turns and the generation call."""
|
||||
|
||||
def factory(role: str) -> BaseChatClient:
|
||||
return ScriptedChatClient(replies[role], sink)
|
||||
return ScriptedChatClient(replies[role], sink, role=role)
|
||||
|
||||
return factory
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue