diff --git a/vertexai/_genai/_evals_visualization.py b/vertexai/_genai/_evals_visualization.py
index 2cfbc6a052..0436d893ae 100644
--- a/vertexai/_genai/_evals_visualization.py
+++ b/vertexai/_genai/_evals_visualization.py
@@ -283,26 +283,27 @@ def _get_evaluation_html(eval_result_json: str) -> str:
let traceHtml = `
`;
eventsArray.forEach(event => {{
if (event.content && event.content.parts && event.content.parts.length > 0) {{
- const part = event.content.parts[0];
- if (part.function_call) {{
- traceHtml += `
`;
- traceHtml += `function name: ${{part.function_call.name}}
`;
- traceHtml += `function args: ${{formatDictVals(part.function_call.args)}}
`;
- }} else if (part.text && event.content.role === 'model') {{
- traceHtml += `
`;
- traceHtml += `model response: ${{part.text}}
`;
- }} else if (part.function_response) {{
- traceHtml += `
`;
- traceHtml += `function name: ${{part.function_response.name}}
`;
- let response_val = part.function_response.response;
- if(typeof response_val === 'object' && response_val !== null && response_val.result !== undefined) {{
- response_val = response_val.result;
+ event.content.parts.forEach(part => {{
+ if (part.function_call) {{
+ traceHtml += `
`;
+ traceHtml += `function name: ${{part.function_call.name}}
`;
+ traceHtml += `function args: ${{formatDictVals(part.function_call.args)}}
`;
+ }} else if (part.text && event.content.role === 'model') {{
+ traceHtml += `
`;
+ traceHtml += `model response: ${{part.text}}
`;
+ }} else if (part.function_response) {{
+ traceHtml += `
`;
+ traceHtml += `function name: ${{part.function_response.name}}
`;
+ let response_val = part.function_response.response;
+ if(typeof response_val === 'object' && response_val !== null && response_val.result !== undefined) {{
+ response_val = response_val.result;
+ }}
+ traceHtml += `function response: ${{formatDictVals(response_val)}}
`;
+ }} else {{
+ // Skipping user messages and other parts in trace view
+ return;
}}
- traceHtml += `function response: ${{formatDictVals(response_val)}}
`;
- }} else {{
- // Skipping user messages and other parts in trace view
- return;
- }}
+ }});
}}
}});
return traceHtml;
@@ -313,16 +314,17 @@ def _get_evaluation_html(eval_result_json: str) -> str:
const role = event.content.role;
let contentHtml = '';
if (event.content && event.content.parts && event.content.parts.length > 0) {{
- const part = event.content.parts[0];
- if (part.text) {{
- contentHtml = DOMPurify.sanitize(marked.parse(String(part.text)));
- }} else if (part.function_call) {{
- contentHtml = `${{DOMPurify.sanitize(JSON.stringify(part.function_call, null, 2))}}`;
- }} else if (part.function_response) {{
- contentHtml = `${{DOMPurify.sanitize(JSON.stringify(part.function_response, null, 2))}}`;
- }} else {{
- contentHtml = `${{DOMPurify.sanitize(JSON.stringify(event.content, null, 2))}}`;
- }}
+ event.content.parts.forEach(part => {{
+ if (part.text) {{
+ contentHtml += DOMPurify.sanitize(marked.parse(String(part.text)));
+ }} else if (part.function_call) {{
+ contentHtml += `${{DOMPurify.sanitize(JSON.stringify(part.function_call, null, 2))}}`;
+ }} else if (part.function_response) {{
+ contentHtml += `${{DOMPurify.sanitize(JSON.stringify(part.function_response, null, 2))}}`;
+ }} else {{
+ contentHtml += `${{DOMPurify.sanitize(JSON.stringify(part, null, 2))}}`;
+ }}
+ }});
}} else {{
contentHtml = `${{DOMPurify.sanitize(JSON.stringify(event.content, null, 2))}}`;
}}
@@ -456,28 +458,43 @@ def _get_evaluation_html(eval_result_json: str) -> str:
if (name.startsWith('hallucination') && val.explanation) {{
try {{
- const explanationData = JSON.parse(val.explanation);
- if (Array.isArray(explanationData) && explanationData.length > 0 && explanationData[0].sentence) {{
- bubbles += '';
- explanationData.forEach(item => {{
- let sentence = item.sentence || 'N/A';
- const label = item.label ? item.label.toLowerCase() : '';
- const isPass = label === 'no_rad' || label === 'supported';
- const verdictText = isPass ? '
Pass' : '
Fail';
- if (isPass) {{
- sentence = `"${{sentence}}" is grounded`;
- }}
- const rationale = item.rationale || 'N/A';
- const itemJson = JSON.stringify(item, null, 2);
- bubbles += `
-
- ${{verdictText}}: ${{DOMPurify.sanitize(sentence)}}
- ${{DOMPurify.sanitize(rationale)}}
- ${{DOMPurify.sanitize(itemJson)}}
- `;
- }});
- bubbles += '
';
- explanationHandled = true;
+ const explanationData = typeof val.explanation === 'string' ? JSON.parse(val.explanation) : val.explanation;
+ if (Array.isArray(explanationData) && explanationData.length > 0) {{
+ let sentenceGroups = [];
+ if (explanationData[0].explanation && Array.isArray(explanationData[0].explanation)) {{
+ explanationData.forEach(item => {{
+ if(item.explanation && Array.isArray(item.explanation)) {{
+ sentenceGroups.push(item.explanation);
+ }}
+ }});
+ }} else if (explanationData[0].sentence) {{
+ sentenceGroups.push(explanationData);
+ }}
+
+ if(sentenceGroups.length > 0) {{
+ sentenceGroups.forEach(sentenceList => {{
+ bubbles += '';
+ sentenceList.forEach(item => {{
+ let sentence = item.sentence || 'N/A';
+ const label = item.label ? item.label.toLowerCase() : '';
+ const isPass = label === 'no_rad' || label === 'supported';
+ const verdictText = isPass ? '
Pass' : '
Fail';
+ if (isPass) {{
+ sentence = `"${{sentence}}" is grounded`;
+ }}
+ const rationale = item.rationale || 'N/A';
+ const itemJson = JSON.stringify(item, null, 2);
+ bubbles += `
+
+ ${{verdictText}}: ${{DOMPurify.sanitize(sentence)}}
+ ${{DOMPurify.sanitize(rationale)}}
+ ${{DOMPurify.sanitize(itemJson)}}
+ `;
+ }});
+ bubbles += '
';
+ }});
+ explanationHandled = true;
+ }}
}}
}} catch (e) {{
console.error("Failed to parse hallucination explanation:", e);