google-cloud-agentplatform 1.165.1.dev0__py3-none-any.whl
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.
- agentplatform/__init__.py +72 -0
- agentplatform/_genai/__init__.py +43 -0
- agentplatform/_genai/_agent_engines_utils.py +2341 -0
- agentplatform/_genai/_bigquery_utils.py +49 -0
- agentplatform/_genai/_datasets_utils.py +344 -0
- agentplatform/_genai/_evals_builtin_tools.py +209 -0
- agentplatform/_genai/_evals_common.py +4268 -0
- agentplatform/_genai/_evals_constant.py +122 -0
- agentplatform/_genai/_evals_data_converters.py +926 -0
- agentplatform/_genai/_evals_metric_handlers.py +1783 -0
- agentplatform/_genai/_evals_metric_loaders.py +401 -0
- agentplatform/_genai/_evals_utils.py +1043 -0
- agentplatform/_genai/_evals_visualization.py +2070 -0
- agentplatform/_genai/_gcs_utils.py +262 -0
- agentplatform/_genai/_logging_utils.py +47 -0
- agentplatform/_genai/_memory_bank_utils.py +206 -0
- agentplatform/_genai/_observability_data_converter.py +186 -0
- agentplatform/_genai/_operations_utils.py +94 -0
- agentplatform/_genai/_prompt_management_utils.py +147 -0
- agentplatform/_genai/_prompt_optimizer_utils.py +215 -0
- agentplatform/_genai/_skills_utils.py +69 -0
- agentplatform/_genai/_transformers.py +628 -0
- agentplatform/_genai/a2a_task_events.py +509 -0
- agentplatform/_genai/a2a_tasks.py +861 -0
- agentplatform/_genai/agent_engines.py +3931 -0
- agentplatform/_genai/client.py +519 -0
- agentplatform/_genai/datasets.py +3045 -0
- agentplatform/_genai/endpoints.py +1149 -0
- agentplatform/_genai/evals.py +6883 -0
- agentplatform/_genai/example_stores.py +1445 -0
- agentplatform/_genai/feedback_contexts.py +700 -0
- agentplatform/_genai/feedback_entries.py +1644 -0
- agentplatform/_genai/live.py +64 -0
- agentplatform/_genai/live_agent_engines.py +179 -0
- agentplatform/_genai/memories.py +2962 -0
- agentplatform/_genai/memory_banks.py +1927 -0
- agentplatform/_genai/memory_revisions.py +465 -0
- agentplatform/_genai/model_garden.py +2638 -0
- agentplatform/_genai/prompt_optimizer.py +995 -0
- agentplatform/_genai/prompts.py +4515 -0
- agentplatform/_genai/rag.py +4961 -0
- agentplatform/_genai/runtime_revisions.py +1257 -0
- agentplatform/_genai/runtimes.py +78 -0
- agentplatform/_genai/sandbox_snapshots.py +1015 -0
- agentplatform/_genai/sandbox_templates.py +1088 -0
- agentplatform/_genai/sandboxes.py +1604 -0
- agentplatform/_genai/session_events.py +543 -0
- agentplatform/_genai/sessions.py +1449 -0
- agentplatform/_genai/skill_revisions.py +377 -0
- agentplatform/_genai/skills.py +1708 -0
- agentplatform/_genai/types/__init__.py +4695 -0
- agentplatform/_genai/types/agent_engines.py +16 -0
- agentplatform/_genai/types/common.py +32784 -0
- agentplatform/_genai/types/evals.py +1031 -0
- agentplatform/_genai/types/prompt_optimizer.py +107 -0
- agentplatform/_genai/types/prompts.py +107 -0
- agentplatform/version.py +17 -0
- google_cloud_agentplatform-1.165.1.dev0.dist-info/METADATA +79 -0
- google_cloud_agentplatform-1.165.1.dev0.dist-info/RECORD +62 -0
- google_cloud_agentplatform-1.165.1.dev0.dist-info/WHEEL +5 -0
- google_cloud_agentplatform-1.165.1.dev0.dist-info/licenses/LICENSE +202 -0
- google_cloud_agentplatform-1.165.1.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2070 @@
|
|
|
1
|
+
# Copyright 2025 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
"""Visualization utilities for GenAI Evaluation SDK."""
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
import datetime
|
|
19
|
+
import html
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import textwrap
|
|
23
|
+
from typing import Any, Optional
|
|
24
|
+
|
|
25
|
+
import pandas as pd
|
|
26
|
+
from pydantic import errors
|
|
27
|
+
|
|
28
|
+
from . import _evals_common
|
|
29
|
+
from . import types
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_ipython_env() -> bool:
|
|
36
|
+
"""Checks if the code is running in an IPython environment."""
|
|
37
|
+
try:
|
|
38
|
+
from IPython import get_ipython
|
|
39
|
+
|
|
40
|
+
return get_ipython() is not None
|
|
41
|
+
except ImportError:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _pydantic_serializer(obj: Any) -> Any:
|
|
46
|
+
"""Custom serializer for Pydantic models."""
|
|
47
|
+
if hasattr(obj, "model_dump"):
|
|
48
|
+
return obj.model_dump(mode="json")
|
|
49
|
+
if isinstance(obj, datetime.datetime):
|
|
50
|
+
return obj.isoformat()
|
|
51
|
+
if isinstance(obj, bytes):
|
|
52
|
+
return base64.b64encode(obj).decode("utf-8")
|
|
53
|
+
raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _preprocess_df_for_json(df: Optional[pd.DataFrame]) -> Optional[pd.DataFrame]:
|
|
57
|
+
"""Prepares a DataFrame for JSON serialization by converting complex objects to strings."""
|
|
58
|
+
if df is None:
|
|
59
|
+
return None
|
|
60
|
+
df_copy = df.copy()
|
|
61
|
+
|
|
62
|
+
for col in df_copy.columns:
|
|
63
|
+
if (
|
|
64
|
+
df_copy[col].dtype == "object"
|
|
65
|
+
or df_copy[col].apply(lambda x: isinstance(x, (dict, list))).any()
|
|
66
|
+
):
|
|
67
|
+
|
|
68
|
+
def stringify_cell(cell: Any) -> Optional[str]:
|
|
69
|
+
if isinstance(cell, (dict, list)):
|
|
70
|
+
try:
|
|
71
|
+
return json.dumps(
|
|
72
|
+
cell, ensure_ascii=False, default=_pydantic_serializer
|
|
73
|
+
)
|
|
74
|
+
except TypeError:
|
|
75
|
+
return str(cell)
|
|
76
|
+
elif pd.isna(cell):
|
|
77
|
+
return None
|
|
78
|
+
elif not isinstance(cell, (str, int, float, bool)):
|
|
79
|
+
if hasattr(cell, "model_dump"):
|
|
80
|
+
return json.dumps(
|
|
81
|
+
cell.model_dump(mode="json"), ensure_ascii=False
|
|
82
|
+
)
|
|
83
|
+
return str(cell)
|
|
84
|
+
return str(cell)
|
|
85
|
+
|
|
86
|
+
df_copy[col] = df_copy[col].apply(stringify_cell)
|
|
87
|
+
return df_copy
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _encode_to_base64(data: str) -> str:
|
|
91
|
+
"""Encodes a string to a web-safe Base64 string."""
|
|
92
|
+
return base64.b64encode(data.encode("utf-8")).decode("utf-8")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _extract_text_and_raw_json(content: Any) -> dict[str, str]:
|
|
96
|
+
"""Extracts display text and raw JSON from a content object."""
|
|
97
|
+
if hasattr(content, "model_dump"):
|
|
98
|
+
content = content.model_dump(mode="json", exclude_none=True)
|
|
99
|
+
|
|
100
|
+
if not isinstance(content, (str, dict)):
|
|
101
|
+
return {"display_text": str(content or ""), "raw_json": ""}
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
data = json.loads(content) if isinstance(content, str) else content
|
|
105
|
+
|
|
106
|
+
if not isinstance(data, dict):
|
|
107
|
+
return {"display_text": str(content), "raw_json": ""}
|
|
108
|
+
|
|
109
|
+
pretty_json = json.dumps(data, indent=2, ensure_ascii=False)
|
|
110
|
+
|
|
111
|
+
# Gemini format check (API Wrapper format).
|
|
112
|
+
if (
|
|
113
|
+
"contents" in data
|
|
114
|
+
and isinstance(data.get("contents"), list)
|
|
115
|
+
and data["contents"]
|
|
116
|
+
):
|
|
117
|
+
first_part = data["contents"][0].get("parts", [{}])[0]
|
|
118
|
+
display_text = first_part.get("text", str(data))
|
|
119
|
+
return {"display_text": display_text, "raw_json": pretty_json}
|
|
120
|
+
|
|
121
|
+
# Direct Gemini Content Object Check
|
|
122
|
+
elif "parts" in data and isinstance(data.get("parts"), list) and data["parts"]:
|
|
123
|
+
text_parts = [p.get("text", "") for p in data["parts"] if "text" in p]
|
|
124
|
+
display_text = "\n".join(text_parts) if text_parts else str(data)
|
|
125
|
+
return {"display_text": display_text, "raw_json": pretty_json}
|
|
126
|
+
|
|
127
|
+
# OpenAI response format check.
|
|
128
|
+
elif (
|
|
129
|
+
"choices" in data
|
|
130
|
+
and isinstance(data.get("choices"), list)
|
|
131
|
+
and data["choices"]
|
|
132
|
+
):
|
|
133
|
+
message = data["choices"][0].get("message", {})
|
|
134
|
+
display_text = message.get("content", str(data))
|
|
135
|
+
return {"display_text": display_text, "raw_json": pretty_json}
|
|
136
|
+
|
|
137
|
+
# OpenAI request format check.
|
|
138
|
+
elif (
|
|
139
|
+
"messages" in data
|
|
140
|
+
and isinstance(data.get("messages"), list)
|
|
141
|
+
and data["messages"]
|
|
142
|
+
):
|
|
143
|
+
user_messages = [
|
|
144
|
+
message.get("content", "")
|
|
145
|
+
for message in data["messages"]
|
|
146
|
+
if message.get("role") == "user"
|
|
147
|
+
]
|
|
148
|
+
display_text = user_messages[-1] if user_messages else str(data)
|
|
149
|
+
return {"display_text": display_text, "raw_json": pretty_json}
|
|
150
|
+
else:
|
|
151
|
+
# Not a recognized format.
|
|
152
|
+
return {"display_text": str(content), "raw_json": pretty_json}
|
|
153
|
+
|
|
154
|
+
except (json.JSONDecodeError, TypeError, IndexError):
|
|
155
|
+
return {"display_text": str(content), "raw_json": ""}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _extract_dataset_rows(dataset: types.EvaluationDataset) -> list[dict[str, Any]]:
|
|
159
|
+
"""Helper to consistently extract rows from either a dataframe or raw eval_cases list."""
|
|
160
|
+
processed_rows = []
|
|
161
|
+
|
|
162
|
+
# Process from DataFrame if available
|
|
163
|
+
if getattr(dataset, "eval_dataset_df", None) is not None:
|
|
164
|
+
processed_df = _preprocess_df_for_json(dataset.eval_dataset_df)
|
|
165
|
+
if processed_df is not None:
|
|
166
|
+
for _, row in processed_df.iterrows():
|
|
167
|
+
prompt_key = "request" if "request" in row else "prompt"
|
|
168
|
+
prompt_info = _extract_text_and_raw_json(row.get(prompt_key))
|
|
169
|
+
response_info = _extract_text_and_raw_json(row.get("response"))
|
|
170
|
+
ref_info = _extract_text_and_raw_json(row.get("reference"))
|
|
171
|
+
processed_row = {
|
|
172
|
+
"prompt_display_text": prompt_info["display_text"],
|
|
173
|
+
"prompt_raw_json": prompt_info["raw_json"],
|
|
174
|
+
"reference": ref_info["display_text"],
|
|
175
|
+
"reference_raw_json": ref_info["raw_json"],
|
|
176
|
+
"response_display_text": response_info["display_text"],
|
|
177
|
+
"response_raw_json": response_info["raw_json"],
|
|
178
|
+
"intermediate_events": row.get("intermediate_events", None),
|
|
179
|
+
"agent_data": row.get("agent_data", None),
|
|
180
|
+
}
|
|
181
|
+
processed_rows.append(processed_row)
|
|
182
|
+
|
|
183
|
+
# Fallback to pure eval_cases extraction
|
|
184
|
+
elif dataset.eval_cases:
|
|
185
|
+
for case in dataset.eval_cases:
|
|
186
|
+
prompt_info = (
|
|
187
|
+
_extract_text_and_raw_json(case.prompt)
|
|
188
|
+
if case.prompt
|
|
189
|
+
else {"display_text": "", "raw_json": ""}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
response_info = {"display_text": "", "raw_json": ""}
|
|
193
|
+
if case.responses and case.responses[0].response:
|
|
194
|
+
response_info = _extract_text_and_raw_json(case.responses[0].response)
|
|
195
|
+
|
|
196
|
+
reference_text = ""
|
|
197
|
+
reference_raw_json = ""
|
|
198
|
+
if case.reference and case.reference.response:
|
|
199
|
+
ref_info = _extract_text_and_raw_json(case.reference.response)
|
|
200
|
+
reference_text = ref_info["display_text"]
|
|
201
|
+
reference_raw_json = ref_info["raw_json"]
|
|
202
|
+
|
|
203
|
+
agent_data_json = None
|
|
204
|
+
if case.agent_data:
|
|
205
|
+
agent_data_json = json.dumps(
|
|
206
|
+
case.agent_data.model_dump(mode="json", exclude_none=True),
|
|
207
|
+
ensure_ascii=False,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
intermediate_events_json = None
|
|
211
|
+
if case.intermediate_events:
|
|
212
|
+
intermediate_events_json = json.dumps(
|
|
213
|
+
[
|
|
214
|
+
e.model_dump(mode="json", exclude_none=True)
|
|
215
|
+
for e in case.intermediate_events
|
|
216
|
+
],
|
|
217
|
+
ensure_ascii=False,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
processed_row = {
|
|
221
|
+
"prompt_display_text": prompt_info["display_text"],
|
|
222
|
+
"prompt_raw_json": prompt_info["raw_json"],
|
|
223
|
+
"reference": reference_text,
|
|
224
|
+
"reference_raw_json": reference_raw_json,
|
|
225
|
+
"response_display_text": response_info["display_text"],
|
|
226
|
+
"response_raw_json": response_info["raw_json"],
|
|
227
|
+
"intermediate_events": intermediate_events_json,
|
|
228
|
+
"agent_data": agent_data_json,
|
|
229
|
+
}
|
|
230
|
+
processed_rows.append(processed_row)
|
|
231
|
+
|
|
232
|
+
return processed_rows
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def get_evaluation_html(eval_result_json: str) -> str:
|
|
236
|
+
"""Returns a self-contained HTML for single evaluation visualization."""
|
|
237
|
+
payload_b64 = _encode_to_base64(eval_result_json)
|
|
238
|
+
return textwrap.dedent(
|
|
239
|
+
f"""
|
|
240
|
+
<!DOCTYPE html>
|
|
241
|
+
<html>
|
|
242
|
+
<head>
|
|
243
|
+
<meta charset="UTF-8">
|
|
244
|
+
<title>Evaluation Report</title>
|
|
245
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
246
|
+
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
|
247
|
+
<style>
|
|
248
|
+
body {{ font-family: 'Roboto', 'Helvetica', sans-serif; margin: 2em; background-color: #f8f9fa; color: #202124; }}
|
|
249
|
+
.container {{ max-width: 1200px; margin: 20px auto; padding: 20px; background-color: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }}
|
|
250
|
+
h1, h2, h3 {{ color: #3c4043; }}
|
|
251
|
+
h1 {{ border-bottom: 2px solid #4285F4; padding-bottom: 8px; }}
|
|
252
|
+
h2 {{ border-bottom: 1px solid #dadce0; padding-bottom: 8px; }}
|
|
253
|
+
table {{ border-collapse: collapse; width: 100%; margin: 1em 0; }}
|
|
254
|
+
th, td {{ border: 1px solid #dadce0; padding: 12px; text-align: left; vertical-align: top; }}
|
|
255
|
+
th {{ background-color: #f2f2f2; font-weight: 500; }}
|
|
256
|
+
details {{ border: 1px solid #dadce0; border-radius: 8px; padding: 16px; margin-bottom: 16px; background: #fff; }}
|
|
257
|
+
summary {{ font-weight: 500; font-size: 1.1em; cursor: pointer; }}
|
|
258
|
+
.prompt-container {{ background-color: #e8f0fe; padding: 16px; margin: 12px 0; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }}
|
|
259
|
+
.reference-container {{ background-color: #fff; border: 1px solid #dadce0; padding: 16px; margin: 12px 0; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }}
|
|
260
|
+
.agent-info-container {{ background-color: #f1f3f4; padding: 16px; margin: 12px 0; border-radius: 8px; word-wrap: break-word; overflow-wrap: break-word; font-size: 14px; }}
|
|
261
|
+
.agent-info-grid {{ display: grid; grid-template-columns: 120px 1fr; gap: 8px; margin-bottom: 12px; }}
|
|
262
|
+
.agent-info-grid dt {{ font-weight: 500; color: #3c4043; }}
|
|
263
|
+
.agent-info-grid dd {{ margin: 0; white-space: pre-wrap; word-wrap: break-word; }}
|
|
264
|
+
.intermediate-events-container {{ background-color: #f1f3f4; padding: 16px; margin: 12px 0; border-radius: 8px; word-wrap: break-word; overflow-wrap: break-word; max-height: 400px; overflow-y: auto; overflow-x: auto; }}
|
|
265
|
+
.response-container {{ background-color: #f9f9f9; padding: 12px; margin-top: 8px; border-radius: 8px; border: 1px solid #eee; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }}
|
|
266
|
+
.explanation {{ color: #5f6368; font-style: italic; font-size: 0.9em; padding-top: 6px; }}
|
|
267
|
+
.raw-json-details summary {{ font-size: 0.9em; cursor: pointer; color: #5f6368;}}
|
|
268
|
+
.raw-json-container {{ white-space: pre-wrap; word-wrap: break-word; max-height: 300px; overflow-y: auto; background-color: #f1f1f1; padding: 10px; border-radius: 4px; margin-top: 8px; }}
|
|
269
|
+
|
|
270
|
+
.rubric-bubble-container {{ display: flex; flex-wrap: wrap; gap: 8px; }}
|
|
271
|
+
.rubric-details {{ border: none; padding: 0; margin: 0; }}
|
|
272
|
+
.rubric-bubble {{ display: inline-flex; align-items: center; background-color: #e8f0fe; color: #1967d2; border-radius: 16px; padding: 8px 12px; font-size: 0.9em; cursor: pointer; list-style: none; }}
|
|
273
|
+
.rubric-bubble::-webkit-details-marker {{ display: none; }}
|
|
274
|
+
.rubric-bubble::before {{ content: '►'; margin-right: 8px; font-size: 0.8em; transition: transform 0.2s; }}
|
|
275
|
+
.rubric-details[open] > .rubric-bubble::before {{ transform: rotate(90deg); }}
|
|
276
|
+
.pass {{ color: green; font-weight: bold; }}
|
|
277
|
+
.fail {{ color: red; font-weight: bold; }}
|
|
278
|
+
|
|
279
|
+
.case-content-wrapper {{ display: flex; gap: 1rem; }}
|
|
280
|
+
.case-content-main {{ flex: 1; }}
|
|
281
|
+
.case-content-sidebar {{ flex: 1; min-width: 0; }}
|
|
282
|
+
|
|
283
|
+
/* Tool Declarations */
|
|
284
|
+
.tool-declarations-container {{ background-color: #f1f1f1; padding: 10px; border-radius: 4px; margin-top: 8px; max-height: 300px; overflow-y: auto; }}
|
|
285
|
+
.tool-declaration {{ margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #ddd; }}
|
|
286
|
+
.tool-declaration:last-child {{ border-bottom: none; margin-bottom: 0; padding-bottom: 0; }}
|
|
287
|
+
|
|
288
|
+
/* Agent Topology UI */
|
|
289
|
+
.system-topology-details {{ background: #fff; border: 1px solid #dadce0; border-radius: 8px; padding: 16px; margin-top: 16px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }}
|
|
290
|
+
.system-topology-details > summary {{ font-size: 1.1em; font-weight: 500; cursor: pointer; outline: none; margin-bottom: 12px; list-style: none; display: flex; align-items: center; color: #3c4043; }}
|
|
291
|
+
.system-topology-details > summary::-webkit-details-marker {{ display: none; }}
|
|
292
|
+
.system-topology-details > summary::before {{ content: '▼'; font-size: 0.8em; margin-right: 8px; transition: transform 0.2s; }}
|
|
293
|
+
.system-topology-details:not([open]) > summary::before {{ transform: rotate(-90deg); }}
|
|
294
|
+
|
|
295
|
+
.topology-container {{ background: #f8f9fa; border-radius: 8px; padding: 16px; margin-top: 8px; border: 1px solid #dadce0; overflow-x: auto; }}
|
|
296
|
+
.agent-node {{ background: #fff; border: 1px solid #dadce0; border-radius: 8px; margin-bottom: 12px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); min-width: 300px; max-width: 600px; }}
|
|
297
|
+
.agent-node-header {{ padding: 10px 16px; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 8px; background: #f1f3f4; border-top-left-radius: 8px; border-top-right-radius: 8px; }}
|
|
298
|
+
.agent-name {{ font-weight: 600; color: #1a73e8; }}
|
|
299
|
+
.agent-type {{ font-size: 11px; background: #e8eaed; padding: 2px 8px; border-radius: 12px; color: #5f6368; font-family: monospace; }}
|
|
300
|
+
.agent-node-body {{ padding: 12px 16px; font-size: 13px; color: #3c4043; }}
|
|
301
|
+
.agent-desc {{ margin-bottom: 8px; }}
|
|
302
|
+
.agent-inst details, .agent-tools details {{ margin-bottom: 8px; padding: 8px; }}
|
|
303
|
+
.agent-inst summary, .agent-tools summary {{ cursor: pointer; font-weight: 500; color: #5f6368; font-size: 12px; outline: none; margin-bottom: 0; }}
|
|
304
|
+
.inst-content, .tools-content {{ margin-top: 8px; padding: 8px; background: #f8f9fa; border-radius: 4px; border: 1px solid #eee; white-space: pre-wrap; word-wrap: break-word; font-family: monospace; font-size: 12px; max-height: 200px; overflow-y: auto; }}
|
|
305
|
+
.sub-agents-container {{ margin-top: 16px; padding-left: 24px; border-left: 2px solid #dadce0; position: relative; }}
|
|
306
|
+
.sub-agents-container::before {{ content: ''; position: absolute; top: -16px; left: -2px; width: 2px; height: 16px; background: #dadce0; }}
|
|
307
|
+
|
|
308
|
+
/* Multi-turn Agent Trace UI */
|
|
309
|
+
.conversation-trace-details {{ background: #fff; border: 1px solid #dadce0; border-radius: 8px; padding: 16px; margin-top: 0; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }}
|
|
310
|
+
.conversation-trace-details > summary {{ font-size: 1.1em; font-weight: 500; cursor: pointer; outline: none; margin-bottom: 8px; list-style: none; display: flex; align-items: center; color: #3c4043; }}
|
|
311
|
+
.conversation-trace-details > summary::-webkit-details-marker {{ display: none; }}
|
|
312
|
+
.conversation-trace-details > summary::before {{ content: '▼'; font-size: 0.8em; margin-right: 8px; transition: transform 0.2s; }}
|
|
313
|
+
.conversation-trace-details:not([open]) > summary::before {{ transform: rotate(-90deg); }}
|
|
314
|
+
|
|
315
|
+
.agent-timeline {{ position: relative; padding-left: 32px; margin-top: 16px; font-family: 'Roboto', sans-serif; }}
|
|
316
|
+
.agent-timeline::before {{ content: ''; position: absolute; top: 0; bottom: 0; left: 11px; width: 2px; background: #e8eaed; }}
|
|
317
|
+
|
|
318
|
+
.turn-details {{ margin-bottom: 16px; padding: 0; border: none; }}
|
|
319
|
+
.turn-summary {{ list-style: none; outline: none; cursor: pointer; display: block; margin-left: -32px; padding-left: 32px; }}
|
|
320
|
+
.turn-summary::-webkit-details-marker {{ display: none; }}
|
|
321
|
+
.turn-header {{ margin: 16px 0 16px -32px; position: relative; z-index: 1; display: inline-flex; align-items: center; }}
|
|
322
|
+
|
|
323
|
+
.turn-badge {{ display: inline-flex; align-items: center; background: #f8f9fa; color: #5f6368; padding: 4px 12px; border-radius: 16px; font-size: 11px; font-weight: 600; border: 1px solid #dadce0; letter-spacing: 0.5px; }}
|
|
324
|
+
.turn-divider {{ color: #dadce0; margin: 0 6px; font-weight: normal; }}
|
|
325
|
+
.turn-badge::before {{ content: '▼'; margin-right: 6px; font-size: 0.8em; display: inline-block; transition: transform 0.2s; }}
|
|
326
|
+
.turn-details:not([open]) .turn-badge::before {{ transform: rotate(-90deg); }}
|
|
327
|
+
|
|
328
|
+
.timeline-item {{ position: relative; margin-bottom: 16px; }}
|
|
329
|
+
.timeline-icon {{ position: absolute; left: -32px; top: 0; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: #e8f0fe; border: 2px solid #fff; color: #1a73e8; z-index: 1; box-sizing: border-box; margin-left: 0; }}
|
|
330
|
+
.timeline-icon.user {{ background: #f3e8fd; color: #9334e6; }}
|
|
331
|
+
.timeline-icon.tool {{ background: #e6f4ea; color: #1e8e3e; }}
|
|
332
|
+
.timeline-content {{ background: #fff; border: 1px solid #dadce0; border-radius: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); overflow: hidden; }}
|
|
333
|
+
.event-header {{ padding: 12px 16px 8px; font-size: 12px; font-weight: 600; display: flex; align-items: baseline; gap: 8px; }}
|
|
334
|
+
.event-author {{ color: #1a73e8; letter-spacing: 0.5px; text-transform: uppercase; }}
|
|
335
|
+
.event-author.user {{ color: #9334e6; }}
|
|
336
|
+
.event-author.tool {{ color: #1e8e3e; }}
|
|
337
|
+
.event-role {{ color: #80868b; font-weight: normal; font-size: 11px; font-family: monospace;}}
|
|
338
|
+
.event-body {{ padding: 0 16px 12px; font-size: 14px; color: #202124; line-height: 1.5; }}
|
|
339
|
+
.dark-code-block {{ background: #0e111a; color: #d4d4d4; padding: 12px; border-radius: 6px; font-family: 'Consolas', 'Courier New', monospace; font-size: 13px; margin: 8px 0 0 0; overflow-x: auto; border: 1px solid #3c4043; }}
|
|
340
|
+
.function-call-title {{ color: #e37400; font-weight: 600; font-size: 12px; margin-top: 8px; display: flex; align-items: center; gap: 4px; }}
|
|
341
|
+
.function-response-title {{ color: #0f9d58; font-weight: 600; font-size: 12px; margin-top: 8px; display: flex; align-items: center; gap: 4px; }}
|
|
342
|
+
.agent-trace-container {{ max-height: 600px; overflow-x: auto; overflow-y: auto; padding-right: 8px; border: 1px solid #eee; padding: 12px; border-radius: 8px; background: #fafafa; }}
|
|
343
|
+
|
|
344
|
+
/* Collapsible Metrics */
|
|
345
|
+
.metric-details {{ background: #fff; border: 1px solid #dadce0; border-radius: 8px; margin-bottom: 12px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); overflow: hidden; }}
|
|
346
|
+
.metric-summary {{ list-style: none; cursor: pointer; padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; background: #f8f9fa; margin: 0; outline: none; }}
|
|
347
|
+
.metric-summary::-webkit-details-marker {{ display: none; }}
|
|
348
|
+
.metric-details[open] .metric-summary {{ border-bottom: 1px solid #dadce0; }}
|
|
349
|
+
.metric-name-wrapper {{ display: flex; align-items: center; font-weight: 600; color: #3c4043; font-size: 14px; }}
|
|
350
|
+
.metric-name-wrapper::before {{ content: '►'; font-size: 0.8em; margin-right: 8px; transition: transform 0.2s; color: #5f6368; }}
|
|
351
|
+
.metric-details[open] .metric-name-wrapper::before {{ transform: rotate(90deg); }}
|
|
352
|
+
.metric-score {{ font-weight: bold; font-size: 16px; color: #1a73e8; }}
|
|
353
|
+
.metric-body {{ padding: 16px; }}
|
|
354
|
+
</style>
|
|
355
|
+
</head>
|
|
356
|
+
<body>
|
|
357
|
+
<div class="container">
|
|
358
|
+
<h1>Evaluation Report</h1>
|
|
359
|
+
<div id="summary-section"></div>
|
|
360
|
+
<div id="details-section"></div>
|
|
361
|
+
</div>
|
|
362
|
+
<script>
|
|
363
|
+
var vizData_vertex_eval_sdk = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob("{payload_b64}"), c => c.charCodeAt(0))));
|
|
364
|
+
|
|
365
|
+
function formatDictVals(obj) {{
|
|
366
|
+
if (typeof obj === 'string') return obj;
|
|
367
|
+
if (obj === undefined || obj === null) return '';
|
|
368
|
+
if (typeof obj !== 'object') return String(obj);
|
|
369
|
+
if (Array.isArray(obj)) return JSON.stringify(obj);
|
|
370
|
+
return Object.entries(obj).map(([k,v]) => `${{k}}=${{formatDictVals(v)}}`).join(', ');
|
|
371
|
+
}}
|
|
372
|
+
|
|
373
|
+
function formatToolDeclarations(toolDeclarations) {{
|
|
374
|
+
if (!toolDeclarations) return '';
|
|
375
|
+
let functions = [];
|
|
376
|
+
const builtins = [];
|
|
377
|
+
|
|
378
|
+
function collectFromTool(tool) {{
|
|
379
|
+
if (!tool || typeof tool !== 'object') return;
|
|
380
|
+
if (Array.isArray(tool.function_declarations) && tool.function_declarations.length > 0) {{
|
|
381
|
+
functions = functions.concat(tool.function_declarations);
|
|
382
|
+
return;
|
|
383
|
+
}}
|
|
384
|
+
if (tool.name && tool.parameters) {{
|
|
385
|
+
functions.push(tool);
|
|
386
|
+
return;
|
|
387
|
+
}}
|
|
388
|
+
Object.keys(tool).forEach(k => {{
|
|
389
|
+
if (k === 'function_declarations') return;
|
|
390
|
+
if (tool[k] === null || tool[k] === undefined) return;
|
|
391
|
+
builtins.push(k);
|
|
392
|
+
}});
|
|
393
|
+
}}
|
|
394
|
+
|
|
395
|
+
if (Array.isArray(toolDeclarations)) {{
|
|
396
|
+
toolDeclarations.forEach(collectFromTool);
|
|
397
|
+
}} else if (typeof toolDeclarations === 'object') {{
|
|
398
|
+
if (toolDeclarations.function_declarations) {{
|
|
399
|
+
functions = functions.concat(toolDeclarations.function_declarations);
|
|
400
|
+
}} else {{
|
|
401
|
+
collectFromTool(toolDeclarations);
|
|
402
|
+
}}
|
|
403
|
+
}}
|
|
404
|
+
|
|
405
|
+
if (functions.length === 0 && builtins.length === 0) {{
|
|
406
|
+
return `<pre class="raw-json-container">${{DOMPurify.sanitize(JSON.stringify(toolDeclarations, null, 2))}}</pre>`;
|
|
407
|
+
}}
|
|
408
|
+
|
|
409
|
+
let html = '<div class="tool-declarations-container">';
|
|
410
|
+
functions.forEach(func => {{
|
|
411
|
+
html += '<div class="tool-declaration">';
|
|
412
|
+
const params = func.parameters && func.parameters.properties ? func.parameters.properties : {{}};
|
|
413
|
+
const requiredParams = func.parameters && func.parameters.required ? new Set(func.parameters.required) : new Set();
|
|
414
|
+
const paramStrings = Object.keys(params).map(p => `${{DOMPurify.sanitize(p)}}: ${{DOMPurify.sanitize(params[p].type)}}`).join(', ');
|
|
415
|
+
html += `<strong>${{DOMPurify.sanitize(func.name)}}</strong>(${{paramStrings}})<br>`;
|
|
416
|
+
if(func.description) html += `<em>${{DOMPurify.sanitize(func.description)}}</em><br>`;
|
|
417
|
+
if(Object.keys(params).length > 0) html += 'Parameters:<br>';
|
|
418
|
+
Object.keys(params).forEach(p => {{
|
|
419
|
+
html += ` - ${{DOMPurify.sanitize(p)}}: ${{DOMPurify.sanitize(params[p].description || '')}} ${{requiredParams.has(p) ? '<strong>(required)</strong>' : ''}}<br>`;
|
|
420
|
+
}});
|
|
421
|
+
html += '</div>';
|
|
422
|
+
}});
|
|
423
|
+
builtins.forEach(name => {{
|
|
424
|
+
html += `<div class="tool-declaration"><strong>${{DOMPurify.sanitize(name)}}</strong> <em>(built-in tool)</em></div>`;
|
|
425
|
+
}});
|
|
426
|
+
html += '</div>';
|
|
427
|
+
return html;
|
|
428
|
+
}}
|
|
429
|
+
|
|
430
|
+
function formatSystemTopology(agents) {{
|
|
431
|
+
if (!agents || Object.keys(agents).length === 0) return '<p>No agent configurations provided.</p>';
|
|
432
|
+
|
|
433
|
+
const allSubAgents = new Set();
|
|
434
|
+
Object.values(agents).forEach(agent => {{
|
|
435
|
+
if (agent.sub_agents) {{
|
|
436
|
+
agent.sub_agents.forEach(sa => allSubAgents.add(sa));
|
|
437
|
+
}}
|
|
438
|
+
}});
|
|
439
|
+
|
|
440
|
+
const roots = Object.keys(agents).filter(id => !allSubAgents.has(id));
|
|
441
|
+
if (roots.length === 0) {{
|
|
442
|
+
roots.push(Object.keys(agents)[0]);
|
|
443
|
+
}}
|
|
444
|
+
|
|
445
|
+
let html = '<div class="topology-container">';
|
|
446
|
+
|
|
447
|
+
const renderAgent = (agentId, visited) => {{
|
|
448
|
+
if (visited.has(agentId)) return '';
|
|
449
|
+
visited.add(agentId);
|
|
450
|
+
|
|
451
|
+
const agent = agents[agentId];
|
|
452
|
+
if (!agent) return '';
|
|
453
|
+
|
|
454
|
+
let nodeHtml = `<div class="agent-node">`;
|
|
455
|
+
nodeHtml += `<div class="agent-node-header">
|
|
456
|
+
<span style="font-size:16px;">🤖</span>
|
|
457
|
+
<span class="agent-name">${{DOMPurify.sanitize(agentId)}}</span>
|
|
458
|
+
${{agent.agent_type ? `<span class="agent-type">${{DOMPurify.sanitize(agent.agent_type)}}</span>` : ''}}
|
|
459
|
+
</div>`;
|
|
460
|
+
|
|
461
|
+
nodeHtml += `<div class="agent-node-body">`;
|
|
462
|
+
if (agent.description) {{
|
|
463
|
+
nodeHtml += `<div class="agent-desc"><strong>Role:</strong> ${{DOMPurify.sanitize(agent.description)}}</div>`;
|
|
464
|
+
}}
|
|
465
|
+
if (agent.instruction) {{
|
|
466
|
+
nodeHtml += `<div class="agent-inst">
|
|
467
|
+
<details>
|
|
468
|
+
<summary>System Instructions</summary>
|
|
469
|
+
<div class="inst-content">${{DOMPurify.sanitize(agent.instruction)}}</div>
|
|
470
|
+
</details>
|
|
471
|
+
</div>`;
|
|
472
|
+
}}
|
|
473
|
+
if (agent.tools && agent.tools.length > 0) {{
|
|
474
|
+
nodeHtml += `<div class="agent-tools">
|
|
475
|
+
<details>
|
|
476
|
+
<summary>Tools (${{agent.tools.length}})</summary>
|
|
477
|
+
<div class="tools-content" style="padding:0; border:none; background:transparent;">
|
|
478
|
+
${{formatToolDeclarations(agent.tools)}}
|
|
479
|
+
</div>
|
|
480
|
+
</details>
|
|
481
|
+
</div>`;
|
|
482
|
+
}}
|
|
483
|
+
|
|
484
|
+
if (agent.sub_agents && agent.sub_agents.length > 0) {{
|
|
485
|
+
nodeHtml += `<div class="sub-agents-container">`;
|
|
486
|
+
agent.sub_agents.forEach(sa => {{
|
|
487
|
+
nodeHtml += renderAgent(sa, new Set(visited));
|
|
488
|
+
}});
|
|
489
|
+
nodeHtml += `</div>`;
|
|
490
|
+
}}
|
|
491
|
+
|
|
492
|
+
nodeHtml += `</div></div>`;
|
|
493
|
+
return nodeHtml;
|
|
494
|
+
}};
|
|
495
|
+
|
|
496
|
+
roots.forEach(rootId => {{
|
|
497
|
+
html += renderAgent(rootId, new Set());
|
|
498
|
+
}});
|
|
499
|
+
|
|
500
|
+
html += '</div>';
|
|
501
|
+
return html;
|
|
502
|
+
}}
|
|
503
|
+
|
|
504
|
+
function formatAgentData(agentData) {{
|
|
505
|
+
let data = agentData;
|
|
506
|
+
if (typeof data === 'string') {{
|
|
507
|
+
try {{ data = JSON.parse(data); }} catch(e) {{ return ''; }}
|
|
508
|
+
}}
|
|
509
|
+
if (!data || !data.turns) return '';
|
|
510
|
+
|
|
511
|
+
let html = '<div class="agent-timeline">';
|
|
512
|
+
data.turns.forEach((turn, idx) => {{
|
|
513
|
+
const tIndex = turn.turn_index !== undefined ? turn.turn_index : (idx + 1);
|
|
514
|
+
const tId = turn.turn_id ? DOMPurify.sanitize(String(turn.turn_id)) : `turn-${{String(tIndex).padStart(3, '0')}}`;
|
|
515
|
+
|
|
516
|
+
html += `<details open class="turn-details">`;
|
|
517
|
+
html += `<summary class="turn-summary"><div class="turn-header"><span class="turn-badge">TURN ${{tIndex}} <span class="turn-divider">|</span> ID: ${{tId}}</span></div></summary>`;
|
|
518
|
+
|
|
519
|
+
if (turn.events) {{
|
|
520
|
+
turn.events.forEach(event => {{
|
|
521
|
+
const role = (event.content && event.content.role) ? event.content.role.toLowerCase() : 'model';
|
|
522
|
+
const author = event.author ? event.author : role;
|
|
523
|
+
|
|
524
|
+
let iconClass = 'model';
|
|
525
|
+
if (role === 'user') iconClass = 'user';
|
|
526
|
+
if (role === 'tool') iconClass = 'tool';
|
|
527
|
+
|
|
528
|
+
let svgIcon = '';
|
|
529
|
+
if (iconClass === 'user') {{
|
|
530
|
+
svgIcon = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`;
|
|
531
|
+
}} else if (iconClass === 'tool') {{
|
|
532
|
+
svgIcon = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>`;
|
|
533
|
+
}} else {{
|
|
534
|
+
svgIcon = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="10" rx="2"></rect><circle cx="12" cy="5" r="2"></circle><path d="M12 7v4"></path><line x1="8" y1="16" x2="8" y2="16"></line><line x1="16" y1="16" x2="16" y2="16"></line></svg>`;
|
|
535
|
+
}}
|
|
536
|
+
|
|
537
|
+
html += `<div class="timeline-item">
|
|
538
|
+
<div class="timeline-icon ${{iconClass}}">${{svgIcon}}</div>
|
|
539
|
+
<div class="timeline-content">
|
|
540
|
+
<div class="event-header">
|
|
541
|
+
<span class="event-author ${{iconClass}}">${{DOMPurify.sanitize(author)}}</span>
|
|
542
|
+
<span class="event-role">${{DOMPurify.sanitize(role)}}</span>
|
|
543
|
+
</div>
|
|
544
|
+
<div class="event-body">`;
|
|
545
|
+
|
|
546
|
+
if (event.content && event.content.parts) {{
|
|
547
|
+
event.content.parts.forEach(part => {{
|
|
548
|
+
if (part.text) {{
|
|
549
|
+
html += `<div>${{DOMPurify.sanitize(marked.parse(String(part.text)))}}</div>`;
|
|
550
|
+
}} else if (part.function_call) {{
|
|
551
|
+
const fnName = part.function_call.name;
|
|
552
|
+
const fnArgs = JSON.stringify(part.function_call.args, null, 2);
|
|
553
|
+
html += `<div class="function-call-title">>_ Function Call: ${{DOMPurify.sanitize(fnName)}}</div>
|
|
554
|
+
<pre class="dark-code-block">${{DOMPurify.sanitize(fnArgs)}}</pre>`;
|
|
555
|
+
}} else if (part.function_response) {{
|
|
556
|
+
const fnName = part.function_response.name;
|
|
557
|
+
let fnRes = part.function_response.response;
|
|
558
|
+
if(typeof fnRes === 'object' && fnRes !== null && fnRes.result !== undefined) {{
|
|
559
|
+
fnRes = fnRes.result;
|
|
560
|
+
}}
|
|
561
|
+
const fnResStr = JSON.stringify(fnRes, null, 2);
|
|
562
|
+
html += `<div class="function-response-title">
|
|
563
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 4px;"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>
|
|
564
|
+
Tool Output: ${{DOMPurify.sanitize(fnName)}}
|
|
565
|
+
</div>
|
|
566
|
+
<pre class="dark-code-block">${{DOMPurify.sanitize(fnResStr)}}</pre>`;
|
|
567
|
+
}}
|
|
568
|
+
}});
|
|
569
|
+
}} else {{
|
|
570
|
+
html += `<div><pre class="raw-json-container">${{DOMPurify.sanitize(JSON.stringify(event.content, null, 2))}}</pre></div>`;
|
|
571
|
+
}}
|
|
572
|
+
|
|
573
|
+
html += `</div></div></div>`;
|
|
574
|
+
}});
|
|
575
|
+
}}
|
|
576
|
+
html += `</details>`;
|
|
577
|
+
}});
|
|
578
|
+
html += '</div>';
|
|
579
|
+
return html;
|
|
580
|
+
}}
|
|
581
|
+
|
|
582
|
+
function renderSummary(summaryMetrics) {{
|
|
583
|
+
const container = document.getElementById('summary-section');
|
|
584
|
+
let content = '<h2>Summary Metrics</h2>';
|
|
585
|
+
if (!summaryMetrics || summaryMetrics.length === 0) {{ container.innerHTML = content + '<p>No summary metrics.</p>'; return; }}
|
|
586
|
+
let table = '<table><thead><tr><th>Metric</th><th>Mean Score</th><th>Std. Dev.</th></tr></thead><tbody>';
|
|
587
|
+
summaryMetrics.forEach(m => {{
|
|
588
|
+
table += `<tr><td>${{m.metric_name || 'N/A'}}</td><td>${{m.mean_score != null ? m.mean_score.toFixed(4) : 'N/A'}}</td><td>${{m.stdev_score != null ? m.stdev_score.toFixed(4) : 'N/A'}}</td></tr>`;
|
|
589
|
+
}});
|
|
590
|
+
container.innerHTML = content + table + '</tbody></table>';
|
|
591
|
+
}}
|
|
592
|
+
|
|
593
|
+
function renderDetails(caseResults, metadata) {{
|
|
594
|
+
const container = document.getElementById('details-section');
|
|
595
|
+
container.innerHTML = '<h2>Detailed Results</h2>';
|
|
596
|
+
if (!caseResults || caseResults.length === 0) {{ container.innerHTML += '<p>No detailed results.</p>'; return; }}
|
|
597
|
+
|
|
598
|
+
const datasetRows = metadata && metadata.dataset ? metadata.dataset : [];
|
|
599
|
+
|
|
600
|
+
caseResults.forEach((caseResult, i) => {{
|
|
601
|
+
const original_case = datasetRows[caseResult.eval_case_index] || {{}};
|
|
602
|
+
|
|
603
|
+
const isValEmpty = (val) => !val || val === 'None' || val === 'nan' || String(val).trim() === '';
|
|
604
|
+
|
|
605
|
+
const promptText = isValEmpty(original_case.prompt_display_text) ? '' : original_case.prompt_display_text;
|
|
606
|
+
const promptJson = original_case.prompt_raw_json;
|
|
607
|
+
const reference = isValEmpty(original_case.reference) ? '' : original_case.reference;
|
|
608
|
+
const referenceJson = original_case.reference_raw_json;
|
|
609
|
+
const responseText = isValEmpty(original_case.response_display_text) ? '' : original_case.response_display_text;
|
|
610
|
+
const responseJson = original_case.response_raw_json;
|
|
611
|
+
|
|
612
|
+
let agentData = original_case.agent_data;
|
|
613
|
+
if (typeof agentData === 'string') {{
|
|
614
|
+
try {{ agentData = JSON.parse(agentData); }} catch(e) {{}}
|
|
615
|
+
}}
|
|
616
|
+
const isAgentEval = !!agentData;
|
|
617
|
+
|
|
618
|
+
let isRefAgentData = false;
|
|
619
|
+
let refAgentDataObj = null;
|
|
620
|
+
if (reference) {{
|
|
621
|
+
try {{
|
|
622
|
+
let parsed = typeof reference === 'string' ? JSON.parse(reference) : reference;
|
|
623
|
+
if (parsed && parsed.turns) {{
|
|
624
|
+
isRefAgentData = true;
|
|
625
|
+
refAgentDataObj = parsed;
|
|
626
|
+
}}
|
|
627
|
+
}} catch(e) {{}}
|
|
628
|
+
}}
|
|
629
|
+
|
|
630
|
+
let card = `<details open><summary style="font-size: 1.2em;">Case #${{caseResult.eval_case_index != null ? caseResult.eval_case_index : i}}</summary>`;
|
|
631
|
+
|
|
632
|
+
if (isAgentEval && agentData.agents && Object.keys(agentData.agents).length > 0) {{
|
|
633
|
+
card += `<details open class="system-topology-details">
|
|
634
|
+
<summary>System Topology</summary>
|
|
635
|
+
${{formatSystemTopology(agentData.agents)}}
|
|
636
|
+
</details>`;
|
|
637
|
+
}}
|
|
638
|
+
|
|
639
|
+
if (promptText) {{
|
|
640
|
+
card += `<div class="prompt-container"><strong>Prompt:</strong><br>${{DOMPurify.sanitize(marked.parse(String(promptText)))}}</div>`;
|
|
641
|
+
}}
|
|
642
|
+
if (promptJson && promptJson !== '""' && promptJson !== 'null' && promptJson !== '{{}}') {{
|
|
643
|
+
card += `<details class="raw-json-details"><summary>View Raw Prompt JSON</summary><pre class="raw-json-container">${{DOMPurify.sanitize(promptJson)}}</pre></details>`;
|
|
644
|
+
}}
|
|
645
|
+
|
|
646
|
+
if (responseText) {{
|
|
647
|
+
const responseTitle = isAgentEval ? 'Final Response' : 'Candidate Response';
|
|
648
|
+
card += `<div class="response-container"><h4>${{responseTitle}}</h4>${{DOMPurify.sanitize(marked.parse(String(responseText)))}}</div>`;
|
|
649
|
+
}}
|
|
650
|
+
if (responseJson && responseJson !== '""' && responseJson !== 'null' && responseJson !== '{{}}') {{
|
|
651
|
+
card += `<details class="raw-json-details"><summary>View Raw Response JSON</summary><pre class="raw-json-container">${{DOMPurify.sanitize(responseJson)}}</pre></details>`;
|
|
652
|
+
}}
|
|
653
|
+
|
|
654
|
+
let hasTrace = isAgentEval && agentData.turns;
|
|
655
|
+
let hasRef = !!reference;
|
|
656
|
+
|
|
657
|
+
if (hasTrace || hasRef) {{
|
|
658
|
+
card += `<div style="display: flex; gap: 1rem; margin-top: 16px;">`;
|
|
659
|
+
|
|
660
|
+
if (hasTrace) {{
|
|
661
|
+
let traceContent = formatAgentData(agentData);
|
|
662
|
+
card += `<div style="flex: 1; min-width: 0;">
|
|
663
|
+
<details open class="conversation-trace-details" style="margin: 0; height: 100%;">
|
|
664
|
+
<summary>Conversation Trace</summary>
|
|
665
|
+
<div style="font-size:13px; color:#5f6368; margin-bottom:12px;">Sequence of multi-agent events across turns</div>
|
|
666
|
+
<div class="agent-trace-container">${{traceContent}}</div>
|
|
667
|
+
</details>
|
|
668
|
+
</div>`;
|
|
669
|
+
}}
|
|
670
|
+
|
|
671
|
+
if (hasRef) {{
|
|
672
|
+
card += `<div style="flex: 1; min-width: 0;">`;
|
|
673
|
+
if (isRefAgentData) {{
|
|
674
|
+
let refTraceContent = formatAgentData(refAgentDataObj);
|
|
675
|
+
card += `<details open class="conversation-trace-details" style="margin: 0; height: 100%;">
|
|
676
|
+
<summary>Reference</summary>
|
|
677
|
+
<div style="font-size:13px; color:#5f6368; margin-bottom:12px;">Sequence of multi-agent events across turns</div>
|
|
678
|
+
<div class="agent-trace-container">${{refTraceContent}}</div>
|
|
679
|
+
</details>`;
|
|
680
|
+
}} else {{
|
|
681
|
+
card += `<div class="reference-container" style="margin: 0; height: 100%;"><strong>Reference</strong><br>${{DOMPurify.sanitize(marked.parse(String(reference)))}}</div>`;
|
|
682
|
+
}}
|
|
683
|
+
card += `</div>`;
|
|
684
|
+
}}
|
|
685
|
+
card += `</div>`;
|
|
686
|
+
}}
|
|
687
|
+
|
|
688
|
+
let metricTable = '<h3 style="margin-top:24px;">Evaluation Metrics</h3><div class="metrics-list">';
|
|
689
|
+
const candidateMetrics = (caseResult.response_candidate_results && caseResult.response_candidate_results[0] && caseResult.response_candidate_results[0].metric_results) || {{}};
|
|
690
|
+
Object.entries(candidateMetrics).forEach(([name, val]) => {{
|
|
691
|
+
let metricNameCell = `<strong>${{name}}</strong>`;
|
|
692
|
+
let explanationHandled = false;
|
|
693
|
+
let bubbles = '';
|
|
694
|
+
|
|
695
|
+
if (name.startsWith('hallucination') && val.explanation) {{
|
|
696
|
+
try {{
|
|
697
|
+
const explanationData = typeof val.explanation === 'string' ? JSON.parse(val.explanation) : val.explanation;
|
|
698
|
+
if (Array.isArray(explanationData) && explanationData.length > 0) {{
|
|
699
|
+
let sentenceGroups = [];
|
|
700
|
+
if (explanationData[0].explanation && Array.isArray(explanationData[0].explanation)) {{
|
|
701
|
+
explanationData.forEach(item => {{
|
|
702
|
+
if(item.explanation && Array.isArray(item.explanation)) {{
|
|
703
|
+
sentenceGroups.push(item.explanation);
|
|
704
|
+
}}
|
|
705
|
+
}});
|
|
706
|
+
}} else if (explanationData[0].sentence) {{
|
|
707
|
+
sentenceGroups.push(explanationData);
|
|
708
|
+
}}
|
|
709
|
+
|
|
710
|
+
if(sentenceGroups.length > 0) {{
|
|
711
|
+
sentenceGroups.forEach(sentenceList => {{
|
|
712
|
+
bubbles += '<div class="rubric-bubble-container" style="margin-top: 8px;">';
|
|
713
|
+
sentenceList.forEach(item => {{
|
|
714
|
+
let sentence = item.sentence || 'N/A';
|
|
715
|
+
const label = item.label ? item.label.toLowerCase() : '';
|
|
716
|
+
const isPass = label === 'no_rad' || label === 'supported';
|
|
717
|
+
const verdictText = isPass ? '<span class="pass">Pass</span>' : '<span class="fail">Fail</span>';
|
|
718
|
+
if (isPass) {{
|
|
719
|
+
sentence = `"${{sentence}}" is grounded`;
|
|
720
|
+
}}
|
|
721
|
+
const rationale = item.rationale || 'N/A';
|
|
722
|
+
const itemJson = JSON.stringify(item, null, 2);
|
|
723
|
+
bubbles += `
|
|
724
|
+
<details class="rubric-details">
|
|
725
|
+
<summary class="rubric-bubble">${{verdictText}}: ${{DOMPurify.sanitize(sentence)}}</summary>
|
|
726
|
+
<div class="explanation" style="padding: 10px 0 0 20px;">${{DOMPurify.sanitize(rationale)}}</div>
|
|
727
|
+
<pre class="raw-json-container">${{DOMPurify.sanitize(itemJson)}}</pre>
|
|
728
|
+
</details>`;
|
|
729
|
+
}});
|
|
730
|
+
bubbles += '</div>';
|
|
731
|
+
}});
|
|
732
|
+
explanationHandled = true;
|
|
733
|
+
}}
|
|
734
|
+
}}
|
|
735
|
+
}} catch (e) {{
|
|
736
|
+
console.error("Failed to parse hallucination explanation:", e);
|
|
737
|
+
}}
|
|
738
|
+
}} else if (name.startsWith('safety') && val.score != null) {{
|
|
739
|
+
try {{
|
|
740
|
+
bubbles += '<div class="rubric-bubble-container" style="margin-top: 8px;">';
|
|
741
|
+
const verdictText = val.score >= 1.0 ? '<span class="pass">Pass</span>' : '<span class="fail">Fail</span>';
|
|
742
|
+
const explanation = val.explanation || (val.score >= 1.0 ? 'Safety check passed' : 'Safety check failed');
|
|
743
|
+
const itemJson = JSON.stringify(val, null, 2);
|
|
744
|
+
bubbles += `
|
|
745
|
+
<details class="rubric-details">
|
|
746
|
+
<summary class="rubric-bubble">${{verdictText}}: ${{DOMPurify.sanitize(explanation)}}</summary>
|
|
747
|
+
<pre class="raw-json-container">${{DOMPurify.sanitize(itemJson)}}</pre>
|
|
748
|
+
</details>`;
|
|
749
|
+
bubbles += '</div>';
|
|
750
|
+
explanationHandled = true;
|
|
751
|
+
}} catch (e) {{
|
|
752
|
+
console.error("Failed to process safety metric:", e);
|
|
753
|
+
}}
|
|
754
|
+
}}
|
|
755
|
+
|
|
756
|
+
if (!bubbles && val.rubric_verdicts && val.rubric_verdicts.length > 0) {{
|
|
757
|
+
bubbles += '<div class="rubric-bubble-container" style="margin-top: 8px;">';
|
|
758
|
+
val.rubric_verdicts.forEach(verdict => {{
|
|
759
|
+
const rubricDescription = verdict.evaluated_rubric && verdict.evaluated_rubric.content && verdict.evaluated_rubric.content.property ? verdict.evaluated_rubric.content.property.description : 'N/A';
|
|
760
|
+
const verdictText = verdict.verdict ? '<span class="pass">Pass</span>' : '<span class="fail">Fail</span>';
|
|
761
|
+
const verdictJson = JSON.stringify(verdict, null, 2);
|
|
762
|
+
bubbles += `
|
|
763
|
+
<details class="rubric-details">
|
|
764
|
+
<summary class="rubric-bubble">${{verdictText}}: ${{DOMPurify.sanitize(rubricDescription)}}</summary>
|
|
765
|
+
<pre class="raw-json-container">${{DOMPurify.sanitize(verdictJson)}}</pre>
|
|
766
|
+
</details>`;
|
|
767
|
+
}});
|
|
768
|
+
bubbles += '</div>';
|
|
769
|
+
}}
|
|
770
|
+
|
|
771
|
+
let scoreDisplay = val.score != null ? val.score.toFixed(2) : 'N/A';
|
|
772
|
+
let metricContent = '';
|
|
773
|
+
|
|
774
|
+
if (val.explanation && !explanationHandled) {{
|
|
775
|
+
metricContent += `<div class="explanation" style="margin-top:0; margin-bottom: 12px;">${{DOMPurify.sanitize(marked.parse(String(val.explanation)))}}</div>`;
|
|
776
|
+
}}
|
|
777
|
+
if (bubbles) {{
|
|
778
|
+
metricContent += bubbles;
|
|
779
|
+
}}
|
|
780
|
+
if (!metricContent) {{
|
|
781
|
+
metricContent = `<div style="color: #80868b; font-style: italic; font-size: 13px;">No additional details.</div>`;
|
|
782
|
+
}}
|
|
783
|
+
|
|
784
|
+
metricTable += `
|
|
785
|
+
<details class="metric-details" open>
|
|
786
|
+
<summary class="metric-summary">
|
|
787
|
+
<div class="metric-name-wrapper">${{DOMPurify.sanitize(name)}}</div>
|
|
788
|
+
<div class="metric-score">${{DOMPurify.sanitize(scoreDisplay)}}</div>
|
|
789
|
+
</summary>
|
|
790
|
+
<div class="metric-body">
|
|
791
|
+
${{metricContent}}
|
|
792
|
+
</div>
|
|
793
|
+
</details>
|
|
794
|
+
`;
|
|
795
|
+
}});
|
|
796
|
+
metricTable += '</div>';
|
|
797
|
+
card += metricTable + '</details>';
|
|
798
|
+
container.innerHTML += card;
|
|
799
|
+
}});
|
|
800
|
+
}}
|
|
801
|
+
|
|
802
|
+
renderSummary(vizData_vertex_eval_sdk.summary_metrics);
|
|
803
|
+
renderDetails(vizData_vertex_eval_sdk.eval_case_results, vizData_vertex_eval_sdk.metadata);
|
|
804
|
+
</script>
|
|
805
|
+
</body>
|
|
806
|
+
</html>
|
|
807
|
+
"""
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def get_comparison_html(eval_result_json: str) -> str:
|
|
812
|
+
"""Returns a self-contained HTML for a side-by-side eval comparison."""
|
|
813
|
+
payload_b64 = _encode_to_base64(eval_result_json)
|
|
814
|
+
return textwrap.dedent(
|
|
815
|
+
f"""
|
|
816
|
+
<!DOCTYPE html>
|
|
817
|
+
<html>
|
|
818
|
+
<head>
|
|
819
|
+
<meta charset="UTF-8">
|
|
820
|
+
<title>Eval Comparison Report</title>
|
|
821
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
822
|
+
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
|
823
|
+
<style>
|
|
824
|
+
|
|
825
|
+
body {{ font-family: 'Roboto', 'Helvetica', sans-serif; margin: 2em; background-color: #f8f9fa; color: #202124; }}
|
|
826
|
+
.container {{ max-width: 95%; margin: 20px auto; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }}
|
|
827
|
+
h1, h2, h3, h4 {{ color: #3c4043; }}
|
|
828
|
+
h1 {{ border-bottom: 2px solid #4285F4; padding-bottom: 8px; }}
|
|
829
|
+
h2 {{ border-bottom: 1px solid #dadce0; padding-bottom: 8px; }}
|
|
830
|
+
table {{ border-collapse: collapse; width: 100%; margin: 1em 0; }}
|
|
831
|
+
th, td {{ border: 1px solid #dadce0; padding: 12px; text-align: left; vertical-align: top; }}
|
|
832
|
+
th {{ background-color: #f2f2f2; font-weight: 500; }}
|
|
833
|
+
details {{ border: 1px solid #dadce0; border-radius: 8px; padding: 24px; margin-bottom: 24px; background: #fff; }}
|
|
834
|
+
summary {{ font-weight: 500; font-size: 1.2em; cursor: pointer; }}
|
|
835
|
+
.prompt-container {{ background-color: #e8f0fe; padding: 16px; margin-bottom: 16px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }}
|
|
836
|
+
.reference-container {{ background-color: #fff; border: 1px solid #dadce0; padding: 16px; margin-bottom: 16px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }}
|
|
837
|
+
.responses-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 20px; margin-top: 16px;}}
|
|
838
|
+
.response-column {{ border: 1px solid #e0e0e0; padding: 16px; border-radius: 8px; background: #f9f9f9; }}
|
|
839
|
+
.response-text-container {{ background-color: #fff; padding: 12px; margin-top: 8px; border-radius: 4px; border: 1px solid #eee; white-space: pre-wrap; word-wrap: break-word; max-height: 400px; overflow-y: auto; overflow-wrap: break-word; }}
|
|
840
|
+
.explanation {{ color: #5f6368; font-style: italic; font-size: 0.9em; padding-top: 8px; }}
|
|
841
|
+
.raw-json-details summary {{ font-size: 0.9em; cursor: pointer; color: #5f6368;}}
|
|
842
|
+
.raw-json-container {{ white-space: pre-wrap; word-wrap: break-word; max-height: 300px; overflow-y: auto; background-color: #f1f1f1; padding: 10px; border-radius: 4px; margin-top: 8px; }}
|
|
843
|
+
|
|
844
|
+
.rubric-bubble-container {{ display: flex; flex-wrap: wrap; gap: 8px; }}
|
|
845
|
+
.rubric-details {{ border: none; padding: 0; margin: 0; }}
|
|
846
|
+
.rubric-bubble {{ display: inline-flex; align-items: center; background-color: #e8f0fe; color: #1967d2; border-radius: 16px; padding: 8px 12px; font-size: 0.9em; cursor: pointer; list-style: none; }}
|
|
847
|
+
.rubric-bubble::-webkit-details-marker {{ display: none; }}
|
|
848
|
+
.rubric-bubble::before {{ content: '►'; margin-right: 8px; font-size: 0.8em; transition: transform 0.2s; }}
|
|
849
|
+
.rubric-details[open] > .rubric-bubble::before {{ transform: rotate(90deg); }}
|
|
850
|
+
.pass {{ color: green; font-weight: bold; }}
|
|
851
|
+
.fail {{ color: red; font-weight: bold; }}
|
|
852
|
+
|
|
853
|
+
/* Tool Declarations */
|
|
854
|
+
.tool-declarations-container {{ background-color: #f1f1f1; padding: 10px; border-radius: 4px; margin-top: 8px; max-height: 300px; overflow-y: auto; }}
|
|
855
|
+
.tool-declaration {{ margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #ddd; }}
|
|
856
|
+
.tool-declaration:last-child {{ border-bottom: none; margin-bottom: 0; padding-bottom: 0; }}
|
|
857
|
+
|
|
858
|
+
/* Agent Topology UI */
|
|
859
|
+
.system-topology-details {{ background: #fff; border: 1px solid #dadce0; border-radius: 8px; padding: 16px; margin-top: 16px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }}
|
|
860
|
+
.system-topology-details > summary {{ font-size: 1.1em; font-weight: 500; cursor: pointer; outline: none; margin-bottom: 12px; list-style: none; display: flex; align-items: center; color: #3c4043; }}
|
|
861
|
+
.system-topology-details > summary::-webkit-details-marker {{ display: none; }}
|
|
862
|
+
.system-topology-details > summary::before {{ content: '▼'; font-size: 0.8em; margin-right: 8px; transition: transform 0.2s; }}
|
|
863
|
+
.system-topology-details:not([open]) > summary::before {{ transform: rotate(-90deg); }}
|
|
864
|
+
|
|
865
|
+
.topology-container {{ background: #f8f9fa; border-radius: 8px; padding: 16px; margin-top: 8px; border: 1px solid #dadce0; overflow-x: auto; }}
|
|
866
|
+
.agent-node {{ background: #fff; border: 1px solid #dadce0; border-radius: 8px; margin-bottom: 12px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); min-width: 300px; max-width: 600px; }}
|
|
867
|
+
.agent-node-header {{ padding: 10px 16px; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 8px; background: #f1f3f4; border-top-left-radius: 8px; border-top-right-radius: 8px; }}
|
|
868
|
+
.agent-name {{ font-weight: 600; color: #1a73e8; }}
|
|
869
|
+
.agent-type {{ font-size: 11px; background: #e8eaed; padding: 2px 8px; border-radius: 12px; color: #5f6368; font-family: monospace; }}
|
|
870
|
+
.agent-node-body {{ padding: 12px 16px; font-size: 13px; color: #3c4043; }}
|
|
871
|
+
.agent-desc {{ margin-bottom: 8px; }}
|
|
872
|
+
.agent-inst details, .agent-tools details {{ margin-bottom: 8px; padding: 8px; }}
|
|
873
|
+
.agent-inst summary, .agent-tools summary {{ cursor: pointer; font-weight: 500; color: #5f6368; font-size: 12px; outline: none; margin-bottom: 0; }}
|
|
874
|
+
.inst-content, .tools-content {{ margin-top: 8px; padding: 8px; background: #f8f9fa; border-radius: 4px; border: 1px solid #eee; white-space: pre-wrap; word-wrap: break-word; font-family: monospace; font-size: 12px; max-height: 200px; overflow-y: auto; }}
|
|
875
|
+
.sub-agents-container {{ margin-top: 16px; padding-left: 24px; border-left: 2px solid #dadce0; position: relative; }}
|
|
876
|
+
.sub-agents-container::before {{ content: ''; position: absolute; top: -16px; left: -2px; width: 2px; height: 16px; background: #dadce0; }}
|
|
877
|
+
|
|
878
|
+
/* Multi-turn Agent Trace UI */
|
|
879
|
+
.conversation-trace-details {{ background: #fff; border: 1px solid #dadce0; border-radius: 8px; padding: 16px; margin-top: 0; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }}
|
|
880
|
+
.conversation-trace-details > summary {{ font-size: 1.1em; font-weight: 500; cursor: pointer; outline: none; margin-bottom: 8px; list-style: none; display: flex; align-items: center; color: #3c4043; }}
|
|
881
|
+
.conversation-trace-details > summary::-webkit-details-marker {{ display: none; }}
|
|
882
|
+
.conversation-trace-details > summary::before {{ content: '▼'; font-size: 0.8em; margin-right: 8px; transition: transform 0.2s; }}
|
|
883
|
+
.conversation-trace-details:not([open]) > summary::before {{ transform: rotate(-90deg); }}
|
|
884
|
+
|
|
885
|
+
.agent-timeline {{ position: relative; padding-left: 32px; margin-top: 16px; font-family: 'Roboto', sans-serif; }}
|
|
886
|
+
.agent-timeline::before {{ content: ''; position: absolute; top: 0; bottom: 0; left: 11px; width: 2px; background: #e8eaed; }}
|
|
887
|
+
|
|
888
|
+
.turn-details {{ margin-bottom: 16px; padding: 0; border: none; }}
|
|
889
|
+
.turn-summary {{ list-style: none; outline: none; cursor: pointer; display: block; margin-left: -32px; padding-left: 32px; }}
|
|
890
|
+
.turn-summary::-webkit-details-marker {{ display: none; }}
|
|
891
|
+
.turn-header {{ margin: 16px 0 16px -32px; position: relative; z-index: 1; display: inline-flex; align-items: center; }}
|
|
892
|
+
|
|
893
|
+
.turn-badge {{ display: inline-flex; align-items: center; background: #f8f9fa; color: #5f6368; padding: 4px 12px; border-radius: 16px; font-size: 11px; font-weight: 600; border: 1px solid #dadce0; letter-spacing: 0.5px; }}
|
|
894
|
+
.turn-divider {{ color: #dadce0; margin: 0 6px; font-weight: normal; }}
|
|
895
|
+
.turn-badge::before {{ content: '▼'; margin-right: 6px; font-size: 0.8em; display: inline-block; transition: transform 0.2s; }}
|
|
896
|
+
.turn-details:not([open]) .turn-badge::before {{ transform: rotate(-90deg); }}
|
|
897
|
+
|
|
898
|
+
.timeline-item {{ position: relative; margin-bottom: 16px; }}
|
|
899
|
+
.timeline-icon {{ position: absolute; left: -32px; top: 0; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: #e8f0fe; border: 2px solid #fff; color: #1a73e8; z-index: 1; box-sizing: border-box; margin-left: 0; }}
|
|
900
|
+
.timeline-icon.user {{ background: #f3e8fd; color: #9334e6; }}
|
|
901
|
+
.timeline-icon.tool {{ background: #e6f4ea; color: #1e8e3e; }}
|
|
902
|
+
.timeline-content {{ background: #fff; border: 1px solid #dadce0; border-radius: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); overflow: hidden; }}
|
|
903
|
+
.event-header {{ padding: 12px 16px 8px; font-size: 12px; font-weight: 600; display: flex; align-items: baseline; gap: 8px; }}
|
|
904
|
+
.event-author {{ color: #1a73e8; letter-spacing: 0.5px; text-transform: uppercase; }}
|
|
905
|
+
.event-author.user {{ color: #9334e6; }}
|
|
906
|
+
.event-author.tool {{ color: #1e8e3e; }}
|
|
907
|
+
.event-role {{ color: #80868b; font-weight: normal; font-size: 11px; font-family: monospace;}}
|
|
908
|
+
.event-body {{ padding: 0 16px 12px; font-size: 14px; color: #202124; line-height: 1.5; }}
|
|
909
|
+
.dark-code-block {{ background: #0e111a; color: #d4d4d4; padding: 12px; border-radius: 6px; font-family: 'Consolas', 'Courier New', monospace; font-size: 13px; margin: 8px 0 0 0; overflow-x: auto; border: 1px solid #3c4043; }}
|
|
910
|
+
.function-call-title {{ color: #e37400; font-weight: 600; font-size: 12px; margin-top: 8px; display: flex; align-items: center; gap: 4px; }}
|
|
911
|
+
.function-response-title {{ color: #0f9d58; font-weight: 600; font-size: 12px; margin-top: 8px; display: flex; align-items: center; gap: 4px; }}
|
|
912
|
+
.agent-trace-container {{ max-height: 600px; overflow-x: auto; overflow-y: auto; padding-right: 8px; border: 1px solid #eee; padding: 12px; border-radius: 8px; background: #fafafa; }}
|
|
913
|
+
|
|
914
|
+
/* Collapsible Metrics */
|
|
915
|
+
.metric-details {{ background: #fff; border: 1px solid #dadce0; border-radius: 8px; margin-bottom: 12px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); overflow: hidden; }}
|
|
916
|
+
.metric-summary {{ list-style: none; cursor: pointer; padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; background: #f8f9fa; margin: 0; outline: none; }}
|
|
917
|
+
.metric-summary::-webkit-details-marker {{ display: none; }}
|
|
918
|
+
.metric-details[open] .metric-summary {{ border-bottom: 1px solid #dadce0; }}
|
|
919
|
+
.metric-name-wrapper {{ display: flex; align-items: center; font-weight: 600; color: #3c4043; font-size: 14px; }}
|
|
920
|
+
.metric-name-wrapper::before {{ content: '►'; font-size: 0.8em; margin-right: 8px; transition: transform 0.2s; color: #5f6368; }}
|
|
921
|
+
.metric-details[open] .metric-name-wrapper::before {{ transform: rotate(90deg); }}
|
|
922
|
+
.metric-score {{ font-weight: bold; font-size: 16px; color: #1a73e8; }}
|
|
923
|
+
.metric-body {{ padding: 16px; }}
|
|
924
|
+
</style>
|
|
925
|
+
</head>
|
|
926
|
+
<body>
|
|
927
|
+
<div class="container">
|
|
928
|
+
<h1>Eval Comparison Report</h1>
|
|
929
|
+
<div id="summary-section"></div>
|
|
930
|
+
<div id="details-section"></div>
|
|
931
|
+
</div>
|
|
932
|
+
<script>
|
|
933
|
+
var vizData_vertex_eval_sdk = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob("{payload_b64}"), c => c.charCodeAt(0))));
|
|
934
|
+
|
|
935
|
+
function formatToolDeclarations(toolDeclarations) {{
|
|
936
|
+
if (!toolDeclarations) return '';
|
|
937
|
+
let functions = [];
|
|
938
|
+
const builtins = [];
|
|
939
|
+
|
|
940
|
+
function collectFromTool(tool) {{
|
|
941
|
+
if (!tool || typeof tool !== 'object') return;
|
|
942
|
+
if (Array.isArray(tool.function_declarations) && tool.function_declarations.length > 0) {{
|
|
943
|
+
functions = functions.concat(tool.function_declarations);
|
|
944
|
+
return;
|
|
945
|
+
}}
|
|
946
|
+
if (tool.name && tool.parameters) {{
|
|
947
|
+
functions.push(tool);
|
|
948
|
+
return;
|
|
949
|
+
}}
|
|
950
|
+
Object.keys(tool).forEach(k => {{
|
|
951
|
+
if (k === 'function_declarations') return;
|
|
952
|
+
if (tool[k] === null || tool[k] === undefined) return;
|
|
953
|
+
builtins.push(k);
|
|
954
|
+
}});
|
|
955
|
+
}}
|
|
956
|
+
|
|
957
|
+
if (Array.isArray(toolDeclarations)) {{
|
|
958
|
+
toolDeclarations.forEach(collectFromTool);
|
|
959
|
+
}} else if (typeof toolDeclarations === 'object') {{
|
|
960
|
+
if (toolDeclarations.function_declarations) {{
|
|
961
|
+
functions = functions.concat(toolDeclarations.function_declarations);
|
|
962
|
+
}} else {{
|
|
963
|
+
collectFromTool(toolDeclarations);
|
|
964
|
+
}}
|
|
965
|
+
}}
|
|
966
|
+
|
|
967
|
+
if (functions.length === 0 && builtins.length === 0) {{
|
|
968
|
+
return `<pre class="raw-json-container">${{DOMPurify.sanitize(JSON.stringify(toolDeclarations, null, 2))}}</pre>`;
|
|
969
|
+
}}
|
|
970
|
+
|
|
971
|
+
let html = '<div class="tool-declarations-container">';
|
|
972
|
+
functions.forEach(func => {{
|
|
973
|
+
html += '<div class="tool-declaration">';
|
|
974
|
+
const params = func.parameters && func.parameters.properties ? func.parameters.properties : {{}};
|
|
975
|
+
const requiredParams = func.parameters && func.parameters.required ? new Set(func.parameters.required) : new Set();
|
|
976
|
+
const paramStrings = Object.keys(params).map(p => `${{DOMPurify.sanitize(p)}}: ${{DOMPurify.sanitize(params[p].type)}}`).join(', ');
|
|
977
|
+
html += `<strong>${{DOMPurify.sanitize(func.name)}}</strong>(${{paramStrings}})<br>`;
|
|
978
|
+
if(func.description) html += `<em>${{DOMPurify.sanitize(func.description)}}</em><br>`;
|
|
979
|
+
if(Object.keys(params).length > 0) html += 'Parameters:<br>';
|
|
980
|
+
Object.keys(params).forEach(p => {{
|
|
981
|
+
html += ` - ${{DOMPurify.sanitize(p)}}: ${{DOMPurify.sanitize(params[p].description || '')}} ${{requiredParams.has(p) ? '<strong>(required)</strong>' : ''}}<br>`;
|
|
982
|
+
}});
|
|
983
|
+
html += '</div>';
|
|
984
|
+
}});
|
|
985
|
+
builtins.forEach(name => {{
|
|
986
|
+
html += `<div class="tool-declaration"><strong>${{DOMPurify.sanitize(name)}}</strong> <em>(built-in tool)</em></div>`;
|
|
987
|
+
}});
|
|
988
|
+
html += '</div>';
|
|
989
|
+
return html;
|
|
990
|
+
}}
|
|
991
|
+
|
|
992
|
+
function formatSystemTopology(agents) {{
|
|
993
|
+
if (!agents || Object.keys(agents).length === 0) return '<p>No agent configurations provided.</p>';
|
|
994
|
+
|
|
995
|
+
const allSubAgents = new Set();
|
|
996
|
+
Object.values(agents).forEach(agent => {{
|
|
997
|
+
if (agent.sub_agents) {{
|
|
998
|
+
agent.sub_agents.forEach(sa => allSubAgents.add(sa));
|
|
999
|
+
}}
|
|
1000
|
+
}});
|
|
1001
|
+
|
|
1002
|
+
const roots = Object.keys(agents).filter(id => !allSubAgents.has(id));
|
|
1003
|
+
if (roots.length === 0) {{
|
|
1004
|
+
roots.push(Object.keys(agents)[0]);
|
|
1005
|
+
}}
|
|
1006
|
+
|
|
1007
|
+
let html = '<div class="topology-container">';
|
|
1008
|
+
|
|
1009
|
+
const renderAgent = (agentId, visited) => {{
|
|
1010
|
+
if (visited.has(agentId)) return '';
|
|
1011
|
+
visited.add(agentId);
|
|
1012
|
+
|
|
1013
|
+
const agent = agents[agentId];
|
|
1014
|
+
if (!agent) return '';
|
|
1015
|
+
|
|
1016
|
+
let nodeHtml = `<div class="agent-node">`;
|
|
1017
|
+
nodeHtml += `<div class="agent-node-header">
|
|
1018
|
+
<span style="font-size:16px;">🤖</span>
|
|
1019
|
+
<span class="agent-name">${{DOMPurify.sanitize(agentId)}}</span>
|
|
1020
|
+
${{agent.agent_type ? `<span class="agent-type">${{DOMPurify.sanitize(agent.agent_type)}}</span>` : ''}}
|
|
1021
|
+
</div>`;
|
|
1022
|
+
|
|
1023
|
+
nodeHtml += `<div class="agent-node-body">`;
|
|
1024
|
+
if (agent.description) {{
|
|
1025
|
+
nodeHtml += `<div class="agent-desc"><strong>Role:</strong> ${{DOMPurify.sanitize(agent.description)}}</div>`;
|
|
1026
|
+
}}
|
|
1027
|
+
if (agent.instruction) {{
|
|
1028
|
+
nodeHtml += `<div class="agent-inst">
|
|
1029
|
+
<details>
|
|
1030
|
+
<summary>System Instructions</summary>
|
|
1031
|
+
<div class="inst-content">${{DOMPurify.sanitize(agent.instruction)}}</div>
|
|
1032
|
+
</details>
|
|
1033
|
+
</div>`;
|
|
1034
|
+
}}
|
|
1035
|
+
if (agent.tools && agent.tools.length > 0) {{
|
|
1036
|
+
nodeHtml += `<div class="agent-tools">
|
|
1037
|
+
<details>
|
|
1038
|
+
<summary>Tools (${{agent.tools.length}})</summary>
|
|
1039
|
+
<div class="tools-content" style="padding:0; border:none; background:transparent;">
|
|
1040
|
+
${{formatToolDeclarations(agent.tools)}}
|
|
1041
|
+
</div>
|
|
1042
|
+
</details>
|
|
1043
|
+
</div>`;
|
|
1044
|
+
}}
|
|
1045
|
+
|
|
1046
|
+
if (agent.sub_agents && agent.sub_agents.length > 0) {{
|
|
1047
|
+
nodeHtml += `<div class="sub-agents-container">`;
|
|
1048
|
+
agent.sub_agents.forEach(sa => {{
|
|
1049
|
+
nodeHtml += renderAgent(sa, new Set(visited));
|
|
1050
|
+
}});
|
|
1051
|
+
nodeHtml += `</div>`;
|
|
1052
|
+
}}
|
|
1053
|
+
|
|
1054
|
+
nodeHtml += `</div></div>`;
|
|
1055
|
+
return nodeHtml;
|
|
1056
|
+
}};
|
|
1057
|
+
|
|
1058
|
+
roots.forEach(rootId => {{
|
|
1059
|
+
html += renderAgent(rootId, new Set());
|
|
1060
|
+
}});
|
|
1061
|
+
|
|
1062
|
+
html += '</div>';
|
|
1063
|
+
return html;
|
|
1064
|
+
}}
|
|
1065
|
+
|
|
1066
|
+
function formatAgentData(agentData) {{
|
|
1067
|
+
let data = agentData;
|
|
1068
|
+
if (typeof data === 'string') {{
|
|
1069
|
+
try {{ data = JSON.parse(data); }} catch(e) {{ return ''; }}
|
|
1070
|
+
}}
|
|
1071
|
+
if (!data || !data.turns) return '';
|
|
1072
|
+
|
|
1073
|
+
let html = '<div class="agent-timeline">';
|
|
1074
|
+
data.turns.forEach((turn, idx) => {{
|
|
1075
|
+
const tIndex = turn.turn_index !== undefined ? turn.turn_index : (idx + 1);
|
|
1076
|
+
const tId = turn.turn_id ? DOMPurify.sanitize(String(turn.turn_id)) : `turn-${{String(tIndex).padStart(3, '0')}}`;
|
|
1077
|
+
|
|
1078
|
+
html += `<details open class="turn-details">`;
|
|
1079
|
+
html += `<summary class="turn-summary"><div class="turn-header"><span class="turn-badge">TURN ${{tIndex}} <span class="turn-divider">|</span> ID: ${{tId}}</span></div></summary>`;
|
|
1080
|
+
|
|
1081
|
+
if (turn.events) {{
|
|
1082
|
+
turn.events.forEach(event => {{
|
|
1083
|
+
const role = (event.content && event.content.role) ? event.content.role.toLowerCase() : 'model';
|
|
1084
|
+
const author = event.author ? event.author : role;
|
|
1085
|
+
|
|
1086
|
+
let iconClass = 'model';
|
|
1087
|
+
if (role === 'user') iconClass = 'user';
|
|
1088
|
+
if (role === 'tool') iconClass = 'tool';
|
|
1089
|
+
|
|
1090
|
+
let svgIcon = '';
|
|
1091
|
+
if (iconClass === 'user') {{
|
|
1092
|
+
svgIcon = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`;
|
|
1093
|
+
}} else if (iconClass === 'tool') {{
|
|
1094
|
+
svgIcon = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>`;
|
|
1095
|
+
}} else {{
|
|
1096
|
+
svgIcon = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="10" rx="2"></rect><circle cx="12" cy="5" r="2"></circle><path d="M12 7v4"></path><line x1="8" y1="16" x2="8" y2="16"></line><line x1="16" y1="16" x2="16" y2="16"></line></svg>`;
|
|
1097
|
+
}}
|
|
1098
|
+
|
|
1099
|
+
html += `<div class="timeline-item">
|
|
1100
|
+
<div class="timeline-icon ${{iconClass}}">${{svgIcon}}</div>
|
|
1101
|
+
<div class="timeline-content">
|
|
1102
|
+
<div class="event-header">
|
|
1103
|
+
<span class="event-author ${{iconClass}}">${{DOMPurify.sanitize(author)}}</span>
|
|
1104
|
+
<span class="event-role">${{DOMPurify.sanitize(role)}}</span>
|
|
1105
|
+
</div>
|
|
1106
|
+
<div class="event-body">`;
|
|
1107
|
+
|
|
1108
|
+
if (event.content && event.content.parts) {{
|
|
1109
|
+
event.content.parts.forEach(part => {{
|
|
1110
|
+
if (part.text) {{
|
|
1111
|
+
html += `<div>${{DOMPurify.sanitize(marked.parse(String(part.text)))}}</div>`;
|
|
1112
|
+
}} else if (part.function_call) {{
|
|
1113
|
+
const fnName = part.function_call.name;
|
|
1114
|
+
const fnArgs = JSON.stringify(part.function_call.args, null, 2);
|
|
1115
|
+
html += `<div class="function-call-title">>_ Function Call: ${{DOMPurify.sanitize(fnName)}}</div>
|
|
1116
|
+
<pre class="dark-code-block">${{DOMPurify.sanitize(fnArgs)}}</pre>`;
|
|
1117
|
+
}} else if (part.function_response) {{
|
|
1118
|
+
const fnName = part.function_response.name;
|
|
1119
|
+
let fnRes = part.function_response.response;
|
|
1120
|
+
if(typeof fnRes === 'object' && fnRes !== null && fnRes.result !== undefined) {{
|
|
1121
|
+
fnRes = fnRes.result;
|
|
1122
|
+
}}
|
|
1123
|
+
const fnResStr = JSON.stringify(fnRes, null, 2);
|
|
1124
|
+
html += `<div class="function-response-title">
|
|
1125
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 4px;"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>
|
|
1126
|
+
Tool Output: ${{DOMPurify.sanitize(fnName)}}
|
|
1127
|
+
</div>
|
|
1128
|
+
<pre class="dark-code-block">${{DOMPurify.sanitize(fnResStr)}}</pre>`;
|
|
1129
|
+
}}
|
|
1130
|
+
}});
|
|
1131
|
+
}} else {{
|
|
1132
|
+
html += `<div><pre class="raw-json-container">${{DOMPurify.sanitize(JSON.stringify(event.content, null, 2))}}</pre></div>`;
|
|
1133
|
+
}}
|
|
1134
|
+
|
|
1135
|
+
html += `</div></div></div>`;
|
|
1136
|
+
}});
|
|
1137
|
+
}}
|
|
1138
|
+
html += `</details>`;
|
|
1139
|
+
}});
|
|
1140
|
+
html += '</div>';
|
|
1141
|
+
return html;
|
|
1142
|
+
}}
|
|
1143
|
+
|
|
1144
|
+
function renderSummary(summaryMetrics, metadata) {{
|
|
1145
|
+
const container = document.getElementById('summary-section');
|
|
1146
|
+
if (!summaryMetrics || summaryMetrics.length === 0) {{ container.innerHTML = '<h2>Summary Metrics</h2><p>No summary metrics.</p>'; return; }}
|
|
1147
|
+
const candidateNames = (metadata.candidate_names && metadata.candidate_names.length) ? metadata.candidate_names : null;
|
|
1148
|
+
let table = '<h2>Summary Metrics</h2><table><thead><tr><th>Metric</th><th>Mean Score</th><th>Std Dev</th><th>Win/Tie Rates</th></tr></thead><tbody>';
|
|
1149
|
+
summaryMetrics.forEach(m => {{
|
|
1150
|
+
let winRateText = 'N/A';
|
|
1151
|
+
if (m.win_rates) {{
|
|
1152
|
+
winRateText = m.win_rates.map((rate, i) => `<b>${{candidateNames ? candidateNames[i] : `Candidate #${{i+1}}`}}</b> wins: <b>${{(rate * 100).toFixed(1)}}%</b>`).join('<br>');
|
|
1153
|
+
if (m.tie_rate !== undefined) {{ winRateText += `<br>Ties: <b>${{(m.tie_rate * 100).toFixed(1)}}%</b>`; }}
|
|
1154
|
+
}}
|
|
1155
|
+
table += `<tr><td>${{m.metric_name}}</td><td>${{m.mean_score.toFixed(4)}}</td><td>${{m.stdev_score.toFixed(4)}}</td><td>${{winRateText}}</td></tr>`;
|
|
1156
|
+
}});
|
|
1157
|
+
container.innerHTML = table + '</tbody></table>';
|
|
1158
|
+
}}
|
|
1159
|
+
|
|
1160
|
+
function renderDetails(caseResults, metadata) {{
|
|
1161
|
+
const container = document.getElementById('details-section');
|
|
1162
|
+
container.innerHTML = '<h2>Detailed Comparison</h2>';
|
|
1163
|
+
if (!caseResults || caseResults.length === 0) {{ container.innerHTML += '<p>No detailed results.</p>'; return; }}
|
|
1164
|
+
|
|
1165
|
+
const datasetRows = metadata.dataset || [];
|
|
1166
|
+
const candidateNames = (metadata.candidate_names && metadata.candidate_names.length) ? metadata.candidate_names : null;
|
|
1167
|
+
|
|
1168
|
+
caseResults.forEach((caseResult, i) => {{
|
|
1169
|
+
const original_case = datasetRows[caseResult.eval_case_index] || {{}};
|
|
1170
|
+
const isValEmpty = (val) => !val || val === 'None' || val === 'nan' || String(val).trim() === '';
|
|
1171
|
+
|
|
1172
|
+
const promptText = isValEmpty(original_case.prompt_display_text) ? '' : original_case.prompt_display_text;
|
|
1173
|
+
const promptJson = original_case.prompt_raw_json;
|
|
1174
|
+
const reference = isValEmpty(original_case.reference) ? '' : original_case.reference;
|
|
1175
|
+
|
|
1176
|
+
let agentData = original_case.agent_data;
|
|
1177
|
+
if (typeof agentData === 'string') {{
|
|
1178
|
+
try {{ agentData = JSON.parse(agentData); }} catch(e) {{}}
|
|
1179
|
+
}}
|
|
1180
|
+
|
|
1181
|
+
let isRefAgentData = false;
|
|
1182
|
+
let refAgentDataObj = null;
|
|
1183
|
+
if (reference) {{
|
|
1184
|
+
try {{
|
|
1185
|
+
let parsed = typeof reference === 'string' ? JSON.parse(reference) : reference;
|
|
1186
|
+
if (parsed && parsed.turns) {{
|
|
1187
|
+
isRefAgentData = true;
|
|
1188
|
+
refAgentDataObj = parsed;
|
|
1189
|
+
}}
|
|
1190
|
+
}} catch(e) {{}}
|
|
1191
|
+
}}
|
|
1192
|
+
|
|
1193
|
+
let card = `<details open><summary>Case #${{caseResult.eval_case_index}}</summary>`;
|
|
1194
|
+
|
|
1195
|
+
if (agentData && agentData.agents && Object.keys(agentData.agents).length > 0) {{
|
|
1196
|
+
card += `<details open class="system-topology-details">
|
|
1197
|
+
<summary>System Topology</summary>
|
|
1198
|
+
${{formatSystemTopology(agentData.agents)}}
|
|
1199
|
+
</details>`;
|
|
1200
|
+
}}
|
|
1201
|
+
|
|
1202
|
+
if (promptText) {{
|
|
1203
|
+
card += `<div class="prompt-container"><strong>Prompt:</strong><br>${{DOMPurify.sanitize(marked.parse(String(promptText)))}}</div>`;
|
|
1204
|
+
}}
|
|
1205
|
+
|
|
1206
|
+
if (promptJson && promptJson !== '""' && promptJson !== 'null' && promptJson !== '{{}}') {{
|
|
1207
|
+
card += `<details class="raw-json-details"><summary>View Raw Prompt JSON</summary><pre class="raw-json-container">${{DOMPurify.sanitize(promptJson)}}</pre></details>`;
|
|
1208
|
+
}}
|
|
1209
|
+
|
|
1210
|
+
let hasTrace = agentData && agentData.turns;
|
|
1211
|
+
let hasRef = !!reference;
|
|
1212
|
+
|
|
1213
|
+
if (hasTrace || hasRef) {{
|
|
1214
|
+
card += `<div style="display: flex; gap: 1rem; margin-top: 16px; margin-bottom: 16px;">`;
|
|
1215
|
+
|
|
1216
|
+
if (hasTrace) {{
|
|
1217
|
+
let traceContent = formatAgentData(agentData);
|
|
1218
|
+
card += `<div style="flex: 1; min-width: 0;">
|
|
1219
|
+
<details open class="conversation-trace-details" style="margin: 0; height: 100%;">
|
|
1220
|
+
<summary>Conversation Trace</summary>
|
|
1221
|
+
<div style="font-size:13px; color:#5f6368; margin-bottom:12px;">Sequence of multi-agent events across turns</div>
|
|
1222
|
+
<div class="agent-trace-container">${{traceContent}}</div>
|
|
1223
|
+
</details>
|
|
1224
|
+
</div>`;
|
|
1225
|
+
}}
|
|
1226
|
+
|
|
1227
|
+
if (hasRef) {{
|
|
1228
|
+
card += `<div style="flex: 1; min-width: 0;">`;
|
|
1229
|
+
if (isRefAgentData) {{
|
|
1230
|
+
let refTraceContent = formatAgentData(refAgentDataObj);
|
|
1231
|
+
card += `<details open class="conversation-trace-details" style="margin: 0; height: 100%;">
|
|
1232
|
+
<summary>Reference</summary>
|
|
1233
|
+
<div style="font-size:13px; color:#5f6368; margin-bottom:12px;">Sequence of multi-agent events across turns</div>
|
|
1234
|
+
<div class="agent-trace-container">${{refTraceContent}}</div>
|
|
1235
|
+
</details>`;
|
|
1236
|
+
}} else {{
|
|
1237
|
+
card += `<div class="reference-container" style="margin: 0; height: 100%;"><strong>Reference</strong><br>${{DOMPurify.sanitize(marked.parse(String(reference)))}}</div>`;
|
|
1238
|
+
}}
|
|
1239
|
+
card += `</div>`;
|
|
1240
|
+
}}
|
|
1241
|
+
|
|
1242
|
+
card += `</div>`;
|
|
1243
|
+
}}
|
|
1244
|
+
|
|
1245
|
+
card += `<div class="responses-grid">`;
|
|
1246
|
+
|
|
1247
|
+
(caseResult.response_candidate_results || []).forEach((candidate, j) => {{
|
|
1248
|
+
const candidateName = candidateNames ? candidateNames[j] : `Candidate #${{j + 1}}`;
|
|
1249
|
+
const displayText = isValEmpty(candidate.display_text) ? '' : candidate.display_text;
|
|
1250
|
+
const rawJsonResponse = candidate.raw_json;
|
|
1251
|
+
|
|
1252
|
+
card += `<div class="response-column"><h4>${{candidateName}}</h4>`;
|
|
1253
|
+
|
|
1254
|
+
if (displayText) {{
|
|
1255
|
+
card += `<div class="response-text-container">${{DOMPurify.sanitize(marked.parse(String(displayText)))}}</div>`;
|
|
1256
|
+
}}
|
|
1257
|
+
|
|
1258
|
+
if (rawJsonResponse && rawJsonResponse !== '""' && rawJsonResponse !== 'null' && rawJsonResponse !== '{{}}') {{
|
|
1259
|
+
card += `<details class="raw-json-details"><summary>View Raw Response JSON</summary><pre class="raw-json-container">${{DOMPurify.sanitize(rawJsonResponse)}}</pre></details>`;
|
|
1260
|
+
}}
|
|
1261
|
+
|
|
1262
|
+
card += `<h5 style="margin-top: 16px; margin-bottom: 8px; font-size: 1.1em; color: #3c4043;">Metrics</h5><div class="metrics-list">`;
|
|
1263
|
+
Object.entries(candidate.metric_results || {{}}).forEach(([name, val]) => {{
|
|
1264
|
+
let explanationHandled = false;
|
|
1265
|
+
let bubbles = '';
|
|
1266
|
+
|
|
1267
|
+
if (val.rubric_verdicts && val.rubric_verdicts.length > 0) {{
|
|
1268
|
+
bubbles += '<div class="rubric-bubble-container" style="margin-top: 8px;">';
|
|
1269
|
+
val.rubric_verdicts.forEach(verdict => {{
|
|
1270
|
+
const rubricDescription = verdict.evaluated_rubric && verdict.evaluated_rubric.content && verdict.evaluated_rubric.content.property ? verdict.evaluated_rubric.content.property.description : 'N/A';
|
|
1271
|
+
const verdictText = verdict.verdict ? '<span class="pass">Pass</span>' : '<span class="fail">Fail</span>';
|
|
1272
|
+
const verdictJson = JSON.stringify(verdict, null, 2);
|
|
1273
|
+
bubbles += `
|
|
1274
|
+
<details class="rubric-details">
|
|
1275
|
+
<summary class="rubric-bubble">${{verdictText}}: ${{DOMPurify.sanitize(rubricDescription)}}</summary>
|
|
1276
|
+
<pre class="raw-json-container">${{DOMPurify.sanitize(verdictJson)}}</pre>
|
|
1277
|
+
</details>`;
|
|
1278
|
+
}});
|
|
1279
|
+
bubbles += '</div>';
|
|
1280
|
+
}}
|
|
1281
|
+
|
|
1282
|
+
let scoreDisplay = val.score != null ? val.score.toFixed(2) : 'N/A';
|
|
1283
|
+
let metricContent = '';
|
|
1284
|
+
|
|
1285
|
+
if (val.explanation && !explanationHandled) {{
|
|
1286
|
+
metricContent += `<div class="explanation" style="margin-top:0; margin-bottom: 12px;">${{DOMPurify.sanitize(marked.parse(String(val.explanation)))}}</div>`;
|
|
1287
|
+
}}
|
|
1288
|
+
if (bubbles) {{
|
|
1289
|
+
metricContent += bubbles;
|
|
1290
|
+
}}
|
|
1291
|
+
if (!metricContent) {{
|
|
1292
|
+
metricContent = `<div style="color: #80868b; font-style: italic; font-size: 13px;">No additional details.</div>`;
|
|
1293
|
+
}}
|
|
1294
|
+
|
|
1295
|
+
card += `
|
|
1296
|
+
<details class="metric-details" open>
|
|
1297
|
+
<summary class="metric-summary">
|
|
1298
|
+
<div class="metric-name-wrapper">${{DOMPurify.sanitize(name)}}</div>
|
|
1299
|
+
<div class="metric-score">${{DOMPurify.sanitize(scoreDisplay)}}</div>
|
|
1300
|
+
</summary>
|
|
1301
|
+
<div class="metric-body">
|
|
1302
|
+
${{metricContent}}
|
|
1303
|
+
</div>
|
|
1304
|
+
</details>
|
|
1305
|
+
`;
|
|
1306
|
+
}});
|
|
1307
|
+
card += '</div></div>';
|
|
1308
|
+
}});
|
|
1309
|
+
container.innerHTML += card + '</div></details>';
|
|
1310
|
+
}});
|
|
1311
|
+
}}
|
|
1312
|
+
renderSummary(vizData_vertex_eval_sdk.summary_metrics, vizData_vertex_eval_sdk.metadata);
|
|
1313
|
+
renderDetails(vizData_vertex_eval_sdk.eval_case_results, vizData_vertex_eval_sdk.metadata);
|
|
1314
|
+
</script>
|
|
1315
|
+
</body>
|
|
1316
|
+
</html>
|
|
1317
|
+
"""
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
def get_inference_html(dataframe_json: str) -> str:
|
|
1322
|
+
"""Returns a self-contained HTML for displaying inference results."""
|
|
1323
|
+
payload_b64 = _encode_to_base64(dataframe_json)
|
|
1324
|
+
return textwrap.dedent(
|
|
1325
|
+
f"""
|
|
1326
|
+
<!DOCTYPE html>
|
|
1327
|
+
<html>
|
|
1328
|
+
<head>
|
|
1329
|
+
<meta charset="UTF-8">
|
|
1330
|
+
<title>Evaluation Dataset</title>
|
|
1331
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
1332
|
+
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
|
1333
|
+
<style>
|
|
1334
|
+
body {{ font-family: 'Roboto', sans-serif; margin: 2em; background-color: #f8f9fa; color: #202124;}}
|
|
1335
|
+
.container {{ max-width: 95%; margin: 20px auto; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }}
|
|
1336
|
+
h1 {{ color: #3c4043; border-bottom: 2px solid #4285F4; padding-bottom: 8px; }}
|
|
1337
|
+
table {{ border-collapse: collapse; width: 100%; table-layout: fixed; }}
|
|
1338
|
+
th, td {{ border: 1px solid #dadce0; padding: 12px; text-align: left; vertical-align: top; }}
|
|
1339
|
+
th {{ background-color: #f2f2f2; font-weight: 500;}}
|
|
1340
|
+
td > div {{ white-space: pre-wrap; word-wrap: break-word; max-height: 400px; overflow-y: auto; overflow-wrap: break-word; }}
|
|
1341
|
+
.raw-json-details summary {{ font-size: 0.9em; cursor: pointer; color: #5f6368; }}
|
|
1342
|
+
.raw-json-container {{ white-space: pre-wrap; word-wrap: break-word; max-height: 300px; overflow-y: auto; background-color: #f1f1f1; padding: 10px; border-radius: 4px; margin-top: 8px; }}
|
|
1343
|
+
.rubric-group-title {{ font-weight: bold; margin-bottom: 10px; display: block; }}
|
|
1344
|
+
.rubric-bubble-container {{ display: flex; flex-wrap: wrap; gap: 8px; }}
|
|
1345
|
+
.rubric-details {{ border: none; padding: 0; margin: 0; }}
|
|
1346
|
+
.rubric-bubble {{
|
|
1347
|
+
display: inline-flex;
|
|
1348
|
+
align-items: center;
|
|
1349
|
+
background-color: #e8f0fe;
|
|
1350
|
+
color: #1967d2;
|
|
1351
|
+
border-radius: 16px;
|
|
1352
|
+
padding: 8px 12px;
|
|
1353
|
+
font-size: 0.9em;
|
|
1354
|
+
cursor: pointer;
|
|
1355
|
+
list-style: none; /* Hide default marker in Safari */
|
|
1356
|
+
}}
|
|
1357
|
+
.rubric-bubble::-webkit-details-marker {{ display: none; }} /* Hide default marker in Chrome */
|
|
1358
|
+
.rubric-bubble::before {{
|
|
1359
|
+
content: '►';
|
|
1360
|
+
margin-right: 8px;
|
|
1361
|
+
font-size: 0.8em;
|
|
1362
|
+
transition: transform 0.2s;
|
|
1363
|
+
}}
|
|
1364
|
+
.rubric-details[open] > .rubric-bubble::before {{
|
|
1365
|
+
transform: rotate(90deg);
|
|
1366
|
+
}}
|
|
1367
|
+
</style>
|
|
1368
|
+
</head>
|
|
1369
|
+
<body>
|
|
1370
|
+
<div class="container">
|
|
1371
|
+
<h1>Evaluation Dataset</h1>
|
|
1372
|
+
<div id="results-table"></div>
|
|
1373
|
+
</div>
|
|
1374
|
+
<script>
|
|
1375
|
+
var vizData_vertex_eval_sdk = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob("{payload_b64}"), c => c.charCodeAt(0))));
|
|
1376
|
+
var container_vertex_eval_sdk = document.getElementById('results-table');
|
|
1377
|
+
|
|
1378
|
+
function renderRubrics(cellValue) {{
|
|
1379
|
+
let content = '';
|
|
1380
|
+
let rubricData = cellValue;
|
|
1381
|
+
if (typeof rubricData === 'string') {{
|
|
1382
|
+
try {{
|
|
1383
|
+
rubricData = JSON.parse(rubricData);
|
|
1384
|
+
}} catch (e) {{
|
|
1385
|
+
console.error("Error parsing rubric_groups JSON:", e, rubricData);
|
|
1386
|
+
return `<div>Error parsing rubrics.</div>`;
|
|
1387
|
+
}}
|
|
1388
|
+
}}
|
|
1389
|
+
|
|
1390
|
+
if (typeof rubricData !== 'object' || rubricData === null) {{
|
|
1391
|
+
return `<div>Invalid rubric data.</div>`;
|
|
1392
|
+
}}
|
|
1393
|
+
|
|
1394
|
+
for (const groupName in rubricData) {{
|
|
1395
|
+
const rubrics = rubricData[groupName];
|
|
1396
|
+
content += `<div class="rubric-group-title">${{groupName}}</div>`;
|
|
1397
|
+
if (Array.isArray(rubrics) && rubrics.length > 0) {{
|
|
1398
|
+
content += '<div class="rubric-bubble-container">';
|
|
1399
|
+
rubrics.forEach((rubric, index) => {{
|
|
1400
|
+
const rubricJson = JSON.stringify(rubric, null, 2);
|
|
1401
|
+
const description = rubric.content && rubric.content.property ? rubric.content.property.description : 'N/A';
|
|
1402
|
+
content += `
|
|
1403
|
+
<details class="rubric-details">
|
|
1404
|
+
<summary class="rubric-bubble">${{DOMPurify.sanitize(description)}}</summary>
|
|
1405
|
+
<pre class="raw-json-container">${{DOMPurify.sanitize(rubricJson)}}</pre>
|
|
1406
|
+
</details>`;
|
|
1407
|
+
}});
|
|
1408
|
+
content += '</div>';
|
|
1409
|
+
}}
|
|
1410
|
+
}}
|
|
1411
|
+
return `<div>${{content}}</div>`;
|
|
1412
|
+
}}
|
|
1413
|
+
|
|
1414
|
+
function renderCell(cellValue, header) {{
|
|
1415
|
+
let cellContent = '';
|
|
1416
|
+
if (header === 'rubric_groups') {{
|
|
1417
|
+
return `<td>${{renderRubrics(cellValue)}}</td>`;
|
|
1418
|
+
}}
|
|
1419
|
+
|
|
1420
|
+
if (cellValue && typeof cellValue === 'object' && cellValue.display_text !== undefined) {{
|
|
1421
|
+
cellContent += `<div>${{DOMPurify.sanitize(marked.parse(String(cellValue.display_text)))}}</div>`;
|
|
1422
|
+
if (cellValue.raw_json) {{
|
|
1423
|
+
cellContent += `<details class="raw-json-details"><summary>View Raw JSON</summary><pre class="raw-json-container">${{DOMPurify.sanitize(cellValue.raw_json)}}</pre></details>`;
|
|
1424
|
+
}}
|
|
1425
|
+
}} else {{
|
|
1426
|
+
const cellDisplay = cellValue === null || cellValue === undefined ? '' : String(cellValue);
|
|
1427
|
+
cellContent = `<div>${{DOMPurify.sanitize(marked.parse(cellDisplay))}}</div>`;
|
|
1428
|
+
}}
|
|
1429
|
+
return `<td>${{cellContent}}</td>`;
|
|
1430
|
+
}}
|
|
1431
|
+
|
|
1432
|
+
if (!vizData_vertex_eval_sdk || vizData_vertex_eval_sdk.length === 0) {{ container_vertex_eval_sdk.innerHTML = "<p>No data.</p>"; }}
|
|
1433
|
+
else {{
|
|
1434
|
+
let table = '<table><thead><tr>';
|
|
1435
|
+
const headers = Object.keys(vizData_vertex_eval_sdk[0] || {{}});
|
|
1436
|
+
headers.forEach(h => table += `<th>${{h}}</th>`);
|
|
1437
|
+
table += '</tr></thead><tbody>';
|
|
1438
|
+
vizData_vertex_eval_sdk.forEach(row => {{
|
|
1439
|
+
table += '<tr>';
|
|
1440
|
+
headers.forEach(header => {{
|
|
1441
|
+
table += renderCell(row[header], header);
|
|
1442
|
+
}});
|
|
1443
|
+
table += '</tr>';
|
|
1444
|
+
}});
|
|
1445
|
+
container_vertex_eval_sdk.innerHTML = table + '</tbody></table>';
|
|
1446
|
+
}}
|
|
1447
|
+
</script>
|
|
1448
|
+
</body>
|
|
1449
|
+
</html>
|
|
1450
|
+
"""
|
|
1451
|
+
)
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
def display_evaluation_result(
|
|
1455
|
+
eval_result_obj: types.EvaluationResult,
|
|
1456
|
+
candidate_names: Optional[list[str]] = None,
|
|
1457
|
+
) -> None:
|
|
1458
|
+
"""Displays evaluation result in an IPython environment."""
|
|
1459
|
+
if not _is_ipython_env():
|
|
1460
|
+
logger.warning("Skipping display: not in an IPython environment.")
|
|
1461
|
+
return
|
|
1462
|
+
else:
|
|
1463
|
+
from IPython import display
|
|
1464
|
+
|
|
1465
|
+
try:
|
|
1466
|
+
result_dump = eval_result_obj.model_dump(
|
|
1467
|
+
mode="json", exclude_none=True, exclude={"evaluation_dataset"}
|
|
1468
|
+
)
|
|
1469
|
+
except errors.PydanticSerializationError as e:
|
|
1470
|
+
logger.error(
|
|
1471
|
+
"Serialization Error: %s\nCould not display the evaluation "
|
|
1472
|
+
"result due to a data serialization issue. Please check the "
|
|
1473
|
+
"content of the EvaluationResult object.",
|
|
1474
|
+
e,
|
|
1475
|
+
)
|
|
1476
|
+
return
|
|
1477
|
+
except Exception as e:
|
|
1478
|
+
logger.error("Failed to serialize EvaluationResult: %s", e, exc_info=True)
|
|
1479
|
+
raise
|
|
1480
|
+
|
|
1481
|
+
input_dataset_list = eval_result_obj.evaluation_dataset
|
|
1482
|
+
is_comparison = input_dataset_list and len(input_dataset_list) > 1
|
|
1483
|
+
|
|
1484
|
+
metadata_payload = result_dump.get("metadata", {})
|
|
1485
|
+
metadata_payload["candidate_names"] = candidate_names or metadata_payload.get(
|
|
1486
|
+
"candidate_names"
|
|
1487
|
+
)
|
|
1488
|
+
|
|
1489
|
+
if is_comparison and input_dataset_list:
|
|
1490
|
+
if input_dataset_list[0]:
|
|
1491
|
+
metadata_payload["dataset"] = _extract_dataset_rows(input_dataset_list[0])
|
|
1492
|
+
|
|
1493
|
+
if "eval_case_results" in result_dump:
|
|
1494
|
+
for case_res in result_dump["eval_case_results"]:
|
|
1495
|
+
for resp_idx, cand_res in enumerate(
|
|
1496
|
+
case_res.get("response_candidate_results", [])
|
|
1497
|
+
):
|
|
1498
|
+
if (
|
|
1499
|
+
input_dataset_list is not None
|
|
1500
|
+
and resp_idx < len(input_dataset_list)
|
|
1501
|
+
and input_dataset_list[resp_idx]
|
|
1502
|
+
):
|
|
1503
|
+
rows = _extract_dataset_rows(input_dataset_list[resp_idx])
|
|
1504
|
+
case_idx = case_res.get("eval_case_index")
|
|
1505
|
+
if case_idx is not None and case_idx < len(rows):
|
|
1506
|
+
original_case = rows[case_idx]
|
|
1507
|
+
cand_res["display_text"] = original_case[
|
|
1508
|
+
"response_display_text"
|
|
1509
|
+
]
|
|
1510
|
+
cand_res["raw_json"] = original_case["response_raw_json"]
|
|
1511
|
+
|
|
1512
|
+
win_rates = eval_result_obj.win_rates if eval_result_obj.win_rates else {}
|
|
1513
|
+
if "summary_metrics" in result_dump:
|
|
1514
|
+
for summary in result_dump["summary_metrics"]:
|
|
1515
|
+
if summary.get("metric_name") in win_rates:
|
|
1516
|
+
summary.update(win_rates[summary["metric_name"]])
|
|
1517
|
+
|
|
1518
|
+
result_dump["metadata"] = metadata_payload
|
|
1519
|
+
html_content = get_comparison_html(json.dumps(result_dump))
|
|
1520
|
+
else:
|
|
1521
|
+
single_dataset = input_dataset_list[0] if input_dataset_list else None
|
|
1522
|
+
processed_rows = []
|
|
1523
|
+
if single_dataset is not None:
|
|
1524
|
+
processed_rows = _extract_dataset_rows(single_dataset)
|
|
1525
|
+
metadata_payload["dataset"] = processed_rows
|
|
1526
|
+
|
|
1527
|
+
if "eval_case_results" in result_dump and processed_rows:
|
|
1528
|
+
for case_res in result_dump["eval_case_results"]:
|
|
1529
|
+
case_idx = case_res.get("eval_case_index")
|
|
1530
|
+
if (
|
|
1531
|
+
case_idx is not None
|
|
1532
|
+
and case_idx < len(processed_rows)
|
|
1533
|
+
and case_res.get("response_candidate_results")
|
|
1534
|
+
):
|
|
1535
|
+
original_case = processed_rows[case_idx]
|
|
1536
|
+
cand_res = case_res["response_candidate_results"][0]
|
|
1537
|
+
cand_res["display_text"] = original_case[
|
|
1538
|
+
"response_display_text"
|
|
1539
|
+
]
|
|
1540
|
+
cand_res["raw_json"] = original_case["response_raw_json"]
|
|
1541
|
+
|
|
1542
|
+
result_dump["metadata"] = metadata_payload
|
|
1543
|
+
html_content = get_evaluation_html(json.dumps(result_dump))
|
|
1544
|
+
|
|
1545
|
+
display.display(display.HTML(html_content))
|
|
1546
|
+
|
|
1547
|
+
|
|
1548
|
+
def display_evaluation_dataset(eval_dataset_obj: types.EvaluationDataset) -> None:
|
|
1549
|
+
"""Displays an evaluation dataset in an IPython environment."""
|
|
1550
|
+
if not _is_ipython_env():
|
|
1551
|
+
logger.warning("Skipping display: not in an IPython environment.")
|
|
1552
|
+
return
|
|
1553
|
+
else:
|
|
1554
|
+
from IPython import display
|
|
1555
|
+
|
|
1556
|
+
df = eval_dataset_obj.eval_dataset_df
|
|
1557
|
+
|
|
1558
|
+
# Fall back to eval_cases when eval_dataset_df is not populated (e.g.
|
|
1559
|
+
# when the dataset was constructed manually with eval_cases).
|
|
1560
|
+
if df is None or df.empty:
|
|
1561
|
+
if eval_dataset_obj.eval_cases:
|
|
1562
|
+
df = _evals_common._eval_cases_to_dataframe(eval_dataset_obj.eval_cases)
|
|
1563
|
+
if df is None or df.empty:
|
|
1564
|
+
logger.warning("No inference data to display.")
|
|
1565
|
+
return
|
|
1566
|
+
|
|
1567
|
+
processed_rows = []
|
|
1568
|
+
|
|
1569
|
+
for _, row in df.iterrows():
|
|
1570
|
+
processed_row = {}
|
|
1571
|
+
for col_name, cell_value in row.items():
|
|
1572
|
+
if col_name in ["prompt", "request", "response"]:
|
|
1573
|
+
processed_row[col_name] = _extract_text_and_raw_json(cell_value)
|
|
1574
|
+
elif col_name == "rubric_groups":
|
|
1575
|
+
# Special handling for rubric_groups to keep it as a dict
|
|
1576
|
+
if isinstance(cell_value, dict):
|
|
1577
|
+
processed_row[col_name] = {
|
|
1578
|
+
k: [ # type: ignore[misc]
|
|
1579
|
+
(
|
|
1580
|
+
v_item.model_dump(mode="json")
|
|
1581
|
+
if hasattr(v_item, "model_dump")
|
|
1582
|
+
else v_item
|
|
1583
|
+
)
|
|
1584
|
+
for v_item in v
|
|
1585
|
+
]
|
|
1586
|
+
for k, v in cell_value.items()
|
|
1587
|
+
}
|
|
1588
|
+
else:
|
|
1589
|
+
processed_row[col_name] = cell_value
|
|
1590
|
+
else:
|
|
1591
|
+
if isinstance(cell_value, (dict, list)):
|
|
1592
|
+
processed_row[col_name] = json.dumps( # type: ignore[assignment]
|
|
1593
|
+
cell_value, ensure_ascii=False, default=_pydantic_serializer
|
|
1594
|
+
)
|
|
1595
|
+
else:
|
|
1596
|
+
processed_row[col_name] = cell_value
|
|
1597
|
+
processed_rows.append(processed_row)
|
|
1598
|
+
|
|
1599
|
+
dataframe_json_string = json.dumps(processed_rows, ensure_ascii=False, default=str)
|
|
1600
|
+
html_content = get_inference_html(dataframe_json_string)
|
|
1601
|
+
display.display(display.HTML(html_content))
|
|
1602
|
+
|
|
1603
|
+
|
|
1604
|
+
def get_loss_analysis_html(loss_analysis_json: str) -> str:
|
|
1605
|
+
"""Returns self-contained HTML for loss pattern analysis visualization."""
|
|
1606
|
+
payload_b64 = _encode_to_base64(loss_analysis_json)
|
|
1607
|
+
return textwrap.dedent(
|
|
1608
|
+
f"""
|
|
1609
|
+
<!DOCTYPE html>
|
|
1610
|
+
<html>
|
|
1611
|
+
<head>
|
|
1612
|
+
<meta charset="UTF-8">
|
|
1613
|
+
<title>Loss Pattern Analysis</title>
|
|
1614
|
+
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
|
1615
|
+
<style>
|
|
1616
|
+
body {{ font-family: 'Roboto', 'Helvetica', sans-serif; margin: 2em; background-color: #f8f9fa; color: #202124; }}
|
|
1617
|
+
.container {{ max-width: 1200px; margin: 20px auto; padding: 20px; background-color: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }}
|
|
1618
|
+
h1, h2, h3 {{ color: #3c4043; }}
|
|
1619
|
+
h1 {{ border-bottom: 2px solid #4285F4; padding-bottom: 8px; }}
|
|
1620
|
+
h2 {{ border-bottom: 1px solid #dadce0; padding-bottom: 8px; }}
|
|
1621
|
+
table {{ border-collapse: collapse; width: 100%; margin: 1em 0; }}
|
|
1622
|
+
th, td {{ border: 1px solid #dadce0; padding: 12px; text-align: left; vertical-align: top; }}
|
|
1623
|
+
th {{ background-color: #f2f2f2; font-weight: 500; }}
|
|
1624
|
+
details {{ border: 1px solid #dadce0; border-radius: 8px; padding: 16px; margin-bottom: 16px; background: #fff; }}
|
|
1625
|
+
summary {{ font-weight: 500; font-size: 1.1em; cursor: pointer; }}
|
|
1626
|
+
|
|
1627
|
+
.metric-header {{ display: flex; align-items: baseline; gap: 12px; margin-bottom: 4px; }}
|
|
1628
|
+
.metric-label {{ color: #1a73e8; font-weight: 600; font-size: 1.1em; }}
|
|
1629
|
+
.candidate-label {{ color: #5f6368; font-size: 0.95em; }}
|
|
1630
|
+
.item-count {{ font-weight: bold; font-size: 16px; color: #1a73e8; }}
|
|
1631
|
+
|
|
1632
|
+
.cluster-card {{ background: #fff; border: 1px solid #dadce0; border-radius: 8px; margin-bottom: 12px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); overflow: hidden; }}
|
|
1633
|
+
.cluster-summary {{ list-style: none; cursor: pointer; padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; background: #f8f9fa; margin: 0; outline: none; }}
|
|
1634
|
+
.cluster-summary::-webkit-details-marker {{ display: none; }}
|
|
1635
|
+
.cluster-card[open] .cluster-summary {{ border-bottom: 1px solid #dadce0; }}
|
|
1636
|
+
|
|
1637
|
+
.cluster-name {{ display: flex; align-items: center; font-weight: 600; color: #3c4043; font-size: 14px; }}
|
|
1638
|
+
.cluster-name::before {{ content: '\\25B6'; font-size: 0.8em; margin-right: 8px; transition: transform 0.2s; color: #5f6368; }}
|
|
1639
|
+
.cluster-card[open] .cluster-name::before {{ transform: rotate(90deg); }}
|
|
1640
|
+
|
|
1641
|
+
.l1-pill {{ display: inline-block; background-color: #e8f0fe; color: #1967d2; border-radius: 16px; padding: 2px 10px; font-size: 0.85em; font-weight: 500; margin-right: 6px; }}
|
|
1642
|
+
.cluster-body {{ padding: 16px; }}
|
|
1643
|
+
.cluster-description {{ color: #5f6368; font-size: 0.95em; margin-bottom: 12px; line-height: 1.5; }}
|
|
1644
|
+
|
|
1645
|
+
.example-card {{ background: #f8f9fa; border: 1px solid #eee; border-radius: 6px; padding: 12px; margin-bottom: 8px; }}
|
|
1646
|
+
.example-label {{ font-weight: 600; font-size: 0.9em; color: #3c4043; margin-bottom: 6px; padding-bottom: 6px; border-bottom: 1px solid #eee; }}
|
|
1647
|
+
.example-scenario {{ background: #e8f0fe; border-radius: 6px; padding: 8px 12px; margin: 6px 0; font-size: 0.9em; color: #1967d2; display: flex; align-items: flex-start; gap: 6px; }}
|
|
1648
|
+
.example-scenario-icon {{ flex-shrink: 0; margin-top: 1px; }}
|
|
1649
|
+
.example-scenario-text {{ word-break: break-word; }}
|
|
1650
|
+
.example-rubric {{ display: inline-block; background-color: #fce8e6; color: #c5221f; border-radius: 12px; padding: 2px 10px; font-size: 0.85em; font-weight: 500; margin-right: 6px; margin-bottom: 4px; }}
|
|
1651
|
+
.example-rationale {{ color: #5f6368; font-size: 0.9em; line-height: 1.5; margin-top: 6px; background: #f1f1f1; padding: 8px 12px; border-radius: 4px; white-space: pre-wrap; word-wrap: break-word; }}
|
|
1652
|
+
.example-section-label {{ font-size: 0.8em; font-weight: 500; color: #5f6368; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; margin-top: 8px; }}
|
|
1653
|
+
.rubric-description {{ color: #5f6368; font-size: 0.9em; margin: 4px 0 6px 0; line-height: 1.4; }}
|
|
1654
|
+
.examples-details {{ border: none; padding: 0; margin-top: 8px; }}
|
|
1655
|
+
.examples-details > summary {{ font-size: 0.95em; color: #1a73e8; cursor: pointer; }}
|
|
1656
|
+
.no-data {{ color: #5f6368; font-style: italic; padding: 16px; text-align: center; }}
|
|
1657
|
+
</style>
|
|
1658
|
+
</head>
|
|
1659
|
+
<body>
|
|
1660
|
+
<div class="container">
|
|
1661
|
+
<div id="loss-analysis-root"></div>
|
|
1662
|
+
</div>
|
|
1663
|
+
<script>
|
|
1664
|
+
(function() {{
|
|
1665
|
+
var data = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob("{payload_b64}"), c => c.charCodeAt(0))));
|
|
1666
|
+
|
|
1667
|
+
const root = document.getElementById('loss-analysis-root');
|
|
1668
|
+
const results = data.results || [];
|
|
1669
|
+
|
|
1670
|
+
if (results.length === 0) {{
|
|
1671
|
+
root.innerHTML = '<h1>Loss Pattern Analysis</h1><p class="no-data">No loss analysis results found.</p>';
|
|
1672
|
+
return;
|
|
1673
|
+
}}
|
|
1674
|
+
|
|
1675
|
+
let html = '<h1>Loss Pattern Analysis</h1>';
|
|
1676
|
+
|
|
1677
|
+
// Summary table
|
|
1678
|
+
if (results.length > 0) {{
|
|
1679
|
+
html += '<h2>Analysis Summary</h2><table><thead><tr><th>Metric</th><th>Candidate</th><th>Clusters</th><th>Total Failed Items</th></tr></thead><tbody>';
|
|
1680
|
+
results.forEach(r => {{
|
|
1681
|
+
const cfg = r.config || {{}};
|
|
1682
|
+
const clusters = r.clusters || [];
|
|
1683
|
+
const totalItems = clusters.reduce((sum, c) => sum + (c.item_count || 0), 0);
|
|
1684
|
+
html += '<tr>';
|
|
1685
|
+
html += '<td>' + DOMPurify.sanitize(cfg.metric || 'N/A') + '</td>';
|
|
1686
|
+
html += '<td>' + DOMPurify.sanitize(cfg.candidate || 'N/A') + '</td>';
|
|
1687
|
+
html += '<td>' + clusters.length + '</td>';
|
|
1688
|
+
html += '<td class="item-count">' + totalItems + '</td>';
|
|
1689
|
+
html += '</tr>';
|
|
1690
|
+
}});
|
|
1691
|
+
html += '</tbody></table>';
|
|
1692
|
+
}}
|
|
1693
|
+
|
|
1694
|
+
// Per-result detail sections
|
|
1695
|
+
results.forEach((r, ri) => {{
|
|
1696
|
+
const cfg = r.config || {{}};
|
|
1697
|
+
const clusters = r.clusters || [];
|
|
1698
|
+
const totalItems = clusters.reduce((sum, c) => sum + (c.item_count || 0), 0);
|
|
1699
|
+
|
|
1700
|
+
html += '<h2>';
|
|
1701
|
+
html += '<span class="metric-label">' + DOMPurify.sanitize(cfg.metric || 'Unknown Metric') + '</span>';
|
|
1702
|
+
if (cfg.candidate) html += ' <span class="candidate-label">/ ' + DOMPurify.sanitize(cfg.candidate) + '</span>';
|
|
1703
|
+
html += '</h2>';
|
|
1704
|
+
|
|
1705
|
+
if (clusters.length === 0) {{
|
|
1706
|
+
html += '<p class="no-data">No loss clusters found for this metric.</p>';
|
|
1707
|
+
return;
|
|
1708
|
+
}}
|
|
1709
|
+
|
|
1710
|
+
// Sort clusters by item_count descending
|
|
1711
|
+
clusters.sort((a, b) => (b.item_count || 0) - (a.item_count || 0));
|
|
1712
|
+
|
|
1713
|
+
// Cluster detail cards
|
|
1714
|
+
clusters.forEach((c, ci) => {{
|
|
1715
|
+
const entry = c.taxonomy_entry || {{}};
|
|
1716
|
+
const examples = c.examples || [];
|
|
1717
|
+
const isFirst = ci === 0;
|
|
1718
|
+
const count = c.item_count || 0;
|
|
1719
|
+
const pct = totalItems > 0 ? Math.round(count / totalItems * 100) : 0;
|
|
1720
|
+
|
|
1721
|
+
html += '<details class="cluster-card"' + (isFirst ? ' open' : '') + '>';
|
|
1722
|
+
html += '<summary class="cluster-summary">';
|
|
1723
|
+
html += '<div class="cluster-name">';
|
|
1724
|
+
html += '<span class="l1-pill">' + DOMPurify.sanitize(entry.l1_category || '') + '</span> ';
|
|
1725
|
+
html += DOMPurify.sanitize(entry.l2_category || 'Cluster ' + (ci + 1));
|
|
1726
|
+
html += '</div>';
|
|
1727
|
+
html += '<span class="item-count">' + count + ' items (' + pct + '%)</span>';
|
|
1728
|
+
html += '</summary>';
|
|
1729
|
+
|
|
1730
|
+
html += '<div class="cluster-body">';
|
|
1731
|
+
|
|
1732
|
+
if (entry.description) {{
|
|
1733
|
+
html += '<div class="cluster-description">' + DOMPurify.sanitize(entry.description) + '</div>';
|
|
1734
|
+
}}
|
|
1735
|
+
|
|
1736
|
+
// Examples section
|
|
1737
|
+
if (examples.length > 0) {{
|
|
1738
|
+
const exLabel = examples.length < count
|
|
1739
|
+
? 'Examples (' + examples.length + ' of ' + count + ')'
|
|
1740
|
+
: 'Examples (' + examples.length + ')';
|
|
1741
|
+
html += '<details class="examples-details">';
|
|
1742
|
+
html += '<summary>' + exLabel + '</summary>';
|
|
1743
|
+
html += '<div style="margin-top: 8px;">';
|
|
1744
|
+
examples.forEach((ex, ei) => {{
|
|
1745
|
+
html += '<div class="example-card">';
|
|
1746
|
+
html += '<div class="example-label">Example ' + (ei + 1) + '</div>';
|
|
1747
|
+
const scenario = (ex.evaluation_result && ex.evaluation_result.scenario_preview) || extractScenarioPreview(ex);
|
|
1748
|
+
if (scenario) {{
|
|
1749
|
+
html += '<div class="example-section-label">Scenario</div>';
|
|
1750
|
+
html += '<div class="example-scenario">';
|
|
1751
|
+
html += '<span class="example-scenario-icon">';
|
|
1752
|
+
html += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>';
|
|
1753
|
+
html += '</span>';
|
|
1754
|
+
html += '<span class="example-scenario-text">' + DOMPurify.sanitize(scenario) + '</span>';
|
|
1755
|
+
html += '</div>';
|
|
1756
|
+
}}
|
|
1757
|
+
const rubrics = ex.failed_rubrics || [];
|
|
1758
|
+
if (rubrics.length > 0) {{
|
|
1759
|
+
html += '<div class="example-section-label">Failed Rubrics</div>';
|
|
1760
|
+
html += '<div style="margin-top: 4px;">';
|
|
1761
|
+
const rubricDescMap = (ex.evaluation_result && ex.evaluation_result.rubric_descriptions) || {{}};
|
|
1762
|
+
rubrics.forEach(fr => {{
|
|
1763
|
+
const rubricDesc = fr.rubric_id ? (rubricDescMap[fr.rubric_id] || lookupRubricDescription(ex, fr.rubric_id)) : null;
|
|
1764
|
+
if (rubricDesc) {{
|
|
1765
|
+
html += '<div class="rubric-description"><span class="example-rubric">Fail</span> ' + DOMPurify.sanitize(rubricDesc) + '</div>';
|
|
1766
|
+
}} else if (fr.rubric_id) {{
|
|
1767
|
+
html += '<div class="rubric-description"><span class="example-rubric">Fail</span> ' + DOMPurify.sanitize(fr.rubric_id) + '</div>';
|
|
1768
|
+
}}
|
|
1769
|
+
if (fr.classification_rationale) {{
|
|
1770
|
+
html += '<div class="example-section-label" style="margin-top: 6px;">Rationale</div>';
|
|
1771
|
+
html += '<div class="example-rationale">' + DOMPurify.sanitize(fr.classification_rationale) + '</div>';
|
|
1772
|
+
}}
|
|
1773
|
+
}});
|
|
1774
|
+
html += '</div>';
|
|
1775
|
+
}}
|
|
1776
|
+
html += '</div>';
|
|
1777
|
+
}});
|
|
1778
|
+
html += '</div></details>';
|
|
1779
|
+
}}
|
|
1780
|
+
|
|
1781
|
+
html += '</div></details>';
|
|
1782
|
+
}});
|
|
1783
|
+
}});
|
|
1784
|
+
|
|
1785
|
+
root.innerHTML = html;
|
|
1786
|
+
|
|
1787
|
+
function lookupRubricDescription(ex, rubricId) {{
|
|
1788
|
+
// Look up the rubric description from evaluation_result.candidate_results
|
|
1789
|
+
// by matching rubric_id in rubric_verdicts.evaluated_rubric.
|
|
1790
|
+
// Handles both snake_case (SDK-side) and camelCase (API echo-back) keys.
|
|
1791
|
+
const er = ex.evaluation_result;
|
|
1792
|
+
if (!er) return null;
|
|
1793
|
+
const candidateResults = er.candidate_results || er.candidateResults;
|
|
1794
|
+
if (!candidateResults) return null;
|
|
1795
|
+
for (const cr of candidateResults) {{
|
|
1796
|
+
const verdicts = cr.rubric_verdicts || cr.rubricVerdicts || [];
|
|
1797
|
+
for (const v of verdicts) {{
|
|
1798
|
+
const evalRubric = v.evaluated_rubric || v.evaluatedRubric;
|
|
1799
|
+
if (!evalRubric) continue;
|
|
1800
|
+
const rid = evalRubric.rubric_id || evalRubric.rubricId;
|
|
1801
|
+
if (rid === rubricId) {{
|
|
1802
|
+
const content = evalRubric.content;
|
|
1803
|
+
if (content) {{
|
|
1804
|
+
if (content.property && content.property.description) {{
|
|
1805
|
+
return content.property.description;
|
|
1806
|
+
}}
|
|
1807
|
+
if (content.text) return content.text;
|
|
1808
|
+
}}
|
|
1809
|
+
}}
|
|
1810
|
+
}}
|
|
1811
|
+
}}
|
|
1812
|
+
return null;
|
|
1813
|
+
}}
|
|
1814
|
+
|
|
1815
|
+
function extractScenarioPreview(ex) {{
|
|
1816
|
+
// Extract the first user message from evaluation_result as a scenario preview.
|
|
1817
|
+
// Handles both snake_case (SDK-side) and camelCase (API echo-back) keys.
|
|
1818
|
+
const er = ex.evaluation_result;
|
|
1819
|
+
if (!er) return null;
|
|
1820
|
+
const req = er.request;
|
|
1821
|
+
if (!req) return null;
|
|
1822
|
+
const prompt = req.prompt;
|
|
1823
|
+
|
|
1824
|
+
// Helper: extract first user text from agent_data turns
|
|
1825
|
+
function firstUserText(agentData) {{
|
|
1826
|
+
if (!agentData || !agentData.turns) return null;
|
|
1827
|
+
for (const turn of agentData.turns) {{
|
|
1828
|
+
if (!turn.events) continue;
|
|
1829
|
+
for (const event of turn.events) {{
|
|
1830
|
+
const role = event.author || (event.content && event.content.role) || '';
|
|
1831
|
+
if (role.toLowerCase() === 'user' && event.content && event.content.parts) {{
|
|
1832
|
+
for (const part of event.content.parts) {{
|
|
1833
|
+
if (part.text) {{
|
|
1834
|
+
const text = part.text.trim();
|
|
1835
|
+
return text.length > 150 ? text.substring(0, 150) + '...' : text;
|
|
1836
|
+
}}
|
|
1837
|
+
}}
|
|
1838
|
+
}}
|
|
1839
|
+
}}
|
|
1840
|
+
}}
|
|
1841
|
+
return null;
|
|
1842
|
+
}}
|
|
1843
|
+
|
|
1844
|
+
if (prompt) {{
|
|
1845
|
+
// Path 1: prompt.agent_data.turns (LRO inline results path)
|
|
1846
|
+
const agentData = prompt.agent_data || prompt.agentData;
|
|
1847
|
+
const fromPromptAgent = firstUserText(agentData);
|
|
1848
|
+
if (fromPromptAgent) return fromPromptAgent;
|
|
1849
|
+
|
|
1850
|
+
// Path 2: prompt.user_scenario.starting_prompt (eval run path)
|
|
1851
|
+
const scenario = prompt.user_scenario || prompt.userScenario;
|
|
1852
|
+
if (scenario) {{
|
|
1853
|
+
const sp = scenario.starting_prompt || scenario.startingPrompt;
|
|
1854
|
+
if (sp) {{
|
|
1855
|
+
const text = sp.trim();
|
|
1856
|
+
return text.length > 150 ? text.substring(0, 150) + '...' : text;
|
|
1857
|
+
}}
|
|
1858
|
+
}}
|
|
1859
|
+
|
|
1860
|
+
// Path 3: prompt.parts[].text (simple prompt path)
|
|
1861
|
+
if (prompt.parts) {{
|
|
1862
|
+
for (const part of prompt.parts) {{
|
|
1863
|
+
if (part.text) {{
|
|
1864
|
+
const text = part.text.trim();
|
|
1865
|
+
return text.length > 150 ? text.substring(0, 150) + '...' : text;
|
|
1866
|
+
}}
|
|
1867
|
+
}}
|
|
1868
|
+
}}
|
|
1869
|
+
}}
|
|
1870
|
+
|
|
1871
|
+
// Path 4: candidate_responses[].agent_data.turns (eval run path -
|
|
1872
|
+
// agent_data is on the candidate response, not the prompt)
|
|
1873
|
+
const crs = req.candidate_responses || req.candidateResponses;
|
|
1874
|
+
if (crs) {{
|
|
1875
|
+
for (const cr of crs) {{
|
|
1876
|
+
const ad = cr.agent_data || cr.agentData;
|
|
1877
|
+
const fromCr = firstUserText(ad);
|
|
1878
|
+
if (fromCr) return fromCr;
|
|
1879
|
+
}}
|
|
1880
|
+
}}
|
|
1881
|
+
|
|
1882
|
+
return null;
|
|
1883
|
+
}}
|
|
1884
|
+
}})();
|
|
1885
|
+
</script>
|
|
1886
|
+
</body>
|
|
1887
|
+
</html>
|
|
1888
|
+
"""
|
|
1889
|
+
)
|
|
1890
|
+
|
|
1891
|
+
|
|
1892
|
+
def display_loss_clusters_response(
|
|
1893
|
+
response_obj: "types.GenerateLossClustersResponse",
|
|
1894
|
+
) -> None:
|
|
1895
|
+
"""Displays a GenerateLossClustersResponse in an IPython environment."""
|
|
1896
|
+
if not _is_ipython_env():
|
|
1897
|
+
logger.warning("Skipping display: not in an IPython environment.")
|
|
1898
|
+
return
|
|
1899
|
+
else:
|
|
1900
|
+
from IPython import display
|
|
1901
|
+
|
|
1902
|
+
try:
|
|
1903
|
+
result_dump = response_obj.model_dump(mode="json", exclude_none=True)
|
|
1904
|
+
except Exception as e:
|
|
1905
|
+
logger.error(
|
|
1906
|
+
"Failed to serialize GenerateLossClustersResponse: %s",
|
|
1907
|
+
e,
|
|
1908
|
+
exc_info=True,
|
|
1909
|
+
)
|
|
1910
|
+
raise
|
|
1911
|
+
|
|
1912
|
+
html_content = get_loss_analysis_html(
|
|
1913
|
+
json.dumps(result_dump, ensure_ascii=False, default=_pydantic_serializer)
|
|
1914
|
+
)
|
|
1915
|
+
display.display(display.HTML(html_content))
|
|
1916
|
+
|
|
1917
|
+
|
|
1918
|
+
def display_loss_analysis_result(
|
|
1919
|
+
result_obj: "types.LossAnalysisResult",
|
|
1920
|
+
) -> None:
|
|
1921
|
+
"""Displays a single LossAnalysisResult in an IPython environment."""
|
|
1922
|
+
if not _is_ipython_env():
|
|
1923
|
+
logger.warning("Skipping display: not in an IPython environment.")
|
|
1924
|
+
return
|
|
1925
|
+
else:
|
|
1926
|
+
from IPython import display
|
|
1927
|
+
|
|
1928
|
+
try:
|
|
1929
|
+
# Wrap in a response-like structure for the shared HTML generator
|
|
1930
|
+
wrapped = {"results": [result_obj.model_dump(mode="json", exclude_none=True)]}
|
|
1931
|
+
except Exception as e:
|
|
1932
|
+
logger.error(
|
|
1933
|
+
"Failed to serialize LossAnalysisResult: %s",
|
|
1934
|
+
e,
|
|
1935
|
+
exc_info=True,
|
|
1936
|
+
)
|
|
1937
|
+
raise
|
|
1938
|
+
|
|
1939
|
+
html_content = get_loss_analysis_html(
|
|
1940
|
+
json.dumps(wrapped, ensure_ascii=False, default=_pydantic_serializer)
|
|
1941
|
+
)
|
|
1942
|
+
display.display(display.HTML(html_content))
|
|
1943
|
+
|
|
1944
|
+
|
|
1945
|
+
def _get_status_html(status: str, error_message: Optional[str] = None) -> str:
|
|
1946
|
+
"""Returns a simple HTML string for displaying a status and optional error."""
|
|
1947
|
+
error_html = ""
|
|
1948
|
+
if error_message:
|
|
1949
|
+
error_html = f"""
|
|
1950
|
+
<p>
|
|
1951
|
+
<b>Error:</b>
|
|
1952
|
+
<pre style="white-space: pre-wrap; word-wrap: break-word;">{html.escape(error_message)}</pre>
|
|
1953
|
+
</p>
|
|
1954
|
+
"""
|
|
1955
|
+
|
|
1956
|
+
return textwrap.dedent(
|
|
1957
|
+
f"""
|
|
1958
|
+
<div>
|
|
1959
|
+
<p><b>Status:</b> {html.escape(status)}</p>
|
|
1960
|
+
{error_html}
|
|
1961
|
+
</div>
|
|
1962
|
+
"""
|
|
1963
|
+
)
|
|
1964
|
+
|
|
1965
|
+
|
|
1966
|
+
def _enrich_loss_examples_with_eval_items(
|
|
1967
|
+
results: list["types.LossAnalysisResult"],
|
|
1968
|
+
eval_item_map: Optional[dict[str, dict[str, Any]]],
|
|
1969
|
+
) -> list[dict[str, Any]]:
|
|
1970
|
+
"""Enriches loss analysis examples with eval item data for visualization.
|
|
1971
|
+
|
|
1972
|
+
For the eval run path, loss examples only have ``evaluation_item``
|
|
1973
|
+
(a resource name) but no ``evaluation_result``. The JS visualization
|
|
1974
|
+
needs ``evaluation_result`` to extract scenario previews and rubric
|
|
1975
|
+
descriptions. This function joins the loss examples with the eval
|
|
1976
|
+
item map so the visualization works identically to the LRO path.
|
|
1977
|
+
|
|
1978
|
+
Args:
|
|
1979
|
+
results: Loss analysis results from the eval run.
|
|
1980
|
+
eval_item_map: Optional mapping from evaluation item resource name
|
|
1981
|
+
to serialized evaluation response data (built by
|
|
1982
|
+
``_evals_common._build_eval_item_map``).
|
|
1983
|
+
|
|
1984
|
+
Returns:
|
|
1985
|
+
A list of dicts ready for JSON serialization, with ``evaluation_result``
|
|
1986
|
+
populated on each example where a match is found.
|
|
1987
|
+
"""
|
|
1988
|
+
result_dicts = []
|
|
1989
|
+
for r in results:
|
|
1990
|
+
r_dump = r.model_dump(mode="json", exclude_none=True)
|
|
1991
|
+
if eval_item_map:
|
|
1992
|
+
clusters = r_dump.get("clusters", [])
|
|
1993
|
+
for cluster in clusters:
|
|
1994
|
+
examples = cluster.get("examples", [])
|
|
1995
|
+
for ex in examples:
|
|
1996
|
+
# Skip if evaluation_result is already populated (LRO path)
|
|
1997
|
+
if ex.get("evaluation_result"):
|
|
1998
|
+
continue
|
|
1999
|
+
# Match by evaluation_item resource name
|
|
2000
|
+
eval_item_ref = ex.get("evaluation_item")
|
|
2001
|
+
if eval_item_ref and eval_item_ref in eval_item_map:
|
|
2002
|
+
ex["evaluation_result"] = eval_item_map[eval_item_ref]
|
|
2003
|
+
result_dicts.append(r_dump)
|
|
2004
|
+
return result_dicts
|
|
2005
|
+
|
|
2006
|
+
|
|
2007
|
+
def display_loss_analysis_results(
|
|
2008
|
+
results: list["types.LossAnalysisResult"],
|
|
2009
|
+
eval_item_map: Optional[dict[str, dict[str, Any]]] = None,
|
|
2010
|
+
) -> None:
|
|
2011
|
+
"""Displays loss analysis results from an EvaluationRun.
|
|
2012
|
+
|
|
2013
|
+
Wraps the list of LossAnalysisResult objects into the same JSON
|
|
2014
|
+
structure used by GenerateLossClustersResponse and renders using
|
|
2015
|
+
the shared get_loss_analysis_html() function.
|
|
2016
|
+
|
|
2017
|
+
When ``eval_item_map`` is provided (from
|
|
2018
|
+
``get_evaluation_run(include_evaluation_items=True)``), the examples
|
|
2019
|
+
are enriched with scenario and rubric data for the visualization.
|
|
2020
|
+
|
|
2021
|
+
Args:
|
|
2022
|
+
results: A list of LossAnalysisResult objects from
|
|
2023
|
+
EvaluationRunResults.loss_analysis_results.
|
|
2024
|
+
eval_item_map: Optional mapping from evaluation item resource name
|
|
2025
|
+
to serialized evaluation response data for enrichment.
|
|
2026
|
+
"""
|
|
2027
|
+
if not _is_ipython_env():
|
|
2028
|
+
logger.warning("Skipping display: not in an IPython environment.")
|
|
2029
|
+
return
|
|
2030
|
+
else:
|
|
2031
|
+
from IPython import display
|
|
2032
|
+
|
|
2033
|
+
try:
|
|
2034
|
+
result_dicts = _enrich_loss_examples_with_eval_items(results, eval_item_map)
|
|
2035
|
+
wrapped = {"results": result_dicts}
|
|
2036
|
+
except Exception as e:
|
|
2037
|
+
logger.error(
|
|
2038
|
+
"Failed to serialize loss analysis results: %s",
|
|
2039
|
+
e,
|
|
2040
|
+
exc_info=True,
|
|
2041
|
+
)
|
|
2042
|
+
raise
|
|
2043
|
+
|
|
2044
|
+
html_content = get_loss_analysis_html(
|
|
2045
|
+
json.dumps(wrapped, ensure_ascii=False, default=_pydantic_serializer)
|
|
2046
|
+
)
|
|
2047
|
+
display.display(display.HTML(html_content))
|
|
2048
|
+
|
|
2049
|
+
|
|
2050
|
+
def display_evaluation_run_status(eval_run_obj: "types.EvaluationRun") -> None:
|
|
2051
|
+
"""Displays the status of an evaluation run in an IPython environment."""
|
|
2052
|
+
if not _is_ipython_env():
|
|
2053
|
+
logger.warning("Skipping display: not in an IPython environment.")
|
|
2054
|
+
return
|
|
2055
|
+
else:
|
|
2056
|
+
from IPython import display
|
|
2057
|
+
|
|
2058
|
+
status = eval_run_obj.state.name if eval_run_obj.state else "UNKNOWN"
|
|
2059
|
+
error_message = str(eval_run_obj.error) if eval_run_obj.error else None
|
|
2060
|
+
html_content = _get_status_html(status, error_message)
|
|
2061
|
+
display.display(display.HTML(html_content))
|
|
2062
|
+
|
|
2063
|
+
|
|
2064
|
+
# Backward-compatible private aliases for the public HTML generators.
|
|
2065
|
+
# These are kept temporarily to avoid breaking existing callers that depend on
|
|
2066
|
+
# the previous private names. New code should use the public names above.
|
|
2067
|
+
_get_evaluation_html = get_evaluation_html
|
|
2068
|
+
_get_comparison_html = get_comparison_html
|
|
2069
|
+
_get_inference_html = get_inference_html
|
|
2070
|
+
_get_loss_analysis_html = get_loss_analysis_html
|