@salesforce/afv-skills 1.62.0 → 1.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/agentforce-offline-experimentation-run/SKILL.md +253 -0
- package/skills/agentforce-offline-experimentation-run/scripts/build_report.py +796 -0
- package/skills/agentforce-offline-experimentation-run/scripts/resolve-token.sh +111 -0
- package/skills/agentforce-offline-experimentation-run/scripts/sfap_replay.py +1053 -0
- package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +21 -11
- package/skills/automation-sandbox-post-copy-config-generate/assets/config_template.json +10 -0
- package/skills/automation-sandbox-post-copy-config-generate/assets/json_schema.json +33 -1
- package/skills/automation-sandbox-post-copy-config-generate/references/configuration_catalog.md +44 -1
- package/skills/automation-sandbox-post-copy-configure/SKILL.md +132 -124
- package/skills/automation-sandbox-post-copy-configure/references/flow_activation_path.md +151 -0
- package/skills/automation-sandbox-post-copy-configure/references/scheduled_apex_path.md +55 -9
- package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +54 -107
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/copy.json +1 -1
- package/skills/consumer-goods-promotion-bo-api-deploy/references/conventions-and-payload-rules.md +82 -5
- package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +35 -2
- package/skills/consumer-goods-promotion-bo-api-deploy/references/preflight-and-seed-queries.md +149 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +17 -1
- package/skills/dx-devops-conflict-resolve/SKILL.md +1 -5
- package/skills/dx-devops-pipeline-manage/SKILL.md +1 -5
- package/skills/dx-devops-project-manage/SKILL.md +1 -5
- package/skills/dx-devops-promote/SKILL.md +1 -5
- package/skills/dx-devops-request-status/SKILL.md +1 -5
- package/skills/dx-lifecycle-center/SKILL.md +120 -0
- package/skills/dx-lifecycle-center/examples/workflows.md +175 -0
- package/skills/dx-lifecycle-center/references/connect-api.md +123 -0
- package/skills/dx-lifecycle-center/references/connect-org.md +184 -0
- package/skills/dx-lifecycle-center/references/deploy.md +153 -0
- package/skills/dx-lifecycle-center/references/error-handling.md +21 -0
- package/skills/dx-lifecycle-center/references/hub-org.md +37 -0
- package/skills/dx-lifecycle-center/references/list-orgs.md +99 -0
- package/skills/dx-lifecycle-center/references/request-body.md +75 -0
- package/skills/dx-lifecycle-center/references/troubleshooting.md +31 -0
- package/skills/dx-lifecycle-center/scripts/poll-deploy-status.sh +200 -0
- package/skills/dx-lifecycle-center/scripts/resolve-environment.sh +120 -0
- package/skills/dx-org-devhub-configure/SKILL.md +1 -3
- package/skills/dx-org-shape-manage/SKILL.md +1 -5
- package/skills/dx-org-snapshot-manage/SKILL.md +1 -3
- package/skills/education-cloud-academic-calendar-generate/SKILL.md +1 -3
- package/skills/education-cloud-course-catalog-migrate/SKILL.md +1 -3
- package/skills/education-cloud-domain-configure/SKILL.md +1 -3
- package/skills/education-cloud-multi-campus-configure/SKILL.md +1 -3
- package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +1 -5
- package/skills/experience-ui-bundle-2gp-deploy/SKILL.md +1 -3
- package/skills/experience-ui-bundle-features-generate/SKILL.md +1 -5
- package/skills/experience-ui-bundle-mfa-configure/SKILL.md +1 -3
- package/skills/field-service-data-capture-migrate/SKILL.md +1 -3
- package/skills/insurance-brokerage-agency-billing-configure/SKILL.md +1 -3
- package/skills/marketing-message-test/SKILL.md +1 -3
- package/skills/nonprofit-fundraising-configure/SKILL.md +240 -0
- package/skills/nonprofit-fundraising-configure/references/gotchas.md +26 -0
- package/skills/nonprofit-fundraising-configure/references/mcp-invocation.md +678 -0
- package/skills/nonprofit-fundraising-configure/references/permission-targeting.md +35 -0
- package/skills/nonprofit-volunteer-management-model-get/SKILL.md +166 -0
- package/skills/nonprofit-volunteer-management-model-get/references/entities.md +315 -0
- package/skills/nonprofit-volunteer-management-model-get/references/relationships.md +175 -0
- package/skills/nonprofit-volunteer-management-model-get/references/themes.md +143 -0
- package/skills/nonprofit-volunteer-management-records-create/SKILL.md +121 -0
- package/skills/nonprofit-volunteer-management-records-create/references/field-guide.md +284 -0
- package/skills/nonprofit-volunteer-management-records-create/references/mcp-invocation.md +248 -0
- package/skills/nonprofit-volunteer-management-records-create/references/review-format.md +140 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/expand-series.py +632 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/render-review-table.py +419 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/tests/test_expand_series.py +690 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/tests/test_render_review_table.py +421 -0
- package/skills/nonprofit-volunteer-management-structure-generate/SKILL.md +119 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/configuration-decisions.md +159 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/discovery-interview.md +137 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/edge-cases.md +48 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/mcp-invocation.md +75 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/plan-output.md +141 -0
- package/skills/platform-custom-field-generate/SKILL.md +1 -0
- package/skills/platform-custom-field-generate/references/advanced-picklists.md +5 -5
- package/skills/platform-custom-metadata-type-generate/SKILL.md +3 -0
- package/skills/platform-custom-object-generate/SKILL.md +4 -0
- package/skills/platform-custom-setting-generate/SKILL.md +3 -0
- package/skills/platform-datamask-run/SKILL.md +1 -5
- package/skills/platform-permission-set-generate/SKILL.md +3 -0
- package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +1 -3
- package/skills/platform-sandbox-configure/SKILL.md +1 -3
- package/skills/platform-value-set-generate/SKILL.md +3 -0
- package/skills/sales-call-scoring-configure/SKILL.md +1 -5
- package/skills/service-agentforce-channel-configure/SKILL.md +1 -3
- package/skills/service-agentforce-human-escalation-configure/SKILL.md +1 -3
- package/skills/service-catalog-template-coordinate/SKILL.md +1 -3
- package/skills/service-concierge-portal-generate/SKILL.md +1 -3
- package/skills/service-email-to-case-configure/SKILL.md +1 -5
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-agentforce-studio-validate/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-access-assign/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-bundle-deploy/SKILL.md +1 -5
- package/skills/service-itsm-agentic-setup-cmdb-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-discovery-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +1 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +1 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-uel-user-create/SKILL.md +1 -9
- package/skills/service-itsm-incident-mgmt-configure/SKILL.md +1 -5
- package/skills/service-itsm-incident-priority-configure/SKILL.md +1 -3
- package/skills/service-itsm-slack-itservice-configure/SKILL.md +1 -3
- package/skills/service-itsm-swarming-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-debug/SKILL.md +1 -3
- package/skills/service-itsm-teams-employee-agent-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-itdesk-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-itservice-configure/SKILL.md +1 -3
- package/skills/service-native-voice-recording-transcription-configure/SKILL.md +1 -3
- package/skills/service-omni-agent-users-create/SKILL.md +1 -3
- package/skills/service-omni-agent-work-sharing-configure/SKILL.md +1 -3
- package/skills/service-omni-attribute-routing-configure/SKILL.md +1 -3
- package/skills/service-omni-base-settings-configure/SKILL.md +1 -3
- package/skills/service-omni-channel-inventory-analyze/SKILL.md +1 -3
- package/skills/service-omni-channel-limits-analyze/SKILL.md +1 -3
- package/skills/service-omni-channel-setup-coordinate/SKILL.md +1 -3
- package/skills/service-omni-command-center-analyze/SKILL.md +1 -3
- package/skills/service-omni-command-center-configure/SKILL.md +1 -3
- package/skills/service-omni-permission-set-assign/SKILL.md +1 -3
- package/skills/service-omni-presence-status-deploy/SKILL.md +1 -3
- package/skills/service-omni-presence-user-config-deploy/SKILL.md +1 -3
- package/skills/service-omni-queue-deploy/SKILL.md +1 -3
- package/skills/service-omni-queue-members-assign/SKILL.md +1 -3
- package/skills/service-omni-queue-routing-config-deploy/SKILL.md +1 -3
- package/skills/service-omni-routing-flow-deploy/SKILL.md +1 -3
- package/skills/service-omni-service-channel-configure/SKILL.md +1 -3
- package/skills/service-omni-sidebar-configure/SKILL.md +1 -3
- package/skills/service-omni-skills-based-routing-configure/SKILL.md +1 -3
- package/skills/service-omni-supervisor-config-deploy/SKILL.md +1 -3
- package/skills/service-omni-supervisor-permset-assign/SKILL.md +1 -3
- package/skills/service-omni-supervisor-surface-deploy/SKILL.md +1 -3
- package/skills/service-omni-supervisor-users-create/SKILL.md +1 -3
- package/skills/service-omni-work-skill-routing-configure/SKILL.md +1 -3
- package/skills/field-service-setup-orchestrator-get/SKILL.md +0 -47
- package/skills/tableau-next-asset-create/README.md +0 -65
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Generate a self-contained HTML offline-analysis report from replay results JSON.
|
|
4
|
+
Adapted from hawking/ai-agent-backtesting PR #53 (build_failure_report.py).
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python build_report.py [replay_results.json] [output.html]
|
|
8
|
+
"""
|
|
9
|
+
import json, sys, html as _html, textwrap
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from collections import Counter
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# Helpers
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
def _calls(ans_json):
|
|
18
|
+
"""Extract [(name, args_dict), ...] from a structured answer JSON string."""
|
|
19
|
+
try:
|
|
20
|
+
ans = json.loads(ans_json) if isinstance(ans_json, str) else ans_json
|
|
21
|
+
except (json.JSONDecodeError, TypeError):
|
|
22
|
+
return []
|
|
23
|
+
tcs = ans.get("tool_calls") or []
|
|
24
|
+
out = []
|
|
25
|
+
for tc in tcs:
|
|
26
|
+
name = tc.get("name", "")
|
|
27
|
+
args = tc.get("arguments", {})
|
|
28
|
+
if isinstance(args, str):
|
|
29
|
+
try:
|
|
30
|
+
args = json.loads(args)
|
|
31
|
+
except Exception:
|
|
32
|
+
args = {"raw": args}
|
|
33
|
+
out.append((name, args))
|
|
34
|
+
return out
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_router(d):
|
|
38
|
+
golden = _calls(d.get("GoldenAnswer", "{}"))
|
|
39
|
+
if d.get("GoldenType") == "topic":
|
|
40
|
+
return True
|
|
41
|
+
for name, _ in golden:
|
|
42
|
+
if any(name.lower().startswith(p) for p in ("route_to", "go_to", "transfer_to")):
|
|
43
|
+
return True
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _bucket(d, name_hit):
|
|
48
|
+
"""Categorize a failing row into a failure bucket."""
|
|
49
|
+
if d.get("ScoringError"):
|
|
50
|
+
err = d["ScoringError"].lower()
|
|
51
|
+
if "expected tool call" in err and "got text" in err:
|
|
52
|
+
return "no tool call emitted"
|
|
53
|
+
if "expected text" in err and "got tool call" in err:
|
|
54
|
+
return "unexpected tool call"
|
|
55
|
+
return "infra error"
|
|
56
|
+
if d.get("ToolAltReason"):
|
|
57
|
+
return "used functionally similar tool"
|
|
58
|
+
if not name_hit:
|
|
59
|
+
return "called a different tool"
|
|
60
|
+
if d.get("SchemaViolationNotes"):
|
|
61
|
+
return "args violate tool schema"
|
|
62
|
+
return "args mismatch (values differ)"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _score_topic(routers):
|
|
66
|
+
if not routers:
|
|
67
|
+
return None
|
|
68
|
+
hits = sum(1 for d in routers if d.get("FunctionNameAgreementLenient"))
|
|
69
|
+
return round(100 * hits / len(routers), 1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _score_action_strict(actions):
|
|
73
|
+
if not actions:
|
|
74
|
+
return None
|
|
75
|
+
hits = sum(1 for d in actions if d.get("PassStrict") or d.get("FunctionArgsAgreement"))
|
|
76
|
+
return round(100 * hits / len(actions), 1)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _score_action_lenient(actions):
|
|
80
|
+
"""Count a row as passing if it passes strict OR used a functionally equivalent tool."""
|
|
81
|
+
if not actions:
|
|
82
|
+
return None
|
|
83
|
+
hits = sum(1 for d in actions
|
|
84
|
+
if d.get("PassStrict") or d.get("FunctionArgsAgreement") or d.get("FunctionNameAgreementLenient"))
|
|
85
|
+
return round(100 * hits / len(actions), 1)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _rating(score, safe_min=85, review_min=60):
|
|
89
|
+
if score is None:
|
|
90
|
+
return "N/A", "#64748b"
|
|
91
|
+
if score >= safe_min:
|
|
92
|
+
return "SAFE", "#15803d"
|
|
93
|
+
if score >= review_min:
|
|
94
|
+
return "REVIEW", "#b45309"
|
|
95
|
+
return "UNSAFE", "#dc2626"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _esc(text):
|
|
99
|
+
return _html.escape(str(text)) if text else ""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _json_pretty(obj):
|
|
103
|
+
if isinstance(obj, str):
|
|
104
|
+
try:
|
|
105
|
+
obj = json.loads(obj)
|
|
106
|
+
except Exception:
|
|
107
|
+
return _esc(obj)
|
|
108
|
+
return _esc(json.dumps(obj, indent=2))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# Card rendering
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
def _card(d, rec, idx):
|
|
116
|
+
"""Render an expandable card for a single failing row."""
|
|
117
|
+
golden = _calls(d.get("GoldenAnswer", "{}"))
|
|
118
|
+
predicted = _calls(d.get("PredictedAnswer", "{}"))
|
|
119
|
+
g_names = [n for n, _ in golden]
|
|
120
|
+
p_names = [n for n, _ in predicted]
|
|
121
|
+
|
|
122
|
+
name_hit = d.get("FunctionNameAgreement", False)
|
|
123
|
+
bucket = _bucket(d, name_hit)
|
|
124
|
+
|
|
125
|
+
flags = []
|
|
126
|
+
if d.get("ScoringError"):
|
|
127
|
+
flags.append(f'<span class="flag flag-error">{_esc(d["ScoringError"][:80])}</span>')
|
|
128
|
+
if d.get("ToolAltReason"):
|
|
129
|
+
flags.append(f'<span class="flag flag-warn">{_esc(d["ToolAltReason"])}</span>')
|
|
130
|
+
if d.get("FunctionArgsScoreReason"):
|
|
131
|
+
flags.append(f'<span class="flag flag-info">Args: {_esc(str(d["FunctionArgsScoreReason"])[:120])}</span>')
|
|
132
|
+
|
|
133
|
+
# Status badge
|
|
134
|
+
if d.get("PassStrict") or d.get("FunctionArgsAgreement"):
|
|
135
|
+
status_cls = "badge-pass"
|
|
136
|
+
status_txt = "PASS"
|
|
137
|
+
elif d.get("FunctionNameAgreementLenient"):
|
|
138
|
+
status_cls = "badge-warn"
|
|
139
|
+
status_txt = "PARTIAL"
|
|
140
|
+
else:
|
|
141
|
+
status_cls = "badge-fail"
|
|
142
|
+
status_txt = "FAIL"
|
|
143
|
+
|
|
144
|
+
# Golden vs Predicted comparison
|
|
145
|
+
golden_html = ""
|
|
146
|
+
for name, args in golden:
|
|
147
|
+
golden_html += f'<div class="tool-call"><span class="tool-name">{_esc(name)}</span>'
|
|
148
|
+
golden_html += f'<pre class="tool-args">{_json_pretty(args)}</pre></div>'
|
|
149
|
+
if not golden:
|
|
150
|
+
try:
|
|
151
|
+
ga = json.loads(d.get("GoldenAnswer", "{}"))
|
|
152
|
+
if ga.get("content"):
|
|
153
|
+
golden_html = f'<div class="text-resp">{_esc(ga["content"][:300])}</div>'
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
predicted_html = ""
|
|
158
|
+
for name, args in predicted:
|
|
159
|
+
predicted_html += f'<div class="tool-call"><span class="tool-name">{_esc(name)}</span>'
|
|
160
|
+
predicted_html += f'<pre class="tool-args">{_json_pretty(args)}</pre></div>'
|
|
161
|
+
if not predicted:
|
|
162
|
+
try:
|
|
163
|
+
pa = json.loads(d.get("PredictedAnswer", "{}"))
|
|
164
|
+
if pa.get("content"):
|
|
165
|
+
predicted_html = f'<div class="text-resp">{_esc(pa["content"][:300])}</div>'
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
# Tools offered
|
|
170
|
+
tools_offered = rec.get("tools", []) if rec else []
|
|
171
|
+
tools_html = ", ".join(f'<code>{_esc(t)}</code>' for t in tools_offered) if tools_offered else "<em>none</em>"
|
|
172
|
+
|
|
173
|
+
# Transcript (all messages, full content)
|
|
174
|
+
messages = rec.get("messages", []) if rec else []
|
|
175
|
+
transcript_html = ""
|
|
176
|
+
for msg in messages:
|
|
177
|
+
role = msg.get("role", "unknown")
|
|
178
|
+
content = str(msg.get("content", ""))
|
|
179
|
+
transcript_html += f'<div class="msg msg-{_esc(role)}"><strong>{_esc(role)}</strong>: {_esc(content)}</div>'
|
|
180
|
+
|
|
181
|
+
return f'''
|
|
182
|
+
<details class="card">
|
|
183
|
+
<summary class="card-header">
|
|
184
|
+
<span class="card-idx">#{idx}</span>
|
|
185
|
+
<span class="{status_cls}">{status_txt}</span>
|
|
186
|
+
<span class="card-bucket">{_esc(bucket)}</span>
|
|
187
|
+
<span class="card-utterance">{_esc(d.get("Utterance", "")[:80])}</span>
|
|
188
|
+
<span class="card-session">{_esc(d.get("AiAgentSessionId", "")[:12])}...</span>
|
|
189
|
+
</summary>
|
|
190
|
+
<div class="card-body">
|
|
191
|
+
<div class="flags">{" ".join(flags)}</div>
|
|
192
|
+
|
|
193
|
+
<div class="comparison">
|
|
194
|
+
<div class="comp-col">
|
|
195
|
+
<h4>Golden (Expected)</h4>
|
|
196
|
+
{golden_html or "<em>none</em>"}
|
|
197
|
+
</div>
|
|
198
|
+
<div class="comp-col">
|
|
199
|
+
<h4>Predicted (Replay)</h4>
|
|
200
|
+
{predicted_html or "<em>none</em>"}
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div class="section">
|
|
205
|
+
<h4>Scores</h4>
|
|
206
|
+
<table class="score-table">
|
|
207
|
+
<tr><td>Function Name Match</td><td>{"Yes" if d.get("FunctionNameAgreement") else "No"}</td></tr>
|
|
208
|
+
<tr><td>Function Name Match (Lenient)</td><td>{"Yes" if d.get("FunctionNameAgreementLenient") else "No"}</td></tr>
|
|
209
|
+
<tr><td>Args Agreement</td><td>{"Yes" if d.get("FunctionArgsAgreement") else "No"}</td></tr>
|
|
210
|
+
<tr><td>Args Score</td><td>{d.get("FunctionArgsScore", "N/A")}</td></tr>
|
|
211
|
+
<tr><td>Pass Strict</td><td>{"Yes" if d.get("PassStrict") else "No"}</td></tr>
|
|
212
|
+
</table>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div class="section">
|
|
216
|
+
<h4>Tools Offered</h4>
|
|
217
|
+
<div>{tools_html}</div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<details class="transcript-toggle">
|
|
221
|
+
<summary>Transcript ({len(messages)} messages)</summary>
|
|
222
|
+
<div class="transcript">{transcript_html or "<em>no transcript captured</em>"}</div>
|
|
223
|
+
</details>
|
|
224
|
+
</div>
|
|
225
|
+
</details>'''
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
# Main HTML builder
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
CSS = '''
|
|
233
|
+
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');
|
|
234
|
+
|
|
235
|
+
:root {
|
|
236
|
+
--slate-50: #f8fafc; --slate-100: #f1f5f9; --slate-200: #e2e8f0;
|
|
237
|
+
--slate-300: #cbd5e1; --slate-400: #94a3b8; --slate-500: #64748b;
|
|
238
|
+
--slate-600: #475569; --slate-700: #334155; --slate-800: #1e293b;
|
|
239
|
+
--slate-900: #0f172a;
|
|
240
|
+
--safe: #15803d; --safe-bg: #f0fdf4; --safe-border: #bbf7d0;
|
|
241
|
+
--review: #b45309; --review-bg: #fffbeb; --review-border: #fde68a;
|
|
242
|
+
--unsafe: #dc2626; --unsafe-bg: #fef2f2; --unsafe-border: #fecaca;
|
|
243
|
+
--radius: 12px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
247
|
+
|
|
248
|
+
body {
|
|
249
|
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
250
|
+
background: linear-gradient(135deg, var(--slate-50) 0%, var(--slate-100) 100%);
|
|
251
|
+
color: var(--slate-800);
|
|
252
|
+
line-height: 1.6;
|
|
253
|
+
min-height: 100vh;
|
|
254
|
+
padding: 2rem;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
258
|
+
|
|
259
|
+
h1 {
|
|
260
|
+
font-family: 'Space Grotesk', sans-serif;
|
|
261
|
+
font-size: 2rem;
|
|
262
|
+
font-weight: 700;
|
|
263
|
+
color: var(--slate-900);
|
|
264
|
+
margin-bottom: 0.5rem;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
h2 {
|
|
268
|
+
font-family: 'Space Grotesk', sans-serif;
|
|
269
|
+
font-size: 1.4rem;
|
|
270
|
+
font-weight: 600;
|
|
271
|
+
color: var(--slate-700);
|
|
272
|
+
margin: 2rem 0 1rem;
|
|
273
|
+
padding-bottom: 0.5rem;
|
|
274
|
+
border-bottom: 2px solid var(--slate-200);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
h3 {
|
|
278
|
+
font-family: 'Space Grotesk', sans-serif;
|
|
279
|
+
font-size: 1.1rem;
|
|
280
|
+
font-weight: 600;
|
|
281
|
+
color: var(--slate-600);
|
|
282
|
+
margin-bottom: 0.5rem;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
h4 {
|
|
286
|
+
font-family: 'Space Grotesk', sans-serif;
|
|
287
|
+
font-size: 0.9rem;
|
|
288
|
+
font-weight: 600;
|
|
289
|
+
color: var(--slate-500);
|
|
290
|
+
margin-bottom: 0.3rem;
|
|
291
|
+
text-transform: uppercase;
|
|
292
|
+
letter-spacing: 0.05em;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* Header */
|
|
296
|
+
.header {
|
|
297
|
+
background: white;
|
|
298
|
+
border-radius: var(--radius);
|
|
299
|
+
padding: 2rem;
|
|
300
|
+
margin-bottom: 2rem;
|
|
301
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
302
|
+
border: 1px solid var(--slate-200);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.header-meta {
|
|
306
|
+
display: flex;
|
|
307
|
+
flex-wrap: wrap;
|
|
308
|
+
gap: 1.5rem;
|
|
309
|
+
margin-top: 1rem;
|
|
310
|
+
font-size: 0.875rem;
|
|
311
|
+
color: var(--slate-500);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.header-meta dt { font-weight: 600; color: var(--slate-600); }
|
|
315
|
+
.header-meta dd { font-family: 'IBM Plex Mono', monospace; }
|
|
316
|
+
|
|
317
|
+
/* Verdict badge */
|
|
318
|
+
.verdict {
|
|
319
|
+
display: inline-block;
|
|
320
|
+
padding: 0.5rem 1.5rem;
|
|
321
|
+
border-radius: 999px;
|
|
322
|
+
font-family: 'Space Grotesk', sans-serif;
|
|
323
|
+
font-weight: 700;
|
|
324
|
+
font-size: 1.1rem;
|
|
325
|
+
letter-spacing: 0.05em;
|
|
326
|
+
color: white;
|
|
327
|
+
margin-top: 1rem;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* Scorecards */
|
|
331
|
+
.scorecards {
|
|
332
|
+
display: grid;
|
|
333
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
334
|
+
gap: 1.5rem;
|
|
335
|
+
margin-bottom: 2rem;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.scorecard {
|
|
339
|
+
background: white;
|
|
340
|
+
border-radius: var(--radius);
|
|
341
|
+
padding: 1.5rem;
|
|
342
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
343
|
+
border: 1px solid var(--slate-200);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.scorecard-value {
|
|
347
|
+
font-family: 'Space Grotesk', sans-serif;
|
|
348
|
+
font-size: 2.5rem;
|
|
349
|
+
font-weight: 700;
|
|
350
|
+
margin: 0.5rem 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.scorecard-label {
|
|
354
|
+
font-size: 0.875rem;
|
|
355
|
+
color: var(--slate-500);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.scorecard-detail {
|
|
359
|
+
font-size: 0.8rem;
|
|
360
|
+
color: var(--slate-500);
|
|
361
|
+
margin-top: 0.25rem;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* Buckets */
|
|
365
|
+
.buckets {
|
|
366
|
+
background: white;
|
|
367
|
+
border-radius: var(--radius);
|
|
368
|
+
padding: 1.5rem;
|
|
369
|
+
margin-bottom: 2rem;
|
|
370
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
371
|
+
border: 1px solid var(--slate-200);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.bucket-row {
|
|
375
|
+
display: flex;
|
|
376
|
+
align-items: center;
|
|
377
|
+
padding: 0.5rem 0;
|
|
378
|
+
border-bottom: 1px solid var(--slate-100);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.bucket-row:last-child { border-bottom: none; }
|
|
382
|
+
|
|
383
|
+
.bucket-name {
|
|
384
|
+
flex: 1;
|
|
385
|
+
font-size: 0.9rem;
|
|
386
|
+
color: var(--slate-700);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.bucket-bar-wrap {
|
|
390
|
+
flex: 2;
|
|
391
|
+
height: 24px;
|
|
392
|
+
background: var(--slate-100);
|
|
393
|
+
border-radius: 4px;
|
|
394
|
+
overflow: hidden;
|
|
395
|
+
margin: 0 1rem;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.bucket-bar {
|
|
399
|
+
height: 100%;
|
|
400
|
+
background: var(--slate-400);
|
|
401
|
+
border-radius: 4px;
|
|
402
|
+
transition: width 0.3s ease;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.bucket-count {
|
|
406
|
+
width: 3rem;
|
|
407
|
+
text-align: right;
|
|
408
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
409
|
+
font-size: 0.875rem;
|
|
410
|
+
font-weight: 600;
|
|
411
|
+
color: var(--slate-600);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* Cards */
|
|
415
|
+
.card {
|
|
416
|
+
background: white;
|
|
417
|
+
border-radius: var(--radius);
|
|
418
|
+
margin-bottom: 0.75rem;
|
|
419
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
420
|
+
border: 1px solid var(--slate-200);
|
|
421
|
+
overflow: hidden;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.card-header {
|
|
425
|
+
display: flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
gap: 0.75rem;
|
|
428
|
+
padding: 1rem 1.5rem;
|
|
429
|
+
cursor: pointer;
|
|
430
|
+
font-size: 0.9rem;
|
|
431
|
+
list-style: none;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.card-header::-webkit-details-marker { display: none; }
|
|
435
|
+
.card-header::before {
|
|
436
|
+
content: '\\25B6';
|
|
437
|
+
font-size: 0.7rem;
|
|
438
|
+
color: var(--slate-400);
|
|
439
|
+
transition: transform 0.2s;
|
|
440
|
+
}
|
|
441
|
+
details[open] > .card-header::before { transform: rotate(90deg); }
|
|
442
|
+
|
|
443
|
+
.card-idx {
|
|
444
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
445
|
+
font-weight: 600;
|
|
446
|
+
color: var(--slate-500);
|
|
447
|
+
min-width: 2.5rem;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.card-bucket {
|
|
451
|
+
font-size: 0.8rem;
|
|
452
|
+
color: var(--slate-500);
|
|
453
|
+
background: var(--slate-100);
|
|
454
|
+
padding: 0.15rem 0.5rem;
|
|
455
|
+
border-radius: 4px;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.card-utterance {
|
|
459
|
+
flex: 1;
|
|
460
|
+
color: var(--slate-700);
|
|
461
|
+
overflow: hidden;
|
|
462
|
+
text-overflow: ellipsis;
|
|
463
|
+
white-space: nowrap;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.card-session {
|
|
467
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
468
|
+
font-size: 0.75rem;
|
|
469
|
+
color: var(--slate-500);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.card-body { padding: 0 1.5rem 1.5rem; }
|
|
473
|
+
|
|
474
|
+
.badge-pass {
|
|
475
|
+
background: var(--safe-bg); color: var(--safe); border: 1px solid var(--safe-border);
|
|
476
|
+
padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600;
|
|
477
|
+
}
|
|
478
|
+
.badge-warn {
|
|
479
|
+
background: var(--review-bg); color: var(--review); border: 1px solid var(--review-border);
|
|
480
|
+
padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600;
|
|
481
|
+
}
|
|
482
|
+
.badge-fail {
|
|
483
|
+
background: var(--unsafe-bg); color: var(--unsafe); border: 1px solid var(--unsafe-border);
|
|
484
|
+
padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/* Flags */
|
|
488
|
+
.flags { margin-bottom: 1rem; display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
|
489
|
+
.flag {
|
|
490
|
+
font-size: 0.8rem; padding: 0.25rem 0.75rem; border-radius: 6px;
|
|
491
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
492
|
+
}
|
|
493
|
+
.flag-error { background: var(--unsafe-bg); color: var(--unsafe); border: 1px solid var(--unsafe-border); }
|
|
494
|
+
.flag-warn { background: var(--review-bg); color: var(--review); border: 1px solid var(--review-border); }
|
|
495
|
+
.flag-info { background: var(--slate-100); color: var(--slate-600); }
|
|
496
|
+
|
|
497
|
+
/* Comparison */
|
|
498
|
+
.comparison {
|
|
499
|
+
display: grid;
|
|
500
|
+
grid-template-columns: 1fr 1fr;
|
|
501
|
+
gap: 1rem;
|
|
502
|
+
margin-bottom: 1rem;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.comp-col {
|
|
506
|
+
background: var(--slate-50);
|
|
507
|
+
border-radius: 8px;
|
|
508
|
+
padding: 1rem;
|
|
509
|
+
border: 1px solid var(--slate-200);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.tool-call { margin-bottom: 0.5rem; }
|
|
513
|
+
.tool-name {
|
|
514
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
515
|
+
font-weight: 600;
|
|
516
|
+
font-size: 0.9rem;
|
|
517
|
+
color: var(--slate-800);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.tool-args {
|
|
521
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
522
|
+
font-size: 0.8rem;
|
|
523
|
+
background: var(--slate-100);
|
|
524
|
+
padding: 0.5rem;
|
|
525
|
+
border-radius: 4px;
|
|
526
|
+
margin-top: 0.25rem;
|
|
527
|
+
overflow-x: auto;
|
|
528
|
+
white-space: pre-wrap;
|
|
529
|
+
word-break: break-word;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.text-resp {
|
|
533
|
+
font-size: 0.85rem;
|
|
534
|
+
color: var(--slate-600);
|
|
535
|
+
font-style: italic;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/* Section */
|
|
539
|
+
.section { margin-bottom: 1rem; }
|
|
540
|
+
|
|
541
|
+
.score-table {
|
|
542
|
+
width: 100%;
|
|
543
|
+
font-size: 0.85rem;
|
|
544
|
+
border-collapse: collapse;
|
|
545
|
+
}
|
|
546
|
+
.score-table td {
|
|
547
|
+
padding: 0.3rem 0.5rem;
|
|
548
|
+
border-bottom: 1px solid var(--slate-100);
|
|
549
|
+
}
|
|
550
|
+
.score-table td:first-child { color: var(--slate-500); width: 60%; }
|
|
551
|
+
.score-table td:last-child { font-family: 'IBM Plex Mono', monospace; font-weight: 500; }
|
|
552
|
+
|
|
553
|
+
/* Transcript */
|
|
554
|
+
.transcript-toggle { margin-top: 1rem; }
|
|
555
|
+
.transcript-toggle > summary {
|
|
556
|
+
cursor: pointer;
|
|
557
|
+
font-size: 0.85rem;
|
|
558
|
+
font-weight: 600;
|
|
559
|
+
color: var(--slate-500);
|
|
560
|
+
padding: 0.5rem 0;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.transcript {
|
|
564
|
+
max-height: 300px;
|
|
565
|
+
overflow-y: auto;
|
|
566
|
+
margin-top: 0.5rem;
|
|
567
|
+
padding: 0.5rem;
|
|
568
|
+
background: var(--slate-50);
|
|
569
|
+
border-radius: 8px;
|
|
570
|
+
border: 1px solid var(--slate-200);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.msg {
|
|
574
|
+
padding: 0.4rem 0.6rem;
|
|
575
|
+
margin-bottom: 0.25rem;
|
|
576
|
+
font-size: 0.8rem;
|
|
577
|
+
border-radius: 4px;
|
|
578
|
+
word-break: break-word;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.msg strong { font-size: 0.75rem; text-transform: uppercase; }
|
|
582
|
+
.msg-system { background: #eff6ff; color: #1e40af; }
|
|
583
|
+
.msg-user { background: #f0fdf4; color: #166534; }
|
|
584
|
+
.msg-assistant { background: #fdf4ff; color: #86198f; }
|
|
585
|
+
.msg-info { background: var(--slate-100); color: var(--slate-500); }
|
|
586
|
+
|
|
587
|
+
code {
|
|
588
|
+
font-family: 'IBM Plex Mono', monospace;
|
|
589
|
+
font-size: 0.85em;
|
|
590
|
+
background: var(--slate-100);
|
|
591
|
+
padding: 0.1rem 0.3rem;
|
|
592
|
+
border-radius: 3px;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/* Pass rows section */
|
|
596
|
+
.pass-summary {
|
|
597
|
+
background: var(--safe-bg);
|
|
598
|
+
border: 1px solid var(--safe-border);
|
|
599
|
+
border-radius: var(--radius);
|
|
600
|
+
padding: 1rem 1.5rem;
|
|
601
|
+
margin-bottom: 1.5rem;
|
|
602
|
+
color: var(--safe);
|
|
603
|
+
font-weight: 500;
|
|
604
|
+
}
|
|
605
|
+
'''
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def build(data):
|
|
609
|
+
"""Build HTML report from replay results JSON data."""
|
|
610
|
+
meta = data.get("metadata", {})
|
|
611
|
+
rows = data.get("rows", [])
|
|
612
|
+
recs = data.get("recs", {})
|
|
613
|
+
|
|
614
|
+
routers = [d for d in rows if _is_router(d)]
|
|
615
|
+
actions = [d for d in rows if not _is_router(d)]
|
|
616
|
+
|
|
617
|
+
topic_score = _score_topic(routers)
|
|
618
|
+
action_strict = _score_action_strict(actions)
|
|
619
|
+
action_lenient = _score_action_lenient(actions)
|
|
620
|
+
|
|
621
|
+
# Overall verdict uses lenient action score (functionally equivalent tools count)
|
|
622
|
+
action_score = action_lenient
|
|
623
|
+
if topic_score is not None and action_score is not None:
|
|
624
|
+
overall = round(0.3 * topic_score + 0.7 * action_score, 1)
|
|
625
|
+
elif action_score is not None:
|
|
626
|
+
overall = action_score
|
|
627
|
+
elif topic_score is not None:
|
|
628
|
+
overall = topic_score
|
|
629
|
+
else:
|
|
630
|
+
overall = None
|
|
631
|
+
|
|
632
|
+
verdict_text, verdict_color = _rating(overall)
|
|
633
|
+
|
|
634
|
+
# A row passes if strict pass, args agreement, OR lenient name match (equivalent tool)
|
|
635
|
+
def _row_passes(d):
|
|
636
|
+
return d.get("PassStrict") or d.get("FunctionArgsAgreement") or d.get("FunctionNameAgreementLenient")
|
|
637
|
+
|
|
638
|
+
failing = [d for d in rows if not _row_passes(d)]
|
|
639
|
+
bucket_counts = Counter()
|
|
640
|
+
for d in failing:
|
|
641
|
+
name_hit = d.get("FunctionNameAgreement", False)
|
|
642
|
+
bucket_counts[_bucket(d, name_hit)] += 1
|
|
643
|
+
max_bucket = max(bucket_counts.values()) if bucket_counts else 1
|
|
644
|
+
|
|
645
|
+
passing = [d for d in rows if _row_passes(d)]
|
|
646
|
+
|
|
647
|
+
# Build HTML
|
|
648
|
+
parts = []
|
|
649
|
+
parts.append(f'''<!DOCTYPE html>
|
|
650
|
+
<html lang="en">
|
|
651
|
+
<head>
|
|
652
|
+
<meta charset="utf-8">
|
|
653
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
654
|
+
<title>Offline Analysis Report</title>
|
|
655
|
+
<style>{CSS}</style>
|
|
656
|
+
</head>
|
|
657
|
+
<body>
|
|
658
|
+
<div class="container">
|
|
659
|
+
''')
|
|
660
|
+
|
|
661
|
+
# Header
|
|
662
|
+
parts.append(f'''
|
|
663
|
+
<div class="header">
|
|
664
|
+
<h1>Offline Analysis Report</h1>
|
|
665
|
+
<div class="verdict" style="background:{verdict_color}">{verdict_text}</div>
|
|
666
|
+
<dl class="header-meta">
|
|
667
|
+
<div><dt>Baseline Model</dt><dd>{_esc(meta.get("baseline_model", "N/A"))}</dd></div>
|
|
668
|
+
<div><dt>Replay Model</dt><dd>{_esc(meta.get("replay_model", "N/A"))}</dd></div>
|
|
669
|
+
<div><dt>Org ID</dt><dd>{_esc(meta.get("org_id", "N/A"))}</dd></div>
|
|
670
|
+
<div><dt>Agent</dt><dd>{_esc(meta.get("agent_api_name", "N/A") or "N/A")}</dd></div>
|
|
671
|
+
<div><dt>Total Rows</dt><dd>{meta.get("total_rows", len(rows))}</dd></div>
|
|
672
|
+
<div><dt>Timestamp</dt><dd>{_esc(meta.get("timestamp", ""))}</dd></div>
|
|
673
|
+
</dl>
|
|
674
|
+
</div>
|
|
675
|
+
''')
|
|
676
|
+
|
|
677
|
+
# Scorecards
|
|
678
|
+
parts.append('<div class="scorecards">')
|
|
679
|
+
|
|
680
|
+
if routers:
|
|
681
|
+
t_verdict, t_color = _rating(topic_score)
|
|
682
|
+
parts.append(f'''
|
|
683
|
+
<div class="scorecard">
|
|
684
|
+
<h3>Routing</h3>
|
|
685
|
+
<div class="scorecard-value" style="color:{t_color}">{topic_score}%</div>
|
|
686
|
+
<div class="scorecard-label">Topic Agreement (Lenient)</div>
|
|
687
|
+
<div class="scorecard-detail">{len(routers)} routing rows · {t_verdict}</div>
|
|
688
|
+
</div>''')
|
|
689
|
+
|
|
690
|
+
if actions:
|
|
691
|
+
a_verdict, a_color = _rating(action_lenient)
|
|
692
|
+
as_verdict, as_color = _rating(action_strict)
|
|
693
|
+
strict_pass = sum(1 for d in actions if d.get("PassStrict"))
|
|
694
|
+
args_agree = sum(1 for d in actions if d.get("FunctionArgsAgreement"))
|
|
695
|
+
name_exact = sum(1 for d in actions if d.get("FunctionNameAgreement"))
|
|
696
|
+
name_lenient = sum(1 for d in actions if d.get("FunctionNameAgreementLenient"))
|
|
697
|
+
parts.append(f'''
|
|
698
|
+
<div class="scorecard">
|
|
699
|
+
<h3>Action Tools (Lenient)</h3>
|
|
700
|
+
<div class="scorecard-value" style="color:{a_color}">{action_lenient}%</div>
|
|
701
|
+
<div class="scorecard-label">Includes functionally equivalent tools</div>
|
|
702
|
+
<div class="scorecard-detail">{len(actions)} action rows · {a_verdict}</div>
|
|
703
|
+
<div class="scorecard-detail">{name_lenient}/{len(actions)} tool match (lenient) · {name_exact}/{len(actions)} exact</div>
|
|
704
|
+
</div>''')
|
|
705
|
+
parts.append(f'''
|
|
706
|
+
<div class="scorecard">
|
|
707
|
+
<h3>Action Tools (Strict)</h3>
|
|
708
|
+
<div class="scorecard-value" style="color:{as_color}">{action_strict}%</div>
|
|
709
|
+
<div class="scorecard-label">Exact name + args agreement</div>
|
|
710
|
+
<div class="scorecard-detail">{strict_pass} strict pass · {args_agree} args agree</div>
|
|
711
|
+
</div>''')
|
|
712
|
+
|
|
713
|
+
# Overall scorecard
|
|
714
|
+
o_verdict, o_color = _rating(overall)
|
|
715
|
+
parts.append(f'''
|
|
716
|
+
<div class="scorecard">
|
|
717
|
+
<h3>Overall</h3>
|
|
718
|
+
<div class="scorecard-value" style="color:{o_color}">{overall if overall is not None else "N/A"}%</div>
|
|
719
|
+
<div class="scorecard-label">Weighted Score</div>
|
|
720
|
+
<div class="scorecard-detail">{len(rows)} total rows · {len(passing)} pass · {len(failing)} fail</div>
|
|
721
|
+
</div>''')
|
|
722
|
+
|
|
723
|
+
parts.append('</div>') # end scorecards
|
|
724
|
+
|
|
725
|
+
# Failure buckets
|
|
726
|
+
if bucket_counts:
|
|
727
|
+
parts.append('<div class="buckets"><h3>Failure Breakdown</h3>')
|
|
728
|
+
for bucket_name, count in bucket_counts.most_common():
|
|
729
|
+
pct = round(100 * count / max_bucket)
|
|
730
|
+
parts.append(f'''
|
|
731
|
+
<div class="bucket-row">
|
|
732
|
+
<span class="bucket-name">{_esc(bucket_name)}</span>
|
|
733
|
+
<div class="bucket-bar-wrap"><div class="bucket-bar" style="width:{pct}%"></div></div>
|
|
734
|
+
<span class="bucket-count">{count}</span>
|
|
735
|
+
</div>''')
|
|
736
|
+
parts.append('</div>')
|
|
737
|
+
|
|
738
|
+
# Passing rows summary
|
|
739
|
+
if passing:
|
|
740
|
+
parts.append(f'<div class="pass-summary">{len(passing)} row(s) passed (strict pass or args agreement)</div>')
|
|
741
|
+
|
|
742
|
+
# Failing row cards
|
|
743
|
+
if failing:
|
|
744
|
+
parts.append(f'<h2>Failing Rows ({len(failing)})</h2>')
|
|
745
|
+
for d in failing:
|
|
746
|
+
idx = d.get("SessionRowIndex", "?")
|
|
747
|
+
rec = recs.get(str(idx), {})
|
|
748
|
+
parts.append(_card(d, rec, idx))
|
|
749
|
+
|
|
750
|
+
# All passing row cards (collapsed section)
|
|
751
|
+
if passing:
|
|
752
|
+
parts.append(f'''
|
|
753
|
+
<details style="margin-top:1.5rem">
|
|
754
|
+
<summary style="cursor:pointer;font-family:'Space Grotesk',sans-serif;font-weight:600;color:var(--slate-500);font-size:1.1rem;padding:0.5rem 0">
|
|
755
|
+
Passing Rows ({len(passing)})
|
|
756
|
+
</summary>
|
|
757
|
+
<div style="margin-top:0.5rem">''')
|
|
758
|
+
for d in passing:
|
|
759
|
+
idx = d.get("SessionRowIndex", "?")
|
|
760
|
+
rec = recs.get(str(idx), {})
|
|
761
|
+
parts.append(_card(d, rec, idx))
|
|
762
|
+
parts.append('</div></details>')
|
|
763
|
+
|
|
764
|
+
parts.append('''
|
|
765
|
+
</div>
|
|
766
|
+
</body>
|
|
767
|
+
</html>''')
|
|
768
|
+
|
|
769
|
+
return "\n".join(parts)
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
# ---------------------------------------------------------------------------
|
|
773
|
+
# CLI
|
|
774
|
+
# ---------------------------------------------------------------------------
|
|
775
|
+
|
|
776
|
+
def main():
|
|
777
|
+
input_path = sys.argv[1] if len(sys.argv) > 1 else "replay_results.json"
|
|
778
|
+
output_path = sys.argv[2] if len(sys.argv) > 2 else "offline_analysis_report.html"
|
|
779
|
+
|
|
780
|
+
with open(input_path) as f:
|
|
781
|
+
data = json.load(f)
|
|
782
|
+
|
|
783
|
+
html = build(data)
|
|
784
|
+
|
|
785
|
+
with open(output_path, "w") as f:
|
|
786
|
+
f.write(html)
|
|
787
|
+
|
|
788
|
+
rows = data.get("rows", [])
|
|
789
|
+
passing = sum(1 for d in rows
|
|
790
|
+
if d.get("PassStrict") or d.get("FunctionArgsAgreement") or d.get("FunctionNameAgreementLenient"))
|
|
791
|
+
print(f"Report generated: {output_path}")
|
|
792
|
+
print(f" {len(rows)} rows, {passing} passing, {len(rows) - passing} failing")
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
if __name__ == "__main__":
|
|
796
|
+
main()
|