|
@@ -28,9 +28,10 @@ def _pretty(obj: Any) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pre(text: Any, *, klass: str = "code") -> str:
|
|
def _pre(text: Any, *, klass: str = "code") -> str:
|
|
|
- content = _pretty(text) if not isinstance(text, str) else text
|
|
|
|
|
- if content is None:
|
|
|
|
|
|
|
+ if text is None:
|
|
|
content = ""
|
|
content = ""
|
|
|
|
|
+ else:
|
|
|
|
|
+ content = _pretty(text)
|
|
|
return f'<pre class="{klass}">{_esc(content)}</pre>'
|
|
return f'<pre class="{klass}">{_esc(content)}</pre>'
|
|
|
|
|
|
|
|
|
|
|
|
@@ -116,8 +117,8 @@ def _render_messages(
|
|
|
body_parts.append(_pre(msg["content"], klass="code prose"))
|
|
body_parts.append(_pre(msg["content"], klass="code prose"))
|
|
|
if msg.get("tool_calls"):
|
|
if msg.get("tool_calls"):
|
|
|
body_parts.append(
|
|
body_parts.append(
|
|
|
- '<div class="sublabel">tool_calls</div>'
|
|
|
|
|
- + _pre(msg["tool_calls"])
|
|
|
|
|
|
|
+ '<div class="sublabel">决定调用的工具</div>'
|
|
|
|
|
+ + _render_tool_call_cards(msg["tool_calls"])
|
|
|
)
|
|
)
|
|
|
if msg.get("tool_call_id"):
|
|
if msg.get("tool_call_id"):
|
|
|
body_parts.append(
|
|
body_parts.append(
|
|
@@ -127,15 +128,24 @@ def _render_messages(
|
|
|
body_parts.append(
|
|
body_parts.append(
|
|
|
f'<div class="meta-line">name: <code>{_esc(msg["name"])}</code></div>'
|
|
f'<div class="meta-line">name: <code>{_esc(msg["name"])}</code></div>'
|
|
|
)
|
|
)
|
|
|
- if default_open is None:
|
|
|
|
|
|
|
+ # Tool result content: try pretty JSON (already handled by _pre via content) if default_open is None:
|
|
|
open_attr = "open" if role != "system" else ""
|
|
open_attr = "open" if role != "system" else ""
|
|
|
else:
|
|
else:
|
|
|
open_attr = "open" if default_open else ""
|
|
open_attr = "open" if default_open else ""
|
|
|
|
|
+ preview = _preview(msg.get("content"))
|
|
|
|
|
+ if not preview and msg.get("tool_calls"):
|
|
|
|
|
+ names = []
|
|
|
|
|
+ for tc in msg["tool_calls"]:
|
|
|
|
|
+ name, _, _ = _normalize_tool_call(tc)
|
|
|
|
|
+ names.append(name)
|
|
|
|
|
+ preview = "调用: " + ", ".join(names)
|
|
|
|
|
+ elif not preview and msg.get("name"):
|
|
|
|
|
+ preview = f"tool → {msg['name']}"
|
|
|
parts.append(
|
|
parts.append(
|
|
|
f"""
|
|
f"""
|
|
|
<details class="msg" {open_attr}>
|
|
<details class="msg" {open_attr}>
|
|
|
<summary>{_role_badge(role)} <span class="msg-idx">#{index_offset + i + 1}</span>
|
|
<summary>{_role_badge(role)} <span class="msg-idx">#{index_offset + i + 1}</span>
|
|
|
- <span class="msg-preview">{_esc(_preview(msg.get("content")))}</span>
|
|
|
|
|
|
|
+ <span class="msg-preview">{_esc(preview)}</span>
|
|
|
</summary>
|
|
</summary>
|
|
|
<div class="msg-body">{"".join(body_parts) or '<p class="muted">(empty)</p>'}</div>
|
|
<div class="msg-body">{"".join(body_parts) or '<p class="muted">(empty)</p>'}</div>
|
|
|
</details>
|
|
</details>
|
|
@@ -217,12 +227,7 @@ def _render_llm_input(
|
|
|
|
|
|
|
|
def _render_reasoning(reasoning: Any) -> str:
|
|
def _render_reasoning(reasoning: Any) -> str:
|
|
|
if not reasoning:
|
|
if not reasoning:
|
|
|
- return """
|
|
|
|
|
- <div class="reasoning empty">
|
|
|
|
|
- <div class="sublabel">思考过程</div>
|
|
|
|
|
- <p class="muted">本次回复未返回 reasoning 字段</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- """
|
|
|
|
|
|
|
+ return ""
|
|
|
return f"""
|
|
return f"""
|
|
|
<div class="reasoning">
|
|
<div class="reasoning">
|
|
|
<div class="sublabel">思考过程</div>
|
|
<div class="sublabel">思考过程</div>
|
|
@@ -240,19 +245,23 @@ def _parse_tool_arguments(arguments: Any) -> Any:
|
|
|
return arguments
|
|
return arguments
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _render_planned_tool_calls(tool_calls: list[dict[str, Any]]) -> str:
|
|
|
|
|
- """Show each planned tool as name + args; keep raw JSON in a collapsible."""
|
|
|
|
|
|
|
+def _normalize_tool_call(tc: dict[str, Any]) -> tuple[str, Any, str]:
|
|
|
|
|
+ """Return (name, parsed_args, id) from flat or OpenAI function-wrapped tool_call."""
|
|
|
|
|
+ name = tc.get("name") or (tc.get("function") or {}).get("name") or "?"
|
|
|
|
|
+ raw_args = tc.get("arguments")
|
|
|
|
|
+ if raw_args is None and isinstance(tc.get("function"), dict):
|
|
|
|
|
+ raw_args = tc["function"].get("arguments")
|
|
|
|
|
+ return name, _parse_tool_arguments(raw_args), str(tc.get("id") or "")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _render_tool_call_cards(tool_calls: list[dict[str, Any]]) -> str:
|
|
|
|
|
+ """Render tool calls as name + args cards; raw JSON in a collapsible."""
|
|
|
if not tool_calls:
|
|
if not tool_calls:
|
|
|
return ""
|
|
return ""
|
|
|
|
|
|
|
|
cards: list[str] = []
|
|
cards: list[str] = []
|
|
|
for i, tc in enumerate(tool_calls, 1):
|
|
for i, tc in enumerate(tool_calls, 1):
|
|
|
- name = tc.get("name") or (tc.get("function") or {}).get("name") or "?"
|
|
|
|
|
- raw_args = tc.get("arguments")
|
|
|
|
|
- if raw_args is None and isinstance(tc.get("function"), dict):
|
|
|
|
|
- raw_args = tc["function"].get("arguments")
|
|
|
|
|
- args = _parse_tool_arguments(raw_args)
|
|
|
|
|
- tc_id = tc.get("id") or ""
|
|
|
|
|
|
|
+ name, args, tc_id = _normalize_tool_call(tc)
|
|
|
cards.append(
|
|
cards.append(
|
|
|
f"""
|
|
f"""
|
|
|
<div class="planned-tool">
|
|
<div class="planned-tool">
|
|
@@ -268,7 +277,6 @@ def _render_planned_tool_calls(tool_calls: list[dict[str, Any]]) -> str:
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
return f"""
|
|
return f"""
|
|
|
- <h4>模型决定调用的工具</h4>
|
|
|
|
|
<div class="planned-tool-list">{"".join(cards)}</div>
|
|
<div class="planned-tool-list">{"".join(cards)}</div>
|
|
|
<details class="raw-block">
|
|
<details class="raw-block">
|
|
|
<summary>查看原始 tool_calls JSON</summary>
|
|
<summary>查看原始 tool_calls JSON</summary>
|
|
@@ -277,9 +285,22 @@ def _render_planned_tool_calls(tool_calls: list[dict[str, Any]]) -> str:
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _render_planned_tool_calls(tool_calls: list[dict[str, Any]]) -> str:
|
|
|
|
|
+ """Show each planned tool as name + args; omit entirely when empty."""
|
|
|
|
|
+ if not tool_calls:
|
|
|
|
|
+ return ""
|
|
|
|
|
+ return f"""
|
|
|
|
|
+ <h4>模型决定调用的工具</h4>
|
|
|
|
|
+ {_render_tool_call_cards(tool_calls)}
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _render_llm_output(data: dict[str, Any], seq: int, iteration: Any) -> str:
|
|
def _render_llm_output(data: dict[str, Any], seq: int, iteration: Any) -> str:
|
|
|
tool_calls = data.get("tool_calls") or []
|
|
tool_calls = data.get("tool_calls") or []
|
|
|
|
|
+ reasoning = data.get("reasoning")
|
|
|
|
|
+ content = data.get("content")
|
|
|
usage = data.get("usage")
|
|
usage = data.get("usage")
|
|
|
|
|
+
|
|
|
usage_html = ""
|
|
usage_html = ""
|
|
|
if usage:
|
|
if usage:
|
|
|
usage_html = f"""
|
|
usage_html = f"""
|
|
@@ -291,14 +312,16 @@ def _render_llm_output(data: dict[str, Any], seq: int, iteration: Any) -> str:
|
|
|
</div>
|
|
</div>
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
- tool_calls_html = _render_planned_tool_calls(tool_calls)
|
|
|
|
|
- content = data.get("content")
|
|
|
|
|
content_html = (
|
|
content_html = (
|
|
|
- f"<h4>模型输出文本</h4>{_pre(content, klass='code prose')}"
|
|
|
|
|
- if content
|
|
|
|
|
- else '<h4>模型输出文本</h4><p class="muted">(空,可能仅有 tool_calls)</p>'
|
|
|
|
|
|
|
+ f"<h4>模型输出文本</h4>{_pre(content, klass='code prose')}" if content else ""
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+ tags: list[str] = [f'<span class="tag">iteration {iteration}</span>']
|
|
|
|
|
+ if reasoning:
|
|
|
|
|
+ tags.append('<span class="tag">有思考</span>')
|
|
|
|
|
+ if tool_calls:
|
|
|
|
|
+ tags.append(f'<span class="tag">{len(tool_calls)} tool calls</span>')
|
|
|
|
|
+
|
|
|
raw_html = ""
|
|
raw_html = ""
|
|
|
if data.get("raw"):
|
|
if data.get("raw"):
|
|
|
raw_html = f"""
|
|
raw_html = f"""
|
|
@@ -314,15 +337,13 @@ def _render_llm_output(data: dict[str, Any], seq: int, iteration: Any) -> str:
|
|
|
<div class="step-num">Step {seq}</div>
|
|
<div class="step-num">Step {seq}</div>
|
|
|
<div class="card-title">LLM 输出</div>
|
|
<div class="card-title">LLM 输出</div>
|
|
|
<div class="card-tags">
|
|
<div class="card-tags">
|
|
|
- <span class="tag">iteration {iteration}</span>
|
|
|
|
|
- <span class="tag">{'有思考' if data.get('reasoning') or data.get('has_reasoning') else '无思考'}</span>
|
|
|
|
|
- <span class="tag">{len(tool_calls)} tool calls</span>
|
|
|
|
|
|
|
+ {"".join(tags)}
|
|
|
</div>
|
|
</div>
|
|
|
</header>
|
|
</header>
|
|
|
<div class="card-body">
|
|
<div class="card-body">
|
|
|
- {_render_reasoning(data.get("reasoning"))}
|
|
|
|
|
|
|
+ {_render_reasoning(reasoning)}
|
|
|
{content_html}
|
|
{content_html}
|
|
|
- {tool_calls_html}
|
|
|
|
|
|
|
+ {_render_planned_tool_calls(tool_calls)}
|
|
|
{usage_html}
|
|
{usage_html}
|
|
|
{raw_html}
|
|
{raw_html}
|
|
|
</div>
|
|
</div>
|
|
@@ -793,6 +814,7 @@ def render_html(events: list[dict[str, Any]]) -> str:
|
|
|
<div class="layout">
|
|
<div class="layout">
|
|
|
<aside class="sidebar">
|
|
<aside class="sidebar">
|
|
|
<h1>Agent Trace</h1>
|
|
<h1>Agent Trace</h1>
|
|
|
|
|
+ <div class="run-id">{_esc(meta.get("agent_name") or "agent")}</div>
|
|
|
<div class="run-id">{_esc(meta.get("run_id"))}</div>
|
|
<div class="run-id">{_esc(meta.get("run_id"))}</div>
|
|
|
<nav>
|
|
<nav>
|
|
|
{_nav_items(events)}
|
|
{_nav_items(events)}
|
|
@@ -802,6 +824,7 @@ def render_html(events: list[dict[str, Any]]) -> str:
|
|
|
<section class="hero">
|
|
<section class="hero">
|
|
|
<h2>运行概览</h2>
|
|
<h2>运行概览</h2>
|
|
|
<div class="stats">
|
|
<div class="stats">
|
|
|
|
|
+ <div class="stat"><div class="label">Agent</div><div class="value">{_esc(meta.get("agent_name") or "—")}</div></div>
|
|
|
<div class="stat"><div class="label">Model</div><div class="value">{_esc(meta.get("model") or "—")}</div></div>
|
|
<div class="stat"><div class="label">Model</div><div class="value">{_esc(meta.get("model") or "—")}</div></div>
|
|
|
<div class="stat"><div class="label">Iterations</div><div class="value">{_esc(meta.get("iterations"))}</div></div>
|
|
<div class="stat"><div class="label">Iterations</div><div class="value">{_esc(meta.get("iterations"))}</div></div>
|
|
|
<div class="stat"><div class="label">Tool Calls</div><div class="value">{_esc(meta.get("tool_calls_made"))}</div></div>
|
|
<div class="stat"><div class="label">Tool Calls</div><div class="value">{_esc(meta.get("tool_calls_made"))}</div></div>
|