from __future__ import annotations import hashlib from dataclasses import dataclass, field from typing import Any, Callable def sha256_text(value: str) -> str: return hashlib.sha256((value or "").encode("utf-8")).hexdigest() def truncate_text(value: str, limit: int = 500) -> str: text = value or "" if len(text) <= limit: return text return f"{text[:limit]}…[truncated {len(text) - limit} chars]" @dataclass class LlmCallObserver: call: Callable[[str], str] stage: str records: list[dict[str, Any]] = field(default_factory=list) prompt_preview_chars: int = 500 response_preview_chars: int = 500 def __call__(self, prompt: str) -> str: response = self.call(prompt) self.records.append( { "stage": self.stage, "call_index": len(self.records) + 1, "prompt_hash": sha256_text(prompt), "response_hash": sha256_text(response), "prompt_chars": len(prompt or ""), "response_chars": len(response or ""), "prompt_preview": truncate_text(prompt, self.prompt_preview_chars), "response_preview": truncate_text(response, self.response_preview_chars), } ) return response def summarize_observed_calls(observers: list[LlmCallObserver]) -> dict[str, Any]: records: list[dict[str, Any]] = [] by_stage: dict[str, int] = {} for observer in observers: records.extend(observer.records) by_stage[observer.stage] = by_stage.get(observer.stage, 0) + len(observer.records) return { "total_calls": len(records), "by_stage": by_stage, "records": records, }