tracebin / render.py
lvwerra's picture
lvwerra HF Staff
tracebin: capability-URL pastebin for agent traces
d0eee72 verified
Raw History Blame Contribute Delete
6.31 kB
"""Turn whatever someone posted into something a template can loop over.
Agent traces arrive in a handful of shapes. We recognise the common message-list
dialects (OpenAI chat completions, Anthropic messages, and the LangChain-ish
variants that mostly look like them) and fall back to pretty-printed JSON or
plain text. Nothing here trusts its input: every value ends up escaped by the
Jinja autoescaper, and unknown block types are shown as JSON rather than
guessed at.
"""
from __future__ import annotations
import json
from typing import Any
ROLE_ALIASES = {
"human": "user",
"ai": "assistant",
"model": "assistant",
"function": "tool",
}
KNOWN_ROLES = {"system", "developer", "user", "assistant", "tool"}
def _pretty(value: Any) -> str:
if isinstance(value, str):
return value
return json.dumps(value, indent=2, ensure_ascii=False, default=str)
def _blocks_from_content(content: Any) -> list[dict[str, Any]]:
"""Normalise a message's ``content`` into a list of display blocks."""
if content is None:
return []
if isinstance(content, str):
return [{"type": "text", "text": content}] if content.strip() else []
if isinstance(content, dict):
content = [content]
if not isinstance(content, list):
return [{"type": "json", "text": _pretty(content)}]
blocks: list[dict[str, Any]] = []
for item in content:
if isinstance(item, str):
blocks.append({"type": "text", "text": item})
continue
if not isinstance(item, dict):
blocks.append({"type": "json", "text": _pretty(item)})
continue
kind = item.get("type")
if kind == "text":
blocks.append({"type": "text", "text": item.get("text", "")})
elif kind == "thinking":
blocks.append(
{"type": "thinking", "text": item.get("thinking") or item.get("text", "")}
)
elif kind == "tool_use":
blocks.append(
{
"type": "tool_use",
"name": item.get("name", "tool"),
"text": _pretty(item.get("input", {})),
}
)
elif kind == "tool_result":
blocks.append(
{
"type": "tool_result",
"name": item.get("tool_use_id", ""),
"text": _pretty(item.get("content", "")),
"error": bool(item.get("is_error")),
}
)
elif kind in ("image", "image_url"):
# Never render remote images: it would leak the secret URL through
# the Referer header and defeat the whole point.
blocks.append({"type": "note", "text": "[image omitted]"})
else:
blocks.append({"type": "json", "text": _pretty(item)})
return blocks
def _normalize_message(raw: Any) -> dict[str, Any]:
if not isinstance(raw, dict):
return {"role": "unknown", "blocks": [{"type": "json", "text": _pretty(raw)}]}
role = str(raw.get("role") or raw.get("type") or "unknown").lower()
role = ROLE_ALIASES.get(role, role)
blocks = _blocks_from_content(raw.get("content"))
# OpenAI-style tool calls live beside content, not inside it.
for call in raw.get("tool_calls") or []:
if not isinstance(call, dict):
continue
fn = call.get("function") or {}
args = fn.get("arguments")
if isinstance(args, str):
try:
args = json.loads(args)
except (ValueError, TypeError):
pass # leave the raw string; malformed args are worth seeing
blocks.append(
{
"type": "tool_use",
"name": fn.get("name") or call.get("name") or "tool",
"text": _pretty(args if args is not None else {}),
}
)
if role == "tool" and not blocks and raw.get("name"):
blocks.append({"type": "note", "text": str(raw["name"])})
# Anthropic puts tool results in a *user* turn. Labelling that column "user"
# is technically faithful and completely misleading to read, so show what
# the turn actually contains.
if role == "user" and blocks and all(b["type"] == "tool_result" for b in blocks):
role = "tool"
return {
"role": role,
"role_class": role if role in KNOWN_ROLES else "unknown",
"name": raw.get("name") or "",
"blocks": blocks,
}
def _find_messages(payload: Any) -> list | None:
"""Locate a message list in a few common wrapper shapes."""
if isinstance(payload, list) and payload and isinstance(payload[0], dict):
if any(k in payload[0] for k in ("role", "type", "content")):
return payload
return None
if isinstance(payload, dict):
for key in ("messages", "trace", "conversation", "history", "turns", "steps"):
value = payload.get(key)
if isinstance(value, list) and value:
return value
return None
def normalize(payload: Any) -> dict[str, Any]:
"""Return ``{"kind": ..., ...}`` describing how to display ``payload``."""
if isinstance(payload, str):
return {"kind": "text", "text": payload}
messages = _find_messages(payload)
if messages is not None:
meta = {}
if isinstance(payload, dict):
meta = {
k: v
for k, v in payload.items()
if k
not in ("messages", "trace", "conversation", "history", "turns", "steps")
and isinstance(v, (str, int, float, bool))
}
return {
"kind": "messages",
"messages": [_normalize_message(m) for m in messages],
"meta": meta,
}
return {"kind": "json", "text": _pretty(payload)}
def summarize(payload: Any) -> str:
"""One-line description for the page title / header."""
messages = _find_messages(payload)
if messages is not None:
return f"{len(messages)} message{'s' if len(messages) != 1 else ''}"
if isinstance(payload, str):
lines = payload.count("\n") + 1
return f"{lines} line{'s' if lines != 1 else ''}"
return "JSON document"