File size: 6,309 Bytes
d0eee72 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | """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"
|