"""Component registry and discovery for navi_ui."""
from __future__ import annotations
from pathlib import Path
import structlog
from .base import UIComponent
log = structlog.get_logger()
class ComponentRegistry:
"""Discovers and holds UIComponent subclasses."""
def __init__(self) -> None:
self._components: dict[str, type[UIComponent]] = {}
def register(self, component_cls: type[UIComponent]) -> type[UIComponent]:
"""Register a component class."""
if not component_cls.name:
raise ValueError("UIComponent must define a non-empty name")
self._components[component_cls.name] = component_cls
return component_cls
def get(self, name: str) -> type[UIComponent] | None:
return self._components.get(name)
def list_names(self) -> list[str]:
return sorted(self._components.keys())
def instructions(self) -> str:
parts = [
"## Supported components\n",
"Call render_component(component_name, payload, session_id). "
"Only the following component_name values are supported.\n",
]
for name in self.list_names():
parts.append(self._components[name].instructions())
parts.append(
"\nHard rules:\n"
"1. component_name must be one of the supported values above.\n"
"2. session_id is injected automatically by the agent.\n"
"3. Keep payload compact. If more data is available, say so in text and ask before rendering more.\n"
"4. After calling render_component, still provide a short text summary and offer next steps.\n"
"5. If validation fails, fix the payload and retry.\n"
)
return "\n".join(parts)
def discover_components(registry: ComponentRegistry | None = None) -> ComponentRegistry:
"""Import all modules next to this file and collect UIComponent subclasses."""
if registry is None:
registry = ComponentRegistry()
package_dir = Path(__file__).parent
for path in sorted(package_dir.glob("*.py")):
if path.name.startswith("_"):
continue
module_name = f"navi.mcp.ui_server.components.{path.stem}"
try:
__import__(module_name)
except Exception as exc: # noqa: BLE001
# Keep discovery resilient; broken modules are not fatal.
log.warning("ui_component_discovery_failed", module=module_name, error=str(exc))
continue
# Walk the full inheritance tree so intermediate subclasses (e.g.
# RealtorCardGrid extending CardGrid) are still discovered.
def _walk(base: type[UIComponent]) -> None:
for cls in base.__subclasses__():
if cls.name and cls.name not in registry._components:
registry.register(cls)
_walk(cls)
_walk(UIComponent)
return registry