@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.
Files changed (138) hide show
  1. package/package.json +1 -1
  2. package/skills/agentforce-offline-experimentation-run/SKILL.md +253 -0
  3. package/skills/agentforce-offline-experimentation-run/scripts/build_report.py +796 -0
  4. package/skills/agentforce-offline-experimentation-run/scripts/resolve-token.sh +111 -0
  5. package/skills/agentforce-offline-experimentation-run/scripts/sfap_replay.py +1053 -0
  6. package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +21 -11
  7. package/skills/automation-sandbox-post-copy-config-generate/assets/config_template.json +10 -0
  8. package/skills/automation-sandbox-post-copy-config-generate/assets/json_schema.json +33 -1
  9. package/skills/automation-sandbox-post-copy-config-generate/references/configuration_catalog.md +44 -1
  10. package/skills/automation-sandbox-post-copy-configure/SKILL.md +132 -124
  11. package/skills/automation-sandbox-post-copy-configure/references/flow_activation_path.md +151 -0
  12. package/skills/automation-sandbox-post-copy-configure/references/scheduled_apex_path.md +55 -9
  13. package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +54 -107
  14. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/copy.json +1 -1
  15. package/skills/consumer-goods-promotion-bo-api-deploy/references/conventions-and-payload-rules.md +82 -5
  16. package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +35 -2
  17. package/skills/consumer-goods-promotion-bo-api-deploy/references/preflight-and-seed-queries.md +149 -0
  18. package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +17 -1
  19. package/skills/dx-devops-conflict-resolve/SKILL.md +1 -5
  20. package/skills/dx-devops-pipeline-manage/SKILL.md +1 -5
  21. package/skills/dx-devops-project-manage/SKILL.md +1 -5
  22. package/skills/dx-devops-promote/SKILL.md +1 -5
  23. package/skills/dx-devops-request-status/SKILL.md +1 -5
  24. package/skills/dx-lifecycle-center/SKILL.md +120 -0
  25. package/skills/dx-lifecycle-center/examples/workflows.md +175 -0
  26. package/skills/dx-lifecycle-center/references/connect-api.md +123 -0
  27. package/skills/dx-lifecycle-center/references/connect-org.md +184 -0
  28. package/skills/dx-lifecycle-center/references/deploy.md +153 -0
  29. package/skills/dx-lifecycle-center/references/error-handling.md +21 -0
  30. package/skills/dx-lifecycle-center/references/hub-org.md +37 -0
  31. package/skills/dx-lifecycle-center/references/list-orgs.md +99 -0
  32. package/skills/dx-lifecycle-center/references/request-body.md +75 -0
  33. package/skills/dx-lifecycle-center/references/troubleshooting.md +31 -0
  34. package/skills/dx-lifecycle-center/scripts/poll-deploy-status.sh +200 -0
  35. package/skills/dx-lifecycle-center/scripts/resolve-environment.sh +120 -0
  36. package/skills/dx-org-devhub-configure/SKILL.md +1 -3
  37. package/skills/dx-org-shape-manage/SKILL.md +1 -5
  38. package/skills/dx-org-snapshot-manage/SKILL.md +1 -3
  39. package/skills/education-cloud-academic-calendar-generate/SKILL.md +1 -3
  40. package/skills/education-cloud-course-catalog-migrate/SKILL.md +1 -3
  41. package/skills/education-cloud-domain-configure/SKILL.md +1 -3
  42. package/skills/education-cloud-multi-campus-configure/SKILL.md +1 -3
  43. package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +1 -5
  44. package/skills/experience-ui-bundle-2gp-deploy/SKILL.md +1 -3
  45. package/skills/experience-ui-bundle-features-generate/SKILL.md +1 -5
  46. package/skills/experience-ui-bundle-mfa-configure/SKILL.md +1 -3
  47. package/skills/field-service-data-capture-migrate/SKILL.md +1 -3
  48. package/skills/insurance-brokerage-agency-billing-configure/SKILL.md +1 -3
  49. package/skills/marketing-message-test/SKILL.md +1 -3
  50. package/skills/nonprofit-fundraising-configure/SKILL.md +240 -0
  51. package/skills/nonprofit-fundraising-configure/references/gotchas.md +26 -0
  52. package/skills/nonprofit-fundraising-configure/references/mcp-invocation.md +678 -0
  53. package/skills/nonprofit-fundraising-configure/references/permission-targeting.md +35 -0
  54. package/skills/nonprofit-volunteer-management-model-get/SKILL.md +166 -0
  55. package/skills/nonprofit-volunteer-management-model-get/references/entities.md +315 -0
  56. package/skills/nonprofit-volunteer-management-model-get/references/relationships.md +175 -0
  57. package/skills/nonprofit-volunteer-management-model-get/references/themes.md +143 -0
  58. package/skills/nonprofit-volunteer-management-records-create/SKILL.md +121 -0
  59. package/skills/nonprofit-volunteer-management-records-create/references/field-guide.md +284 -0
  60. package/skills/nonprofit-volunteer-management-records-create/references/mcp-invocation.md +248 -0
  61. package/skills/nonprofit-volunteer-management-records-create/references/review-format.md +140 -0
  62. package/skills/nonprofit-volunteer-management-records-create/scripts/expand-series.py +632 -0
  63. package/skills/nonprofit-volunteer-management-records-create/scripts/render-review-table.py +419 -0
  64. package/skills/nonprofit-volunteer-management-records-create/scripts/tests/test_expand_series.py +690 -0
  65. package/skills/nonprofit-volunteer-management-records-create/scripts/tests/test_render_review_table.py +421 -0
  66. package/skills/nonprofit-volunteer-management-structure-generate/SKILL.md +119 -0
  67. package/skills/nonprofit-volunteer-management-structure-generate/references/configuration-decisions.md +159 -0
  68. package/skills/nonprofit-volunteer-management-structure-generate/references/discovery-interview.md +137 -0
  69. package/skills/nonprofit-volunteer-management-structure-generate/references/edge-cases.md +48 -0
  70. package/skills/nonprofit-volunteer-management-structure-generate/references/mcp-invocation.md +75 -0
  71. package/skills/nonprofit-volunteer-management-structure-generate/references/plan-output.md +141 -0
  72. package/skills/platform-custom-field-generate/SKILL.md +1 -0
  73. package/skills/platform-custom-field-generate/references/advanced-picklists.md +5 -5
  74. package/skills/platform-custom-metadata-type-generate/SKILL.md +3 -0
  75. package/skills/platform-custom-object-generate/SKILL.md +4 -0
  76. package/skills/platform-custom-setting-generate/SKILL.md +3 -0
  77. package/skills/platform-datamask-run/SKILL.md +1 -5
  78. package/skills/platform-permission-set-generate/SKILL.md +3 -0
  79. package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +1 -3
  80. package/skills/platform-sandbox-configure/SKILL.md +1 -3
  81. package/skills/platform-value-set-generate/SKILL.md +3 -0
  82. package/skills/sales-call-scoring-configure/SKILL.md +1 -5
  83. package/skills/service-agentforce-channel-configure/SKILL.md +1 -3
  84. package/skills/service-agentforce-human-escalation-configure/SKILL.md +1 -3
  85. package/skills/service-catalog-template-coordinate/SKILL.md +1 -3
  86. package/skills/service-concierge-portal-generate/SKILL.md +1 -3
  87. package/skills/service-email-to-case-configure/SKILL.md +1 -5
  88. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/SKILL.md +1 -3
  89. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +1 -3
  90. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +1 -3
  91. package/skills/service-itsm-agentic-setup-agentforce-studio-validate/SKILL.md +1 -3
  92. package/skills/service-itsm-agentic-setup-cmdb-access-assign/SKILL.md +1 -3
  93. package/skills/service-itsm-agentic-setup-cmdb-bundle-deploy/SKILL.md +1 -5
  94. package/skills/service-itsm-agentic-setup-cmdb-configure/SKILL.md +1 -3
  95. package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +1 -3
  96. package/skills/service-itsm-agentic-setup-cmdb-discovery-configure/SKILL.md +1 -3
  97. package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +1 -3
  98. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +1 -3
  99. package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +1 -0
  100. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +1 -0
  101. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/SKILL.md +1 -3
  102. package/skills/service-itsm-agentic-setup-uel-user-create/SKILL.md +1 -9
  103. package/skills/service-itsm-incident-mgmt-configure/SKILL.md +1 -5
  104. package/skills/service-itsm-incident-priority-configure/SKILL.md +1 -3
  105. package/skills/service-itsm-slack-itservice-configure/SKILL.md +1 -3
  106. package/skills/service-itsm-swarming-configure/SKILL.md +1 -3
  107. package/skills/service-itsm-teams-configure/SKILL.md +1 -3
  108. package/skills/service-itsm-teams-debug/SKILL.md +1 -3
  109. package/skills/service-itsm-teams-employee-agent-configure/SKILL.md +1 -3
  110. package/skills/service-itsm-teams-itdesk-configure/SKILL.md +1 -3
  111. package/skills/service-itsm-teams-itservice-configure/SKILL.md +1 -3
  112. package/skills/service-native-voice-recording-transcription-configure/SKILL.md +1 -3
  113. package/skills/service-omni-agent-users-create/SKILL.md +1 -3
  114. package/skills/service-omni-agent-work-sharing-configure/SKILL.md +1 -3
  115. package/skills/service-omni-attribute-routing-configure/SKILL.md +1 -3
  116. package/skills/service-omni-base-settings-configure/SKILL.md +1 -3
  117. package/skills/service-omni-channel-inventory-analyze/SKILL.md +1 -3
  118. package/skills/service-omni-channel-limits-analyze/SKILL.md +1 -3
  119. package/skills/service-omni-channel-setup-coordinate/SKILL.md +1 -3
  120. package/skills/service-omni-command-center-analyze/SKILL.md +1 -3
  121. package/skills/service-omni-command-center-configure/SKILL.md +1 -3
  122. package/skills/service-omni-permission-set-assign/SKILL.md +1 -3
  123. package/skills/service-omni-presence-status-deploy/SKILL.md +1 -3
  124. package/skills/service-omni-presence-user-config-deploy/SKILL.md +1 -3
  125. package/skills/service-omni-queue-deploy/SKILL.md +1 -3
  126. package/skills/service-omni-queue-members-assign/SKILL.md +1 -3
  127. package/skills/service-omni-queue-routing-config-deploy/SKILL.md +1 -3
  128. package/skills/service-omni-routing-flow-deploy/SKILL.md +1 -3
  129. package/skills/service-omni-service-channel-configure/SKILL.md +1 -3
  130. package/skills/service-omni-sidebar-configure/SKILL.md +1 -3
  131. package/skills/service-omni-skills-based-routing-configure/SKILL.md +1 -3
  132. package/skills/service-omni-supervisor-config-deploy/SKILL.md +1 -3
  133. package/skills/service-omni-supervisor-permset-assign/SKILL.md +1 -3
  134. package/skills/service-omni-supervisor-surface-deploy/SKILL.md +1 -3
  135. package/skills/service-omni-supervisor-users-create/SKILL.md +1 -3
  136. package/skills/service-omni-work-skill-routing-configure/SKILL.md +1 -3
  137. package/skills/field-service-setup-orchestrator-get/SKILL.md +0 -47
  138. 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 &middot; {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 &middot; {a_verdict}</div>
703
+ <div class="scorecard-detail">{name_lenient}/{len(actions)} tool match (lenient) &middot; {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 &middot; {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 &middot; {len(passing)} pass &middot; {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()