oc_chat
Loading...
Searching...
No Matches
iteration_stat.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/core/iteration_stat.py
4
5"""
6One-iteration analytics record from the ReAct loop.
7
8Purpose
9-------
10:class:`IterationStat` is a frozen dataclass capturing every metric
11produced during a single ReAct iteration: timestamp, model, effort,
12tools called, token counts, cache hits/misses, and wall-clock
13duration. Instances are created by the ReAct loop and emitted to
14the presenter for both human-readable display and JSON-lines
15export to a ``--stats-file`` sink.
16
17Responsibilities
18----------------
19- :class:`IterationStat` — frozen, slot-based, serialisable record.
20- :func:`iteration_stat_to_json` — lossless serialisation for the
21 JSON-lines sink.
22- :func:`iteration_stat_human_line` — compact one-line format for
23 the terminal (used by ConsolePresenter and RichPresenter).
24
25Diagnostics
26-----------
27Domain: OC_CHAT.CLIENT
28Levels:
29 L2 — lifecycle events
30 L3 — semantic details
31 L4 — deep tracing
32
33Contracts
34---------
35- All fields are primitive types (str, int, tuple) — no nested
36 references or lambdas, so the dataclass is always serialisable.
37- ``ts`` is ISO-8601 with millisecond precision, UTC.
38- ``tools`` is a tuple (hashable); empty tuple means no tools were
39 called this iteration (final answer).
40- ``dur_ms`` is wall-clock time from start to finish of the iteration,
41 including API streaming and tool dispatch.
42
43Dependencies
44------------
45- oc_diagnostics (structured debug logging via _dbg)
46"""
47
48from __future__ import annotations
49
50import json
51from dataclasses import dataclass
52from datetime import datetime, timezone
53
54from oc_diagnostics import _dbg
55
56
57@dataclass(slots=True, frozen=True)
59 """One ReAct-iteration analytics record."""
60
61 ts: str
62 """Timestamp string."""
63
64 iter: int
65 """Zero-based iteration index within the current turn."""
66
67 model: str
68 """Model name used for this iteration (after routing)."""
69
70 effort: str
71 """Reasoning effort (``"low"`` / ``"medium"`` / ``"high"`` / ``""``)."""
72
73 tools: tuple[str, ...]
74 """Tool names called this iteration. Empty tuple = final answer."""
75
76 prompt_tokens: int
77 """Prompt tokens consumed this iteration."""
78
79 completion_tokens: int
80 """Completion tokens produced this iteration."""
81
82 cache_hit_tokens: int
83 """DeepSeek prefix-cache hit tokens (0 if unavailable)."""
84
85 cache_miss_tokens: int
86 """DeepSeek prefix-cache miss tokens (0 if unavailable)."""
87
88 dur_ms: int
89 """Wall-clock duration of this iteration in milliseconds."""
90
91
92_dbg("OC_CHAT.CLIENT", "module", "loaded", 4)
93
94
95# ── Serialisation ──────────────────────────────────────────────────────────
96
97
98def iteration_stat_to_json(stat: IterationStat) -> str:
99 """Lossless JSON-serialisation of an ``IterationStat``.
100
101 Converts the ``tools`` tuple to a list for JSON compatibility.
102 Returns a single line (no indentation).
103 """
104 d = {
105 "ts": stat.ts,
106 "iter": stat.iter,
107 "model": stat.model,
108 "effort": stat.effort,
109 "tools": list(stat.tools),
110 "prompt_tokens": stat.prompt_tokens,
111 "completion_tokens": stat.completion_tokens,
112 "cache_hit_tokens": stat.cache_hit_tokens,
113 "cache_miss_tokens": stat.cache_miss_tokens,
114 "dur_ms": stat.dur_ms,
115 }
116 return json.dumps(d, ensure_ascii=False, sort_keys=False)
117
118
119def iteration_stat_human_line(stat: IterationStat) -> str:
120 """Compact one-line human-readable representation.
121
122 Example::
123
124 [14:32:01.123] iter=2 model=deepseek-v4-pro tools=[read_file,text_search]
125 p=12410 c=342 cache=11800/610 dur=1.84s
126
127 The bracketed time is local wall-clock, matching the offset in
128 ``stat.ts`` (e.g. the full JSONL emission is
129 ``2026-05-17T14:32:01.123+02:00``). The compact display drops both
130 the date and the offset to fit a per-iteration scrollback line.
131 """
132 # ``stat.ts`` is ``YYYY-MM-DDTHH:MM:SS.mmm±HH:MM``; index 11:23 is
133 # the ``HH:MM:SS.mmm`` slice regardless of the trailing zone offset.
134 ts_part = stat.ts[11:23]
135 tools_part = ",".join(stat.tools) if stat.tools else "-"
136 cache_part = (
137 f"cache={stat.cache_hit_tokens}/{stat.cache_miss_tokens}"
138 if stat.cache_hit_tokens or stat.cache_miss_tokens
139 else ""
140 )
141 dur_s = f"{stat.dur_ms / 1000:.2f}s" if stat.dur_ms >= 1000 else f"{stat.dur_ms}ms"
142
143 parts = [
144 f"[{ts_part}]",
145 f"iter={stat.iter}",
146 f"model={stat.model}",
147 f"tools=[{tools_part}]",
148 f"p={stat.prompt_tokens}",
149 f"c={stat.completion_tokens}",
150 ]
151 if cache_part:
152 parts.append(cache_part)
153 parts.append(f"dur={dur_s}")
154
155 return " ".join(parts)
str iteration_stat_human_line(IterationStat stat)
str iteration_stat_to_json(IterationStat stat)