oc_chat
Loading...
Searching...
No Matches
presenter.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/benchmarks/presenter.py
4
5"""
6BenchmarkPresenter — the measurement tap over the presenter contract.
7
8Purpose
9-------
10A standalone :class:`~oc_ui.presenter_base.Presenter` (not a delegating
11wrapper) that records, per turn, exactly what the measurement surface
12exposes — and nothing else. It is the single lowest-blast-radius way to
13observe a turn with zero production changes (plan 136 ADR-136-1):
14
15- **TTFT** = monotonic at ``show_waiting`` (fired immediately before
16 ``client.stream_chat``) → the first ``show_reasoning`` / ``show_content``
17 chunk of that iteration. Iterations that emit only tool calls have no
18 first-chunk token, so their TTFT is ``None`` — never fabricated.
19- **Raw usage** — ``show_usage`` receives the raw provider dict (the
20 only place ``reasoning_tokens`` and the DeepSeek cache split survive).
21- **Main-loop tool names** — ``show_tool_call`` fires for main-loop
22 calls only; sub-loop internal tools never reach the presenter, so the
23 count is derived and the *names* are marked unavailable (ADR-136-5).
24- **Turn summary** — ``iterations`` and ``tool_calls`` drive the
25 sub-loop-count derivation (``tool_calls - main_loop_names``).
26
27Every other presenter hook is a no-op (the benchmark has no UI, and
28the real startup composition path calls ``show_info`` etc.).
29
30Responsibilities
31----------------
32- :class:`BenchmarkPresenter` — implement the full ``Presenter`` ABC;
33 capture per-turn buffers; :meth:`reset_turn`; :meth:`build_turn`.
34
35Diagnostics
36-----------
37Domain: OC_CHAT.MAIN
38Levels:
39 L2 — lifecycle events
40 L3 — semantic details
41 L4 — deep tracing
42
43Contracts
44---------
45- A *fresh* presenter (or one used only for a discarded warmup) leaks
46 nothing into a measured run; the runner uses a separate throwaway
47 presenter for warmup (plan §3.2 step 6).
48- :meth:`build_turn` is pure given the captured buffers; it does not
49 mutate them.
50
51Dependencies
52------------
53- oc_diagnostics (structured debug logging via _dbg)
54- oc_chat.benchmarks.metrics (CallMetrics, TurnMetrics, extract_call_metrics)
55- oc_ui.presenter_base (Presenter ABC)
56- oc_chat.core.session (lazy; ChatSession - type hint for the no-op show_welcome)
57"""
58from __future__ import annotations
59
60import time
61from contextlib import contextmanager
62from typing import TYPE_CHECKING, Iterator
63
64from oc_diagnostics import _dbg
65
66from oc_chat.benchmarks.metrics import TurnMetrics, extract_call_metrics
67from oc_ui.presenter_base import Presenter
68
69if TYPE_CHECKING:
70 from oc_chat.core.session import ChatSession
71 from oc_ui.presenter_base import IterationStat
72
73_dbg("OC_CHAT.MAIN", "module", "presenter loaded", 4)
74
75
77 """Standalone presenter that records per-turn measurements only."""
78
79 def __init__(self, clock=time.monotonic) -> None:
80 self._clock = clock
81 self.reset_turn()
82
83 # ── Per-turn capture state ───────────────────────────────────────────────
84
85 def reset_turn(self) -> None:
86 """Clear all per-turn buffers. Called between measured turns."""
87 self._t_wait: float | None = None
88 self._await_first_chunk: bool = False
89 self.ttft_ms: float | None = None # first iteration's TTFT
90 self.iteration_ttfts: list[float] = []
91 self.usages: list[dict] = []
92 self.tool_names: list[str] = []
93 # From show_turn_summary:
94 self.summary_iterations: int = 0
95 self.summary_tool_calls: int = 0
96 self.summary_model: str | None = None
97
98 # ── Measurement hooks ────────────────────────────────────────────────────
99
100 def show_waiting(self) -> None:
101 # Fired immediately before client.stream_chat (react_loop.py:565):
102 # the TTFT anchor for this iteration.
103 self._t_wait = self._clock()
104 self._await_first_chunk = True
105
106 def _first_chunk(self) -> None:
107 if self._await_first_chunk and self._t_wait is not None:
108 ttft = (self._clock() - self._t_wait) * 1000.0
109 self.iteration_ttfts.append(ttft)
110 if self.ttft_ms is None: # first iteration only
111 self.ttft_ms = ttft
112 self._await_first_chunk = False
113
114 def show_reasoning(self, text: str) -> None:
115 self._first_chunk()
116
117 def show_content(self, text: str) -> None:
118 self._first_chunk()
119
120 def show_tool_call(self, name: str, arguments: str) -> None:
121 # Main-loop tool call only — sub-loop tools never reach the presenter.
122 self.tool_names.append(name)
123
124 def show_usage(self, usage: dict) -> None:
125 # The RAW provider usage dict (react_loop.py:651).
126 if isinstance(usage, dict):
127 self.usages.append(dict(usage))
128
130 self,
131 prompt: int,
132 completion: int,
133 total: int,
134 iterations: int,
135 tool_calls: int,
136 prompt_cached: int | None = None,
137 *,
138 cache_hit_pct: float | None = None,
139 cache_saved_tokens: int | None = None, # ignored for v1
140 performance_stats: object = None, # ignored for v1
141 model: str | None = None,
142 context_used_pct: float | None = None,
143 duration_ms: int | None = None, # ignored for v1
144 turn_cost: object = None, # plan 188 T4 — the suite
145 # prices its own runs from oc_chat.benchmarks.prices with the full
146 # reasoning split, so the render-lane estimate is not captured here.
147 ) -> None:
148 self.summary_iterations = iterations
149 self.summary_tool_calls = tool_calls
150 self.summary_model = model
151
152 # ── Build a TurnMetrics from the captured buffers ────────────────────────
153
154 def build_turn(self, index: int, cold_label: str) -> TurnMetrics:
155 """Materialise the captured buffers into a :class:`TurnMetrics`.
156
157 Sub-loop tool *names* are not observable via the presenter, so
158 ``sub_loop_count`` is derived as ``summary.tool_calls −
159 len(main_loop_tool_names)`` (acc.tool_calls counts main-loop
160 dispatches + sub-loop offloads — react_loop.py:545/721) and the
161 names are marked unavailable.
162 """
163 calls = [extract_call_metrics(u) for u in self.usages]
164 sub_loop_count = max(0, self.summary_tool_calls - len(self.tool_names))
165 return TurnMetrics(
166 index=index,
167 cold_label=cold_label,
168 calls=calls,
169 ttft_ms=self.ttft_ms,
170 iterations=self.summary_iterations,
171 main_loop_tool_names=list(self.tool_names),
172 sub_loop_count=sub_loop_count,
173 sub_loop_tool_names_available=False,
174 )
175
176 # ── No-op UI hooks (the benchmark has no UI) ─────────────────────────────
177
178 @contextmanager
179 def turn(self) -> Iterator[None]:
180 yield
181
182 def start_turn(self) -> None: ...
183 def end_turn(self) -> None: ...
184 def show_tool_progress(self, name: str) -> None: ...
185 def show_tool_result(self, name: str, result: object) -> None: ...
186 def show_iteration_stat(self, stat: "IterationStat") -> None: ...
187 def show_welcome(self, session: "ChatSession") -> None: ...
188 def show_available_models(self, models: dict) -> None: ...
189 def show_session_list(self, sessions: list, current_name: str | None = None) -> None: ...
190 def show_help(self, command_help: dict, tools: list[str] | None = None) -> None: ...
191 def show_compressed(self) -> None: ...
192 def show_error(self, msg: str) -> None: ...
193 def show_info(self, msg: str) -> None: ...
194
195
196__all__ = ["BenchmarkPresenter"]
TurnMetrics build_turn(self, int index, str cold_label)
Definition presenter.py:154
None __init__(self, clock=time.monotonic)
Definition presenter.py:79
None show_turn_summary(self, int prompt, int completion, int total, int iterations, int tool_calls, int|None prompt_cached=None, *, float|None cache_hit_pct=None, int|None cache_saved_tokens=None, object performance_stats=None, str|None model=None, float|None context_used_pct=None, int|None duration_ms=None, object turn_cost=None)
Definition presenter.py:147
None show_tool_call(self, str name, str arguments)
Definition presenter.py:120
None show_welcome(self, "ChatSession" session)
Definition presenter.py:187
None show_tool_result(self, str name, object result)
Definition presenter.py:185
None show_help(self, dict command_help, list[str]|None tools=None)
Definition presenter.py:190
None show_session_list(self, list sessions, str|None current_name=None)
Definition presenter.py:189
None show_iteration_stat(self, "IterationStat" stat)
Definition presenter.py:186