""" Agent Trace Display Type Purpose-built rendering for agent traces as a vertical sequence of "step cards". Each step shows a type badge (Thought / Action / Observation) with color coding, collapsible tool call parameters, and optional inline screenshot thumbnails. This display type provides a richer visual alternative to the dialogue display for agent trace data, with step-type-aware styling and summary headers. """ import html from typing import Dict, Any, List, Optional from .base import BaseDisplay from ._trace_normalize import ( DEFAULT_STEP_COLORS, SPEAKER_TYPE_PATTERNS, normalize_steps, infer_type_from_speaker, infer_type_from_text, ) class AgentTraceDisplay(BaseDisplay): """ Display type for agent traces rendered as step cards. Supports data as: - List of dicts with speaker/text keys (same as dialogue) - List of dicts with step_type/content keys - List of dicts with thought/action/observation keys (one step per dict) """ name = "agent_trace" required_fields = ["key"] optional_fields = { "show_timestamps": False, "collapse_observations": False, "step_type_colors": DEFAULT_STEP_COLORS, "show_screenshots": True, "show_step_numbers": True, "show_summary": True, "compact": False, "speaker_key": "speaker", "text_key": "text", } description = "Agent trace display with step cards and type badges" supports_span_target = False # Per-step IDs don't follow .text-content wrapper contract def render(self, field_config: Dict[str, Any], data: Any) -> str: if not data: return '
No trace data provided
' options = self.get_display_options(field_config) show_timestamps = options.get("show_timestamps", False) collapse_obs = options.get("collapse_observations", False) colors = options.get("step_type_colors", DEFAULT_STEP_COLORS) show_screenshots = options.get("show_screenshots", True) show_step_numbers = options.get("show_step_numbers", True) show_summary = options.get("show_summary", True) compact = options.get("compact", False) speaker_key = options.get("speaker_key", "speaker") text_key = options.get("text_key", "text") field_key = html.escape(field_config.get("key", ""), quote=True) is_span_target = field_config.get("span_target", False) # Normalize data to steps steps = self._normalize_steps(data, speaker_key, text_key) if not steps: return '
No trace steps found
' # Build CSS for step type colors css = self._build_css(colors, compact) # Build summary header summary_html = "" if show_summary: summary_html = self._build_summary(steps) # Build step cards step_html_list = [] step_counter = 0 for i, step in enumerate(steps): step_type = step.get("type", "observation") speaker = step.get("speaker", "") text = step.get("text", "") timestamp = step.get("timestamp", "") screenshot = step.get("screenshot", "") if step_type in ("action", "thought"): step_counter += 1 # Step card - sanitize step_type for use in CSS class names and attributes safe_step_type = html.escape(step_type, quote=True) type_class = f"step-type-{safe_step_type}" card_classes = ["agent-trace-step", type_class] if compact: card_classes.append("compact") # Badge badge_label = safe_step_type.capitalize() if speaker: badge_label = html.escape(str(speaker)) # Step number step_num_html = "" if show_step_numbers and step_type in ("action", "thought"): step_num_html = f'#{step_counter}' # Timestamp ts_html = "" if show_timestamps and timestamp: ts_html = f'{html.escape(str(timestamp))}' # Text content escaped_text = html.escape(str(text)) text_id = "" span_attrs = "" if is_span_target: text_id = f'id="step-text-{field_key}-{i}"' span_attrs = f'data-original-text="{escaped_text}" data-step-index="{i}"' # Collapsible wrapper for observations if collapse_obs and step_type == "observation": text_html = ( f'
' f'Observation (click to expand)' f'
{escaped_text}
' f'
' ) else: text_html = f'
{escaped_text}
' # Screenshot thumbnail screenshot_html = "" if show_screenshots and screenshot: escaped_src = html.escape(str(screenshot), quote=True) screenshot_html = ( f'
' f'Step {i} screenshot' f'
' ) card_html = f'''
{badge_label} {step_num_html} {ts_html}
{text_html} {screenshot_html}
''' step_html_list.append(card_html) all_steps_html = "\n".join(step_html_list) container_classes = ["agent-trace-display"] if is_span_target: container_classes.append("span-target-agent-trace") return f'''
{summary_html}
{all_steps_html}
''' def _normalize_steps(self, data: Any, speaker_key: str, text_key: str) -> List[Dict[str, str]]: """Normalize various trace data formats to a list of step dicts. Delegates to the shared :func:`normalize_steps` so ``agent_trace`` and ``eval_trace`` parse identical data the same way. """ return normalize_steps(data, speaker_key, text_key) def _infer_type_from_speaker(self, speaker: str) -> str: """Infer step type from speaker name.""" return infer_type_from_speaker(speaker) def _infer_type_from_text(self, text: str) -> str: """Infer step type from text content.""" return infer_type_from_text(text) def _build_summary(self, steps: List[Dict]) -> str: """Build a summary header showing step counts.""" type_counts = {} for step in steps: t = step.get("type", "observation") type_counts[t] = type_counts.get(t, 0) + 1 badges = [] for step_type in ["thought", "action", "observation"]: count = type_counts.get(step_type, 0) if count > 0: badges.append( f'' f'{count} {step_type}{"s" if count != 1 else ""}' ) return f'''
{len(steps)} steps {" ".join(badges)}
''' def _build_css(self, colors: Dict[str, str], compact: bool) -> str: """Build CSS for step type colors and layout.""" thought_color = colors.get("thought", DEFAULT_STEP_COLORS["thought"]) action_color = colors.get("action", DEFAULT_STEP_COLORS["action"]) obs_color = colors.get("observation", DEFAULT_STEP_COLORS["observation"]) system_color = colors.get("system", DEFAULT_STEP_COLORS["system"]) error_color = colors.get("error", DEFAULT_STEP_COLORS["error"]) padding = "8px 12px" if compact else "12px 16px" margin = "4px 0" if compact else "8px 0" return f''' .agent-trace-display {{ font-family: inherit; }} .agent-trace-summary {{ display: flex; align-items: center; gap: 8px; padding: 8px 12px; margin-bottom: 12px; background: #f8f9fa; border-radius: 6px; font-size: 0.9em; }} .summary-total {{ font-weight: 600; }} .summary-badge {{ padding: 2px 8px; border-radius: 12px; font-size: 0.85em; }} .agent-trace-step {{ padding: {padding}; margin: {margin}; border-radius: 6px; border-left: 4px solid #ccc; }} .step-type-thought {{ background: {thought_color}; border-left-color: #2196F3; }} .step-type-action {{ background: {action_color}; border-left-color: #FF9800; }} .step-type-observation {{ background: {obs_color}; border-left-color: #4CAF50; }} .step-type-system {{ background: {system_color}; border-left-color: #9C27B0; }} .step-type-error {{ background: {error_color}; border-left-color: #f44336; }} .step-header {{ display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }} .step-badge {{ padding: 2px 8px; border-radius: 4px; font-size: 0.8em; font-weight: 600; color: #333; }} .badge-thought {{ background: rgba(33,150,243,0.2); }} .badge-action {{ background: rgba(255,152,0,0.2); }} .badge-observation {{ background: rgba(76,175,80,0.2); }} .badge-system {{ background: rgba(156,39,176,0.2); }} .badge-error {{ background: rgba(244,67,54,0.2); }} .step-number {{ color: #666; font-size: 0.8em; }} .step-timestamp {{ color: #999; font-size: 0.75em; margin-left: auto; }} .step-text {{ white-space: pre-wrap; word-break: break-word; line-height: 1.5; }} .step-screenshot {{ margin-top: 8px; }} .step-screenshot-img {{ max-width: 300px; max-height: 200px; border-radius: 4px; border: 1px solid #ddd; cursor: pointer; }} .step-screenshot-img:hover {{ box-shadow: 0 2px 8px rgba(0,0,0,0.15); }} .step-collapsible summary {{ cursor: pointer; color: #666; font-size: 0.9em; padding: 4px 0; }} ''' def get_css_classes(self, field_config: Dict[str, Any]) -> List[str]: classes = super().get_css_classes(field_config) if field_config.get("span_target"): classes.append("span-target-field") return classes def get_data_attributes(self, field_config: Dict[str, Any], data: Any) -> Dict[str, str]: attrs = super().get_data_attributes(field_config, data) if field_config.get("span_target"): attrs["span-target"] = "true" return attrs