55 lines
1.7 KiB
Python
55 lines
1.7 KiB
Python
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,
|
|
}
|