""" Display Registry Provides a centralized registry for managing display types. This module serves as the single source of truth for available display types and their renderers, separating content display from annotation collection. Usage: from potato.server_utils.displays.registry import display_registry # Render content html = display_registry.render("image", field_config, data) # List all available display types types = display_registry.get_supported_types() # Register a custom display type (plugin support) display_registry.register_plugin("my_custom", MyCustomDisplay()) """ from dataclasses import dataclass, field from typing import Callable, Dict, List, Any, Optional, Union import logging from .base import BaseDisplay, render_display_container logger = logging.getLogger(__name__) @dataclass class DisplayDefinition: """ Defines metadata and renderer for a display type. Attributes: name: Unique identifier for the display type (e.g., "text", "image") renderer: Either a BaseDisplay instance or callable that renders content required_fields: List of required configuration fields optional_fields: Dictionary of optional fields with default values supports_span_target: Whether this type can be a span annotation target description: Human-readable description of the display type lazy_populated: When True, the display's data key is populated after initial render (e.g. ``interactive_chat``'s conversation is written only after the user finishes chatting). The ``instance_display`` validator uses this to decide whether a missing data key is a transient state (lazy) or a config error (not lazy). """ name: str renderer: Union[BaseDisplay, Callable[[Dict[str, Any], Any], str]] required_fields: List[str] = field(default_factory=list) optional_fields: Dict[str, Any] = field(default_factory=dict) supports_span_target: bool = False description: str = "" lazy_populated: bool = False class DisplayRegistry: """ Centralized registry for display types. Provides methods to register, retrieve, and render display types. Supports both built-in display types and custom plugins. """ def __init__(self): self._displays: Dict[str, DisplayDefinition] = {} self._plugins: Dict[str, BaseDisplay] = {} logger.debug("DisplayRegistry initialized") def register(self, display: DisplayDefinition) -> None: """ Register a built-in display type. Args: display: DisplayDefinition to register Raises: ValueError: If a display with the same name is already registered """ if display.name in self._displays: raise ValueError(f"Display type '{display.name}' is already registered") self._displays[display.name] = display logger.debug(f"Registered display type: {display.name}") def register_plugin(self, name: str, plugin: BaseDisplay) -> None: """ Register a custom display type from a plugin. Args: name: Unique name for the display type plugin: BaseDisplay instance implementing the display logic Raises: ValueError: If a display with the same name is already registered """ if name in self._displays or name in self._plugins: raise ValueError(f"Display type '{name}' is already registered") self._plugins[name] = plugin logger.debug(f"Registered plugin display type: {name}") def get(self, name: str) -> Optional[Union[DisplayDefinition, BaseDisplay]]: """ Get a display definition or plugin by name. Args: name: The display type name Returns: DisplayDefinition or BaseDisplay if found, None otherwise """ if name in self._displays: return self._displays[name] return self._plugins.get(name) def render(self, field_type: str, field_config: Dict[str, Any], data: Any) -> str: """ Render content using the appropriate display type. Args: field_type: The display type name (e.g., "text", "image") field_config: Configuration for this field from instance_display.fields data: The actual data value from the instance Returns: HTML string for rendering the content Raises: ValueError: If the display type is not registered """ # Check plugins first (allows overriding built-ins) if field_type in self._plugins: plugin = self._plugins[field_type] inner_html = plugin.render(field_config, data) css_classes = plugin.get_css_classes(field_config) data_attrs = plugin.get_data_attributes(field_config, data) label = None if plugin.has_inline_label(field_config) else field_config.get("label") return render_display_container(inner_html, css_classes, data_attrs, label) # Check built-in displays if field_type in self._displays: display = self._displays[field_type] renderer = display.renderer # Handle BaseDisplay instances if isinstance(renderer, BaseDisplay): inner_html = renderer.render(field_config, data) css_classes = renderer.get_css_classes(field_config) data_attrs = renderer.get_data_attributes(field_config, data) label = None if renderer.has_inline_label(field_config) else field_config.get("label") return render_display_container(inner_html, css_classes, data_attrs, label) # Handle callable renderers return renderer(field_config, data) supported = ", ".join(sorted(self.get_supported_types())) raise ValueError( f"Unknown display type: '{field_type}'. " f"Supported types are: {supported}" ) def validate_config(self, field_type: str, field_config: Dict[str, Any]) -> List[str]: """ Validate display configuration. Args: field_type: The display type name field_config: The field configuration to validate Returns: List of error messages (empty if valid) """ errors = [] # Check if display type exists display = self.get(field_type) if not display: errors.append(f"Unknown display type: '{field_type}'") return errors # Get required fields if isinstance(display, BaseDisplay): errors.extend(display.validate_config(field_config)) elif isinstance(display, DisplayDefinition): for req_field in display.required_fields: if req_field not in field_config: errors.append( f"Missing required field '{req_field}' for display type '{field_type}'" ) return errors def type_supports_span_target(self, field_type: str) -> bool: """ Check if a display type supports span annotation. Args: field_type: The display type name Returns: True if the display type supports span_target """ if field_type in self._plugins: return self._plugins[field_type].supports_span_target if field_type in self._displays: display = self._displays[field_type] if isinstance(display.renderer, BaseDisplay): return display.renderer.supports_span_target return display.supports_span_target return False def get_span_target_types(self) -> List[str]: """ Get all display type names that support span annotation. Returns: List of type names where supports_span_target is True """ types = [] for name in self.get_supported_types(): if self.type_supports_span_target(name): types.append(name) return sorted(types) def list_displays(self) -> List[Dict[str, Any]]: """ List all registered displays with their metadata. Returns: List of dictionaries containing display metadata """ result = [] # Add built-in displays for display in sorted(self._displays.values(), key=lambda d: d.name): result.append({ "name": display.name, "description": display.description, "required_fields": display.required_fields, "optional_fields": list(display.optional_fields.keys()), "supports_span_target": display.supports_span_target, "is_plugin": False, }) # Add plugins for name, plugin in sorted(self._plugins.items()): result.append({ "name": name, "description": plugin.description, "required_fields": plugin.required_fields, "optional_fields": list(plugin.optional_fields.keys()), "supports_span_target": plugin.supports_span_target, "is_plugin": True, }) return result def is_lazy_populated(self, name: str) -> bool: """ Check whether a display type populates its data lazily (after initial render). Used by ``instance_display._validate_fields`` to distinguish an expected transient missing key (lazy) from a real configuration error (not lazy). Args: name: The display type name. Returns: True iff the display's ``lazy_populated`` attribute is set. False for unknown types (treat as strict for safety). """ plugin = self._plugins.get(name) if plugin is not None: return bool(getattr(plugin, "lazy_populated", False)) display = self._displays.get(name) if display is not None: # Prefer the DisplayDefinition flag; fall back to the BaseDisplay # class attr when a definition didn't explicitly set it. if getattr(display, "lazy_populated", False): return True renderer = getattr(display, "renderer", None) return bool(getattr(renderer, "lazy_populated", False)) return False def is_registered(self, name: str) -> bool: """ Check if a display type is registered. Args: name: The display type name Returns: True if registered, False otherwise """ return name in self._displays or name in self._plugins def get_supported_types(self) -> List[str]: """ Get a list of all supported display types. Returns: Sorted list of display type names """ types = set(self._displays.keys()) | set(self._plugins.keys()) return sorted(types) def supports_span_target(self, name: str) -> bool: """ Check if a display type supports span annotation targeting. Args: name: The display type name Returns: True if the type supports span targets, False otherwise """ if name in self._plugins: return self._plugins[name].supports_span_target if name in self._displays: return self._displays[name].supports_span_target return False # Global registry instance display_registry = DisplayRegistry() def _register_builtin_displays(): """ Register all built-in display types. Called automatically when this module is imported. """ from .text_display import TextDisplay from .image_display import ImageDisplay from .video_display import VideoDisplay from .audio_display import AudioDisplay from .dialogue_display import DialogueDisplay from .pairwise_display import PairwiseDisplay from .pdf_display import PDFDisplay from .document_display import DocumentDisplay from .spreadsheet_display import SpreadsheetDisplay from .code_display import CodeDisplay from .conversation_tree_display import ConversationTreeDisplay from .agent_trace_display import AgentTraceDisplay from .eval_trace_display import EvalTraceDisplay from .gallery_display import GalleryDisplay from .interactive_chat_display import InteractiveChatDisplay from .web_agent_trace_display import WebAgentTraceDisplay from .live_agent_display import LiveAgentDisplay from .coding_trace_display import CodingTraceDisplay from .live_coding_agent_display import LiveCodingAgentDisplay displays = [ DisplayDefinition( name="text", renderer=TextDisplay(), required_fields=["key"], optional_fields={ "collapsible": False, "max_height": None, "preserve_whitespace": True, }, supports_span_target=True, description="Plain text content display" ), DisplayDefinition( name="html", renderer=TextDisplay(allow_html=True), required_fields=["key"], optional_fields={ "collapsible": False, "max_height": None, }, supports_span_target=False, description="HTML content display (sanitized)" ), DisplayDefinition( name="image", renderer=ImageDisplay(), required_fields=["key"], optional_fields={ "max_width": None, "max_height": None, "zoomable": True, "alt_text": "", }, supports_span_target=False, description="Image display with optional zoom" ), DisplayDefinition( name="video", renderer=VideoDisplay(), required_fields=["key"], optional_fields={ "max_width": None, "max_height": None, "controls": True, "autoplay": False, "loop": False, "muted": False, }, supports_span_target=False, description="Video player display" ), DisplayDefinition( name="audio", renderer=AudioDisplay(), required_fields=["key"], optional_fields={ "controls": True, "autoplay": False, "loop": False, "show_waveform": False, }, supports_span_target=False, description="Audio player display" ), DisplayDefinition( name="dialogue", renderer=DialogueDisplay(), required_fields=["key"], optional_fields={ "alternating_shading": True, "speaker_extraction": True, "show_turn_numbers": False, }, supports_span_target=True, description="Dialogue/conversation turns display" ), DisplayDefinition( name="pairwise", renderer=PairwiseDisplay(), required_fields=["key"], optional_fields={ "cell_width": "50%", "show_labels": True, "vertical_on_mobile": True, }, supports_span_target=False, description="Side-by-side comparison display" ), DisplayDefinition( name="pdf", renderer=PDFDisplay(), required_fields=["key"], optional_fields={ "view_mode": "scroll", "max_height": 700, "max_width": None, "text_layer": True, "show_page_controls": True, "initial_page": 1, "zoom": "auto", }, supports_span_target=False, description="PDF document display with PDF.js rendering" ), DisplayDefinition( name="document", renderer=DocumentDisplay(), required_fields=["key"], optional_fields={ "collapsible": False, "max_height": None, "show_outline": False, "preserve_structure": True, "style_theme": "default", }, supports_span_target=True, description="Document display for DOCX, Markdown, and other formats" ), DisplayDefinition( name="spreadsheet", renderer=SpreadsheetDisplay(), required_fields=["key"], optional_fields={ "annotation_mode": "row", "show_headers": True, "max_height": 400, "max_width": None, "striped": True, "hoverable": True, "sortable": False, "selectable": True, "compact": False, }, supports_span_target=False, description="Spreadsheet/table display with row or cell annotation" ), DisplayDefinition( name="code", renderer=CodeDisplay(), required_fields=["key"], optional_fields={ "language": None, "show_line_numbers": True, "max_height": 500, "max_width": None, "wrap_lines": False, "highlight_lines": None, "start_line": 1, "theme": "default", "copy_button": True, }, supports_span_target=True, description="Source code display with syntax highlighting" ), DisplayDefinition( name="conversation_tree", renderer=ConversationTreeDisplay(), required_fields=["key"], optional_fields={ "collapsed_depth": 2, "node_style": "card", "show_node_ids": False, "max_depth": None, }, supports_span_target=False, description="Conversation tree with collapsible branching nodes" ), DisplayDefinition( name="agent_trace", renderer=AgentTraceDisplay(), required_fields=["key"], optional_fields={ "show_timestamps": False, "collapse_observations": False, "step_type_colors": None, "show_screenshots": True, "show_step_numbers": True, "show_summary": True, "compact": False, }, supports_span_target=False, description="Agent trace display with step cards and type badges" ), DisplayDefinition( name="eval_trace", renderer=EvalTraceDisplay(), required_fields=["key"], optional_fields={ "pane_labels": None, "show_step_numbers": True, "collapse_long_outputs": True, "max_output_lines": 20, "link_steps": True, "compact": False, }, supports_span_target=False, description="Three-pane agent trace eval: reasoning, function calls, and final answer side-by-side" ), DisplayDefinition( name="gallery", renderer=GalleryDisplay(), required_fields=["key"], optional_fields={ "layout": "horizontal", "thumbnail_size": 300, "show_captions": True, "zoomable": True, "max_height": 400, "columns": 3, }, supports_span_target=False, description="Scrollable image gallery with captions" ), DisplayDefinition( name="interactive_chat", renderer=InteractiveChatDisplay(), required_fields=["key"], optional_fields={ "placeholder_text": "Start chatting with the agent to begin the task.", }, supports_span_target=True, lazy_populated=True, # conversation is written by /agent_chat/finish description="Interactive agent chat with post-interaction trace display" ), DisplayDefinition( name="web_agent_trace", renderer=WebAgentTraceDisplay(), required_fields=["key"], optional_fields={ "show_overlays": True, "show_filmstrip": True, "show_thought": True, "show_observation": True, "show_element_info": True, "screenshot_max_width": 800, "screenshot_max_height": 600, "filmstrip_size": 80, "auto_playback": False, "playback_step_delay": 2.0, }, supports_span_target=False, description="Web agent trace viewer with screenshots, SVG overlays, and step navigation" ), DisplayDefinition( name="coding_trace", renderer=CodingTraceDisplay(), required_fields=["key"], optional_fields={ "show_file_tree": True, "diff_view": "unified", "collapse_long_outputs": True, "max_output_lines": 50, "terminal_theme": "dark", "show_step_numbers": True, "show_tool_badges": True, "show_reasoning": True, "compact": False, }, supports_span_target=True, description="Coding agent trace display with diff rendering, terminal blocks, and file tree" ), DisplayDefinition( name="live_agent", renderer=LiveAgentDisplay(), required_fields=["key"], optional_fields={ "show_overlays": True, "show_filmstrip": True, "show_thought": True, "show_controls": True, "allow_takeover": True, "allow_instructions": True, "screenshot_max_width": 900, "screenshot_max_height": 650, "filmstrip_size": 80, }, supports_span_target=False, lazy_populated=True, # trace populated by live agent session description="Live AI agent viewer with real-time screenshots, controls, and interaction" ), DisplayDefinition( name="live_coding_agent", renderer=LiveCodingAgentDisplay(), required_fields=["key"], optional_fields={ "show_file_tree": True, "show_reasoning": True, "collapse_long_outputs": True, "max_output_lines": 50, "show_controls": True, "allow_instructions": True, }, supports_span_target=False, lazy_populated=True, # trace populated by live coding-agent session description="Live coding agent viewer with real-time streaming and intervention controls" ), ] for display in displays: display_registry.register(display) logger.debug(f"Registered {len(displays)} built-in display types") # Auto-register built-in displays on import _register_builtin_displays()