mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21:07:03 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JThjboXTeZ3yJPBzxMx5wS
227 lines
8.6 KiB
Python
227 lines
8.6 KiB
Python
"""Walk one transcript file into the rows the store keeps for it.
|
|
|
|
Reuses token_audit's record primitives -- line loading, usage extraction,
|
|
per-API-message dedup (:class:`TurnMerger`) and list-price pricing -- rather
|
|
than :func:`parse.iter_events`, because the dashboard also needs what that
|
|
stream drops: every record's timestamp (active time), prompt boundaries, the
|
|
``Agent`` calls that spawned subagents, and commit hashes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from python_pkg.token_audit import discover, parse, pricing, records, turns
|
|
from python_pkg.token_dashboard import active, blocks, carry, detail
|
|
from python_pkg.token_dashboard.blocks import PROMPT_CHARS
|
|
from python_pkg.token_dashboard.rows import (
|
|
AgentCall,
|
|
Commit,
|
|
FileFacts,
|
|
PromptRow,
|
|
ToolRow,
|
|
TurnRow,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
from python_pkg.token_audit.model import Turn
|
|
|
|
__all__ = ["NO_TOOL", "PROMPT_CHARS", "FileFacts", "walk_file"]
|
|
|
|
NO_TOOL = "(no tool)"
|
|
AGENT_TOOLS = frozenset({"Agent", "Task"})
|
|
_BOOK = pricing.load()
|
|
|
|
|
|
class _Walker:
|
|
"""Single pass over a file's records, holding the cross-record state."""
|
|
|
|
def __init__(self, facts: FileFacts) -> None:
|
|
self.facts = facts
|
|
self.prompt = -1
|
|
self.ts = 0.0
|
|
self.stamps: list[float] = []
|
|
self.pending: dict[str, dict[str, Any]] = {}
|
|
self.names: dict[str, list[str]] = {}
|
|
self.use_ts: dict[str, float] = {}
|
|
self.tools: dict[tuple[int, str], ToolRow] = {}
|
|
self.carry = carry.Tracker()
|
|
self.detail = detail.Detail()
|
|
self.merger = records.TurnMerger()
|
|
self.last_text = ""
|
|
|
|
def tool(self, name: str) -> ToolRow:
|
|
"""The accumulator for ``name`` in the current prompt."""
|
|
return self.tools.setdefault((self.prompt, name), ToolRow(self.prompt, name))
|
|
|
|
def release(self, turn: Turn | None) -> None:
|
|
"""Price a completed API call and split it across its tools."""
|
|
if turn is None:
|
|
return
|
|
cost = pricing.usd(turn, _BOOK)
|
|
usage = turn.usage
|
|
self.facts.rows.turns.append(
|
|
TurnRow(
|
|
turn.timestamp or self.ts,
|
|
turn.model,
|
|
cost,
|
|
turn.context,
|
|
self.prompt,
|
|
usage,
|
|
)
|
|
)
|
|
self.carry.turn(turn.model, turn.context)
|
|
self.detail.turn(turn, cost, self.prompt)
|
|
names = self.names.pop(turn.message_id, []) or [NO_TOOL]
|
|
for name in names:
|
|
self.tool(name).usd += cost / len(names)
|
|
|
|
def record(self, rec: dict[str, Any]) -> None:
|
|
"""Account one record."""
|
|
stamp = records.timestamp(rec)
|
|
if stamp is not None:
|
|
self.ts = stamp
|
|
self.stamps.append(stamp)
|
|
if self.facts.cwd is None and isinstance(rec.get("cwd"), str):
|
|
self.facts.cwd = rec["cwd"]
|
|
if rec.get("type") == "user":
|
|
self.release(self.merger.flush())
|
|
self.user(rec)
|
|
self.detail.record(rec, self.ts, self.prompt)
|
|
for block in parse.content_blocks(rec):
|
|
self.block(rec, block)
|
|
found = turns.usage_of(rec)
|
|
if found is not None:
|
|
self.release(self.merger.push(turns.build_turn(rec, *found)))
|
|
|
|
def user(self, rec: dict[str, Any]) -> None:
|
|
"""Open a new prompt when this user record is one."""
|
|
text = blocks.prompt_text(rec)
|
|
if text is None or self.facts.parent_session is not None:
|
|
return
|
|
self.prompt += 1
|
|
prompt_id = rec.get("promptId")
|
|
self.facts.rows.prompts.append(
|
|
PromptRow(self.prompt, self.ts, str(prompt_id or ""), text)
|
|
)
|
|
|
|
def block(self, rec: dict[str, Any], block: dict[str, Any]) -> None:
|
|
"""Remember tool calls, pair results, keep the last reply text."""
|
|
kind = block.get("type")
|
|
if kind == "tool_use":
|
|
self.tool_use(rec, block)
|
|
elif kind == "tool_result":
|
|
self.tool_result(rec, block)
|
|
elif kind == "text" and rec.get("type") == "assistant":
|
|
self.last_text = str(block.get("text") or "")
|
|
|
|
def tool_use(self, rec: dict[str, Any], block: dict[str, Any]) -> None:
|
|
"""Hold a call until its result arrives; note which message issued it."""
|
|
call_id = str(block.get("id") or "")
|
|
self.pending[call_id] = block
|
|
self.use_ts[call_id] = self.ts
|
|
message = rec.get("message")
|
|
mid = message.get("id") if isinstance(message, dict) else None
|
|
name = str(block.get("name") or "unknown")
|
|
self.names.setdefault(str(mid or ""), []).append(name)
|
|
self.detail.use(rec, block, self.ts, self.prompt)
|
|
|
|
def tool_result(self, rec: dict[str, Any], block: dict[str, Any]) -> None:
|
|
"""Pair a result with its call and record what it tells us."""
|
|
call_id = str(block.get("tool_use_id") or "")
|
|
use = self.pending.get(call_id)
|
|
paired = parse.pair_result(block, self.pending)
|
|
if use is None or paired is None:
|
|
return
|
|
self.detail.result(block, self.ts)
|
|
row = self.tool(paired.name)
|
|
row.calls += 1
|
|
row.result_tokens += paired.result_tokens
|
|
self.carry.result(row, paired.result_tokens)
|
|
text = blocks.text_of(block.get("content"))
|
|
raw_input = use.get("input")
|
|
tool_input: dict[str, Any] = raw_input if isinstance(raw_input, dict) else {}
|
|
if paired.name in AGENT_TOOLS:
|
|
failed = bool(block.get("is_error"))
|
|
self.agent_call(rec, call_id, tool_input, failed=failed)
|
|
elif paired.name == "SendMessage":
|
|
self.facts.rows.messages.append((self.ts, str(tool_input.get("to") or "")))
|
|
if paired.commit:
|
|
# Every commit command is kept, hash or not: most run through
|
|
# finish_auto.sh with a tailed or backgrounded output, so the hash
|
|
# is usually found later by :mod:`commits` from git log instead.
|
|
hint = blocks.repo_hint(str(tool_input.get("command")), self.facts.cwd)
|
|
started = self.use_ts.pop(call_id, self.ts)
|
|
self.facts.rows.commits += [
|
|
Commit(self.prompt, started, self.ts, sha, hint)
|
|
for sha in blocks.commit_shas(text) or [""]
|
|
]
|
|
table = blocks.lever_table(text)
|
|
if table is not None:
|
|
self.facts.rows.lever_tables.append((self.ts, table))
|
|
|
|
def agent_call(
|
|
self,
|
|
rec: dict[str, Any],
|
|
call_id: str,
|
|
tool_input: dict[str, Any],
|
|
*,
|
|
failed: bool,
|
|
) -> None:
|
|
"""Record a subagent spawn with the ``agentId`` its result names."""
|
|
result = rec.get("toolUseResult")
|
|
result = result if isinstance(result, dict) else {}
|
|
agent_id = result.get("agentId")
|
|
status = "error" if failed else str(result.get("status") or "")
|
|
self.facts.rows.agent_calls.append(
|
|
AgentCall(
|
|
tool_use_id=call_id,
|
|
agent_id=str(agent_id or ""),
|
|
prompt_idx=self.prompt,
|
|
ts=self.use_ts.get(call_id, self.ts),
|
|
description=str(tool_input.get("description") or ""),
|
|
subagent_type=str(tool_input.get("subagent_type") or ""),
|
|
model=str(tool_input.get("model") or ""),
|
|
result_ts=self.ts,
|
|
status=status,
|
|
)
|
|
)
|
|
|
|
def finish(self) -> FileFacts:
|
|
"""Flush state into the facts."""
|
|
self.release(self.merger.flush())
|
|
facts = self.facts
|
|
facts.rows.tools = list(self.tools.values())
|
|
self.carry.settle(_BOOK)
|
|
facts.rows.detail = self.detail.finish(_BOOK)
|
|
facts.rows.intervals = active.intervals(self.stamps)
|
|
if self.stamps:
|
|
facts.span = (min(self.stamps), max(self.stamps))
|
|
facts.final_tag = blocks.final_tag(self.last_text)
|
|
return facts
|
|
|
|
|
|
def walk_file(path: Path, root: Path) -> FileFacts:
|
|
"""Read one transcript (main or subagent) into :class:`FileFacts`."""
|
|
del root # keys are derived from the path's own layout
|
|
if path.parent.name == "subagents":
|
|
parent = path.parent.parent.name
|
|
facts = FileFacts(
|
|
key=f"{parent}/{path.stem}",
|
|
session_id=parent,
|
|
parent_session=parent,
|
|
cwd=None,
|
|
meta=discover.agent_meta(path),
|
|
)
|
|
else:
|
|
facts = FileFacts(path.stem, path.stem, None, None, {})
|
|
walker = _Walker(facts)
|
|
with path.open(encoding="utf-8", errors="replace") as handle:
|
|
for line in handle:
|
|
rec = parse.load_line(line)
|
|
if rec is not None:
|
|
walker.record(rec)
|
|
return walker.finish()
|