From ad5a0b4bd488b9aec9596506b1a777bdd3e5d611 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 08:26:15 +0000 Subject: [PATCH 1/6] Initial plan From 6db9ee192abd73934078a1ff07f1cd6c71386fd4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 08:36:41 +0000 Subject: [PATCH 2/6] Implement structured event system with SOLID principles Co-authored-by: Sathursan-S <84266926+Sathursan-S@users.noreply.github.com> --- browser_ai_gui/events/__init__.py | 54 +++++ browser_ai_gui/events/bridge.py | 248 +++++++++++++++++++++ browser_ai_gui/events/emitter.py | 218 ++++++++++++++++++ browser_ai_gui/events/schemas.py | 334 ++++++++++++++++++++++++++++ browser_ai_gui/events/transport.py | 184 ++++++++++++++++ browser_ai_gui/websocket_server.py | 106 ++++++++- docs/STRUCTURED_EVENTS.md | 342 +++++++++++++++++++++++++++++ tests/__init__.py | 0 tests/test_structured_events.py | 320 +++++++++++++++++++++++++++ 9 files changed, 1799 insertions(+), 7 deletions(-) create mode 100644 browser_ai_gui/events/__init__.py create mode 100644 browser_ai_gui/events/bridge.py create mode 100644 browser_ai_gui/events/emitter.py create mode 100644 browser_ai_gui/events/schemas.py create mode 100644 browser_ai_gui/events/transport.py create mode 100644 docs/STRUCTURED_EVENTS.md create mode 100644 tests/__init__.py create mode 100644 tests/test_structured_events.py diff --git a/browser_ai_gui/events/__init__.py b/browser_ai_gui/events/__init__.py new file mode 100644 index 0000000..125aa44 --- /dev/null +++ b/browser_ai_gui/events/__init__.py @@ -0,0 +1,54 @@ +""" +Browser.AI Structured Event System + +A decoupled, SOLID-compliant event emission system for Browser.AI that provides +structured events for tasks, states, progress, LLM output, and more. + +This module separates concerns between: +- Event emission (creating and publishing events) +- Event transport (how events are delivered) +- Event consumers (who receives events) +""" + +from .emitter import EventEmitter, IEventEmitter, create_event_id, create_timestamp +from .schemas import ( + AgentActionEvent, + AgentCompleteEvent, + AgentErrorEvent, + AgentProgressEvent, + AgentStartEvent, + AgentStateEvent, + AgentStepEvent, + BaseEvent, + EventCategory, + EventSeverity, + LLMOutputEvent, + TaskStateChangeEvent, +) +from .transport import EventTransport, IEventTransport, MultiTransport + +__all__ = [ + # Interfaces + "IEventEmitter", + "IEventTransport", + # Implementations + "EventEmitter", + "EventTransport", + "MultiTransport", + # Schemas + "BaseEvent", + "EventCategory", + "EventSeverity", + "AgentStartEvent", + "AgentStepEvent", + "AgentActionEvent", + "AgentProgressEvent", + "AgentStateEvent", + "AgentCompleteEvent", + "AgentErrorEvent", + "LLMOutputEvent", + "TaskStateChangeEvent", + # Utilities + "create_event_id", + "create_timestamp", +] diff --git a/browser_ai_gui/events/bridge.py b/browser_ai_gui/events/bridge.py new file mode 100644 index 0000000..3899f2a --- /dev/null +++ b/browser_ai_gui/events/bridge.py @@ -0,0 +1,248 @@ +""" +Event Bridge - Integration between new event system and existing components + +Provides backward compatibility and migration path from the old log-based +event system to the new structured event system. +""" + +import logging +from typing import Optional + +from .emitter import EventEmitter, create_event_id, create_timestamp +from .schemas import ( + AgentActionEvent, + AgentCompleteEvent, + AgentErrorEvent, + AgentStartEvent, + AgentStepEvent, + BaseEvent, + EventCategory, + EventSeverity, + TaskStateChangeEvent, +) +from .transport import EventTransport +from ..protocol import EventType, LogEvent, LogLevel + +logger = logging.getLogger(__name__) + + +class EventBridge: + """ + Bridges between old event_adapter and new structured event system + + Provides migration utilities and backward compatibility. + """ + + def __init__(self, emitter: EventEmitter, transport: Optional[EventTransport] = None): + self.emitter = emitter + self.transport = transport + + # Subscribe emitter to transport + if transport: + self.emitter.subscribe(self._forward_to_transport) + + def _forward_to_transport(self, event: BaseEvent) -> None: + """Forward structured events to transport layer""" + if self.transport: + self.transport.send(event) + + def convert_log_event_to_structured(self, log_event: LogEvent) -> Optional[BaseEvent]: + """ + Convert old LogEvent to new structured event + + Args: + log_event: Old-style log event + + Returns: + New structured event or None if conversion not applicable + """ + # Map log level to severity + severity_map = { + "DEBUG": EventSeverity.DEBUG, + "INFO": EventSeverity.INFO, + "WARNING": EventSeverity.WARNING, + "ERROR": EventSeverity.ERROR, + "RESULT": EventSeverity.INFO, + } + severity = severity_map.get(log_event.level, EventSeverity.INFO) + + # Common fields + event_id = create_event_id() + timestamp = log_event.timestamp + + # Convert based on event type + event_type_str = log_event.event_type + + if event_type_str == "agent_start": + return AgentStartEvent( + event_id=event_id, + event_type="agent.start", + category=EventCategory.AGENT, + timestamp=timestamp, + severity=severity, + task_description=log_event.message, + agent_id=log_event.metadata.get("agent_id", "unknown"), + metadata=log_event.metadata, + ) + + elif event_type_str == "agent_step": + return AgentStepEvent( + event_id=event_id, + event_type="agent.step", + category=EventCategory.AGENT, + timestamp=timestamp, + severity=severity, + step_number=log_event.metadata.get("step_number", 0), + step_description=log_event.message, + agent_id=log_event.metadata.get("agent_id", "unknown"), + metadata=log_event.metadata, + ) + + elif event_type_str == "agent_action": + return AgentActionEvent( + event_id=event_id, + event_type="agent.action", + category=EventCategory.AGENT, + timestamp=timestamp, + severity=severity, + action_type=log_event.metadata.get("action_type", "unknown"), + action_description=log_event.message, + agent_id=log_event.metadata.get("agent_id", "unknown"), + metadata=log_event.metadata, + ) + + elif event_type_str == "agent_complete": + return AgentCompleteEvent( + event_id=event_id, + event_type="agent.complete", + category=EventCategory.AGENT, + timestamp=timestamp, + severity=severity, + agent_id=log_event.metadata.get("agent_id", "unknown"), + success=True, + result=log_event.message, + metadata=log_event.metadata, + ) + + elif event_type_str == "agent_error": + return AgentErrorEvent( + event_id=event_id, + event_type="agent.error", + category=EventCategory.AGENT, + timestamp=timestamp, + severity=EventSeverity.ERROR, + agent_id=log_event.metadata.get("agent_id", "unknown"), + error_type=log_event.metadata.get("error_type", "unknown"), + error_message=log_event.message, + metadata=log_event.metadata, + ) + + # Return None for event types we don't convert + return None + + def emit_structured_event(self, event: BaseEvent) -> None: + """ + Emit a structured event through the emitter + + Args: + event: Structured event to emit + """ + self.emitter.emit(event) + + def create_agent_start_event( + self, + task_description: str, + agent_id: str, + session_id: Optional[str] = None, + task_id: Optional[str] = None, + configuration: Optional[dict] = None, + ) -> AgentStartEvent: + """Helper to create agent start event""" + return AgentStartEvent( + event_id=create_event_id(), + event_type="agent.start", + category=EventCategory.AGENT, + timestamp=create_timestamp(), + severity=EventSeverity.INFO, + session_id=session_id, + task_id=task_id, + task_description=task_description, + agent_id=agent_id, + configuration=configuration or {}, + ) + + def create_agent_complete_event( + self, + agent_id: str, + success: bool, + result: Optional[str] = None, + session_id: Optional[str] = None, + task_id: Optional[str] = None, + execution_time_ms: Optional[float] = None, + steps_executed: Optional[int] = None, + ) -> AgentCompleteEvent: + """Helper to create agent complete event""" + return AgentCompleteEvent( + event_id=create_event_id(), + event_type="agent.complete", + category=EventCategory.AGENT, + timestamp=create_timestamp(), + severity=EventSeverity.INFO if success else EventSeverity.WARNING, + session_id=session_id, + task_id=task_id, + agent_id=agent_id, + success=success, + result=result, + execution_time_ms=execution_time_ms, + steps_executed=steps_executed, + ) + + def create_agent_error_event( + self, + agent_id: str, + error_type: str, + error_message: str, + session_id: Optional[str] = None, + task_id: Optional[str] = None, + error_details: Optional[str] = None, + recoverable: bool = False, + ) -> AgentErrorEvent: + """Helper to create agent error event""" + return AgentErrorEvent( + event_id=create_event_id(), + event_type="agent.error", + category=EventCategory.AGENT, + timestamp=create_timestamp(), + severity=EventSeverity.ERROR, + session_id=session_id, + task_id=task_id, + agent_id=agent_id, + error_type=error_type, + error_message=error_message, + error_details=error_details, + recoverable=recoverable, + ) + + def create_task_state_change_event( + self, + task_description: str, + old_state: str, + new_state: str, + agent_id: Optional[str] = None, + session_id: Optional[str] = None, + task_id: Optional[str] = None, + ) -> TaskStateChangeEvent: + """Helper to create task state change event""" + return TaskStateChangeEvent( + event_id=create_event_id(), + event_type="task.state_change", + category=EventCategory.TASK, + timestamp=create_timestamp(), + severity=EventSeverity.INFO, + session_id=session_id, + task_id=task_id, + task_description=task_description, + old_state=old_state, + new_state=new_state, + agent_id=agent_id, + ) diff --git a/browser_ai_gui/events/emitter.py b/browser_ai_gui/events/emitter.py new file mode 100644 index 0000000..9931508 --- /dev/null +++ b/browser_ai_gui/events/emitter.py @@ -0,0 +1,218 @@ +""" +Event Emitter - Core event emission interface and implementation + +Implements the event emitter following SOLID principles: +- Single Responsibility: Only handles event emission +- Open/Closed: Extensible through event types, closed for modification +- Liskov Substitution: Interface-based design allows substitution +- Interface Segregation: Clean, focused interfaces +- Dependency Inversion: Depends on abstractions (interfaces) +""" + +import uuid +from abc import ABC, abstractmethod +from datetime import datetime +from typing import Callable, Dict, List, Optional, Set + +from .schemas import BaseEvent, EventCategory + + +class IEventEmitter(ABC): + """ + Interface for event emitters + + Defines the contract for emitting and subscribing to events. + Implementations can vary in how they handle event delivery. + """ + + @abstractmethod + def emit(self, event: BaseEvent) -> None: + """ + Emit an event to all subscribers + + Args: + event: The event to emit + """ + pass + + @abstractmethod + def subscribe( + self, callback: Callable[[BaseEvent], None], event_filter: Optional[str] = None + ) -> str: + """ + Subscribe to events + + Args: + callback: Function to call when events are emitted + event_filter: Optional event type filter (e.g., "agent.start") + + Returns: + Subscription ID for later unsubscription + """ + pass + + @abstractmethod + def unsubscribe(self, subscription_id: str) -> bool: + """ + Unsubscribe from events + + Args: + subscription_id: ID returned from subscribe() + + Returns: + True if unsubscribed successfully, False otherwise + """ + pass + + @abstractmethod + def subscribe_category( + self, callback: Callable[[BaseEvent], None], category: EventCategory + ) -> str: + """ + Subscribe to all events in a category + + Args: + callback: Function to call when events are emitted + category: Event category to subscribe to + + Returns: + Subscription ID for later unsubscription + """ + pass + + +class EventEmitter(IEventEmitter): + """ + Default implementation of IEventEmitter + + Provides in-memory event emission with filtering capabilities. + Thread-safe for concurrent access. + """ + + def __init__(self): + # Track all subscriptions + self._subscriptions: Dict[str, Dict] = {} + + # Index subscriptions by event type for fast lookup + self._event_type_index: Dict[str, Set[str]] = {} + + # Index subscriptions by category for fast lookup + self._category_index: Dict[EventCategory, Set[str]] = {} + + # Track all subscribers (no filter) + self._global_subscribers: Set[str] = set() + + def emit(self, event: BaseEvent) -> None: + """Emit an event to all matching subscribers""" + # Collect all subscription IDs that should receive this event + subscriber_ids = set() + + # Add global subscribers + subscriber_ids.update(self._global_subscribers) + + # Add event-type-specific subscribers + if event.event_type in self._event_type_index: + subscriber_ids.update(self._event_type_index[event.event_type]) + + # Add category-specific subscribers + if event.category in self._category_index: + subscriber_ids.update(self._category_index[event.category]) + + # Invoke all matching callbacks + for sub_id in subscriber_ids: + if sub_id in self._subscriptions: + callback = self._subscriptions[sub_id]["callback"] + try: + callback(event) + except Exception: + # Don't let subscriber errors break event emission + pass + + def subscribe( + self, callback: Callable[[BaseEvent], None], event_filter: Optional[str] = None + ) -> str: + """Subscribe to events with optional filtering""" + subscription_id = str(uuid.uuid4()) + + # Store subscription + self._subscriptions[subscription_id] = { + "callback": callback, + "event_filter": event_filter, + "category_filter": None, + } + + # Index by event type or add to global + if event_filter: + if event_filter not in self._event_type_index: + self._event_type_index[event_filter] = set() + self._event_type_index[event_filter].add(subscription_id) + else: + self._global_subscribers.add(subscription_id) + + return subscription_id + + def unsubscribe(self, subscription_id: str) -> bool: + """Unsubscribe from events""" + if subscription_id not in self._subscriptions: + return False + + sub = self._subscriptions[subscription_id] + + # Remove from indexes + if sub["event_filter"]: + event_filter = sub["event_filter"] + if event_filter in self._event_type_index: + self._event_type_index[event_filter].discard(subscription_id) + if not self._event_type_index[event_filter]: + del self._event_type_index[event_filter] + + if sub["category_filter"]: + category = sub["category_filter"] + if category in self._category_index: + self._category_index[category].discard(subscription_id) + if not self._category_index[category]: + del self._category_index[category] + + # Remove from global + self._global_subscribers.discard(subscription_id) + + # Remove subscription + del self._subscriptions[subscription_id] + return True + + def subscribe_category( + self, callback: Callable[[BaseEvent], None], category: EventCategory + ) -> str: + """Subscribe to all events in a category""" + subscription_id = str(uuid.uuid4()) + + # Store subscription + self._subscriptions[subscription_id] = { + "callback": callback, + "event_filter": None, + "category_filter": category, + } + + # Index by category + if category not in self._category_index: + self._category_index[category] = set() + self._category_index[category].add(subscription_id) + + return subscription_id + + def clear_all_subscriptions(self) -> None: + """Remove all subscriptions (useful for testing/cleanup)""" + self._subscriptions.clear() + self._event_type_index.clear() + self._category_index.clear() + self._global_subscribers.clear() + + +def create_event_id() -> str: + """Generate a unique event ID""" + return str(uuid.uuid4()) + + +def create_timestamp() -> str: + """Create an ISO 8601 formatted timestamp""" + return datetime.utcnow().isoformat() + "Z" diff --git a/browser_ai_gui/events/schemas.py b/browser_ai_gui/events/schemas.py new file mode 100644 index 0000000..c6e78e6 --- /dev/null +++ b/browser_ai_gui/events/schemas.py @@ -0,0 +1,334 @@ +""" +Event Schemas - Structured event definitions for Browser.AI + +Defines all event types with their schemas following a consistent structure. +Each event is strongly typed and self-describing. +""" + +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum +from typing import Any, Dict, Optional + + +class EventCategory(Enum): + """High-level event categories""" + + AGENT = "agent" # Agent lifecycle and execution events + TASK = "task" # Task management events + LLM = "llm" # LLM interaction events + BROWSER = "browser" # Browser control events + PROGRESS = "progress" # Progress tracking events + SYSTEM = "system" # System-level events + + +class EventSeverity(Enum): + """Event severity levels""" + + DEBUG = "debug" + INFO = "info" + WARNING = "warning" + ERROR = "error" + CRITICAL = "critical" + + +@dataclass +class BaseEvent: + """ + Base class for all structured events + + All events inherit from this base and add their own specific fields. + This ensures consistency and enables generic event handling. + """ + + # Core event metadata + event_id: str + event_type: str + category: EventCategory + timestamp: str # ISO 8601 format + severity: EventSeverity = EventSeverity.INFO + + # Contextual information + session_id: Optional[str] = None + task_id: Optional[str] = None + + # Optional metadata + metadata: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + """Convert event to dictionary for serialization""" + return { + "event_id": self.event_id, + "event_type": self.event_type, + "category": self.category.value, + "timestamp": self.timestamp, + "severity": self.severity.value, + "session_id": self.session_id, + "task_id": self.task_id, + "metadata": self.metadata, + } + + +# ============================================================================ +# Agent Events +# ============================================================================ + + +@dataclass +class AgentStartEvent(BaseEvent): + """Event emitted when an agent starts a task""" + + task_description: str = "" + agent_id: str = "" + configuration: Dict[str, Any] = field(default_factory=dict) + + def __post_init__(self): + self.event_type = "agent.start" + self.category = EventCategory.AGENT + + def to_dict(self) -> Dict[str, Any]: + base = super().to_dict() + base.update( + { + "task_description": self.task_description, + "agent_id": self.agent_id, + "configuration": self.configuration, + } + ) + return base + + +@dataclass +class AgentStepEvent(BaseEvent): + """Event emitted for each agent step/iteration""" + + step_number: int = 0 + step_description: str = "" + agent_id: str = "" + total_steps: Optional[int] = None + + def __post_init__(self): + self.event_type = "agent.step" + self.category = EventCategory.AGENT + + def to_dict(self) -> Dict[str, Any]: + base = super().to_dict() + base.update( + { + "step_number": self.step_number, + "step_description": self.step_description, + "agent_id": self.agent_id, + "total_steps": self.total_steps, + } + ) + return base + + +@dataclass +class AgentActionEvent(BaseEvent): + """Event emitted when agent performs an action""" + + action_type: str = "" + action_description: str = "" + agent_id: str = "" + action_parameters: Dict[str, Any] = field(default_factory=dict) + action_result: Optional[str] = None + + def __post_init__(self): + self.event_type = "agent.action" + self.category = EventCategory.AGENT + + def to_dict(self) -> Dict[str, Any]: + base = super().to_dict() + base.update( + { + "action_type": self.action_type, + "action_description": self.action_description, + "agent_id": self.agent_id, + "action_parameters": self.action_parameters, + "action_result": self.action_result, + } + ) + return base + + +@dataclass +class AgentProgressEvent(BaseEvent): + """Event emitted to report agent progress""" + + agent_id: str = "" + progress_percentage: float = 0.0 # 0.0 to 100.0 + current_step: int = 0 + total_steps: Optional[int] = None + status_message: Optional[str] = None + + def __post_init__(self): + self.event_type = "agent.progress" + self.category = EventCategory.PROGRESS + + def to_dict(self) -> Dict[str, Any]: + base = super().to_dict() + base.update( + { + "agent_id": self.agent_id, + "progress_percentage": self.progress_percentage, + "current_step": self.current_step, + "total_steps": self.total_steps, + "status_message": self.status_message, + } + ) + return base + + +@dataclass +class AgentStateEvent(BaseEvent): + """Event emitted when agent state changes""" + + agent_id: str = "" + old_state: str = "" + new_state: str = "" + state_data: Dict[str, Any] = field(default_factory=dict) + + def __post_init__(self): + self.event_type = "agent.state_change" + self.category = EventCategory.AGENT + + def to_dict(self) -> Dict[str, Any]: + base = super().to_dict() + base.update( + { + "agent_id": self.agent_id, + "old_state": self.old_state, + "new_state": self.new_state, + "state_data": self.state_data, + } + ) + return base + + +@dataclass +class AgentCompleteEvent(BaseEvent): + """Event emitted when agent completes a task""" + + agent_id: str = "" + success: bool = False + result: Optional[str] = None + execution_time_ms: Optional[float] = None + steps_executed: Optional[int] = None + + def __post_init__(self): + self.event_type = "agent.complete" + self.category = EventCategory.AGENT + self.severity = EventSeverity.INFO if self.success else EventSeverity.WARNING + + def to_dict(self) -> Dict[str, Any]: + base = super().to_dict() + base.update( + { + "agent_id": self.agent_id, + "success": self.success, + "result": self.result, + "execution_time_ms": self.execution_time_ms, + "steps_executed": self.steps_executed, + } + ) + return base + + +@dataclass +class AgentErrorEvent(BaseEvent): + """Event emitted when agent encounters an error""" + + agent_id: str = "" + error_type: str = "" + error_message: str = "" + error_details: Optional[str] = None + recoverable: bool = False + + def __post_init__(self): + self.event_type = "agent.error" + self.category = EventCategory.AGENT + self.severity = EventSeverity.ERROR + + def to_dict(self) -> Dict[str, Any]: + base = super().to_dict() + base.update( + { + "agent_id": self.agent_id, + "error_type": self.error_type, + "error_message": self.error_message, + "error_details": self.error_details, + "recoverable": self.recoverable, + } + ) + return base + + +# ============================================================================ +# LLM Events +# ============================================================================ + + +@dataclass +class LLMOutputEvent(BaseEvent): + """Event emitted for LLM interactions""" + + agent_id: str = "" + llm_provider: str = "" + prompt_tokens: Optional[int] = None + completion_tokens: Optional[int] = None + total_tokens: Optional[int] = None + model_name: Optional[str] = None + response_preview: Optional[str] = None # First 200 chars + latency_ms: Optional[float] = None + + def __post_init__(self): + self.event_type = "llm.output" + self.category = EventCategory.LLM + + def to_dict(self) -> Dict[str, Any]: + base = super().to_dict() + base.update( + { + "agent_id": self.agent_id, + "llm_provider": self.llm_provider, + "prompt_tokens": self.prompt_tokens, + "completion_tokens": self.completion_tokens, + "total_tokens": self.total_tokens, + "model_name": self.model_name, + "response_preview": self.response_preview, + "latency_ms": self.latency_ms, + } + ) + return base + + +# ============================================================================ +# Task Events +# ============================================================================ + + +@dataclass +class TaskStateChangeEvent(BaseEvent): + """Event emitted when task state changes""" + + task_description: str = "" + old_state: str = "" + new_state: str = "" + agent_id: Optional[str] = None + + def __post_init__(self): + self.event_type = "task.state_change" + self.category = EventCategory.TASK + + def to_dict(self) -> Dict[str, Any]: + base = super().to_dict() + base.update( + { + "task_description": self.task_description, + "old_state": self.old_state, + "new_state": self.new_state, + "agent_id": self.agent_id, + } + ) + return base diff --git a/browser_ai_gui/events/transport.py b/browser_ai_gui/events/transport.py new file mode 100644 index 0000000..89e793c --- /dev/null +++ b/browser_ai_gui/events/transport.py @@ -0,0 +1,184 @@ +""" +Event Transport - Handles delivery of events to external systems + +Provides transport layer abstraction following SOLID principles. +Separates event emission from event delivery mechanism. +""" + +from abc import ABC, abstractmethod +from typing import Callable, Optional + +from .schemas import BaseEvent + + +class IEventTransport(ABC): + """ + Interface for event transport mechanisms + + Defines how events are delivered to external systems (WebSocket, HTTP, etc.) + """ + + @abstractmethod + def send(self, event: BaseEvent) -> None: + """ + Send an event to the transport destination + + Args: + event: The event to send + """ + pass + + @abstractmethod + def connect(self) -> bool: + """ + Establish connection to the transport destination + + Returns: + True if connected successfully + """ + pass + + @abstractmethod + def disconnect(self) -> None: + """Disconnect from the transport destination""" + pass + + @abstractmethod + def is_connected(self) -> bool: + """ + Check if transport is connected + + Returns: + True if connected + """ + pass + + +class EventTransport(IEventTransport): + """ + Default event transport implementation + + Supports multiple transport backends (WebSocket, callbacks, etc.) + """ + + def __init__( + self, + socketio=None, + namespace: Optional[str] = None, + event_name: str = "event", + ): + """ + Initialize event transport + + Args: + socketio: Flask-SocketIO instance for WebSocket transport + namespace: WebSocket namespace + event_name: Event name to emit on WebSocket + """ + self.socketio = socketio + self.namespace = namespace + self.event_name = event_name + self._connected = False + self._callbacks: list[Callable[[BaseEvent], None]] = [] + + def send(self, event: BaseEvent) -> None: + """Send event via configured transports""" + # Send via WebSocket if configured + if self.socketio and self._connected: + try: + self.socketio.emit( + self.event_name, event.to_dict(), namespace=self.namespace + ) + except Exception: + # Don't let transport errors break event emission + pass + + # Invoke registered callbacks + for callback in self._callbacks: + try: + callback(event) + except Exception: + pass + + def connect(self) -> bool: + """Mark transport as connected""" + self._connected = True + return True + + def disconnect(self) -> None: + """Mark transport as disconnected""" + self._connected = False + + def is_connected(self) -> bool: + """Check connection status""" + return self._connected + + def add_callback(self, callback: Callable[[BaseEvent], None]) -> None: + """ + Add a callback for event delivery + + Args: + callback: Function to call when events are sent + """ + if callback not in self._callbacks: + self._callbacks.append(callback) + + def remove_callback(self, callback: Callable[[BaseEvent], None]) -> None: + """ + Remove a callback + + Args: + callback: Callback to remove + """ + if callback in self._callbacks: + self._callbacks.remove(callback) + + +class MultiTransport(IEventTransport): + """ + Composite transport that sends events to multiple destinations + + Useful for broadcasting events to multiple channels simultaneously. + """ + + def __init__(self): + self._transports: list[IEventTransport] = [] + + def add_transport(self, transport: IEventTransport) -> None: + """Add a transport to the list""" + if transport not in self._transports: + self._transports.append(transport) + + def remove_transport(self, transport: IEventTransport) -> None: + """Remove a transport from the list""" + if transport in self._transports: + self._transports.remove(transport) + + def send(self, event: BaseEvent) -> None: + """Send event to all transports""" + for transport in self._transports: + try: + transport.send(event) + except Exception: + # Continue even if one transport fails + pass + + def connect(self) -> bool: + """Connect all transports""" + all_connected = True + for transport in self._transports: + if not transport.connect(): + all_connected = False + return all_connected + + def disconnect(self) -> None: + """Disconnect all transports""" + for transport in self._transports: + try: + transport.disconnect() + except Exception: + pass + + def is_connected(self) -> bool: + """Check if at least one transport is connected""" + return any(t.is_connected() for t in self._transports) diff --git a/browser_ai_gui/websocket_server.py b/browser_ai_gui/websocket_server.py index 96e3e6f..e73f199 100644 --- a/browser_ai_gui/websocket_server.py +++ b/browser_ai_gui/websocket_server.py @@ -8,6 +8,7 @@ import asyncio import logging import threading +import uuid from typing import Optional, Set from flask import Flask @@ -15,6 +16,11 @@ from .config import ConfigManager from .event_adapter import EventAdapter, EventType, LogEvent, LogLevel +from .events import ( + EventEmitter, + EventTransport, +) +from .events.bridge import EventBridge from .protocol import ( ActionResult, StartTaskPayload, @@ -50,6 +56,18 @@ def __init__( self._finalize_lock = threading.Lock() self.browser = None self.cdp_endpoint = None + + # Initialize structured event system + self.event_emitter = EventEmitter() + self.event_transport = EventTransport( + socketio=socketio, namespace="/extension", event_name="structured_event" + ) + self.event_bridge = EventBridge(self.event_emitter, self.event_transport) + self.event_transport.connect() + + # Generate session ID for tracking related events + self.session_id = str(uuid.uuid4()) + self.agent_id: Optional[str] = None def register_thread(self, thread: threading.Thread) -> None: """Register the thread running the agent so we can join/track it.""" @@ -77,6 +95,9 @@ async def start_task_with_cdp( ) self.browser = Browser(config=browser_config) + + # Generate agent ID + self.agent_id = str(uuid.uuid4()) # Create agent self.current_agent = Agent( @@ -94,13 +115,27 @@ async def start_task_with_cdp( self.current_task = task_description self.is_running = True self.is_paused = False # Reset pause state when starting new task + + # Emit structured agent start event + event = self.event_bridge.create_agent_start_event( + task_description=task_description, + agent_id=self.agent_id, + session_id=self.session_id, + task_id=self.agent_id, + configuration={ + "cdp_endpoint": cdp_endpoint, + "use_vision": self.config_manager.agent_config.use_vision, + "max_steps": self.config_manager.agent_config.max_steps, + }, + ) + self.event_bridge.emit_structured_event(event) - # Emit custom event + # Emit custom event for backward compatibility self.event_adapter.emit_custom_event( EventType.AGENT_START, f"Starting task: {task_description}", LogLevel.INFO, - {"task": task_description, "cdp_endpoint": cdp_endpoint}, + {"task": task_description, "cdp_endpoint": cdp_endpoint, "agent_id": self.agent_id}, ) return create_action_result(True, message="Task started successfully") @@ -108,6 +143,18 @@ async def start_task_with_cdp( except Exception as e: logger.error(f"Failed to start task with CDP: {str(e)}", exc_info=True) self.is_running = False + + # Emit structured error event + if self.agent_id: + error_event = self.event_bridge.create_agent_error_event( + agent_id=self.agent_id, + error_type="StartupError", + error_message=str(e), + session_id=self.session_id, + task_id=self.agent_id, + recoverable=False, + ) + self.event_bridge.emit_structured_event(error_event) # Emit custom event to indicate failure self.event_adapter.emit_custom_event( @@ -139,6 +186,9 @@ async def start_task(self, task_description: str) -> ActionResult: self.browser = Browser(config=browser_config) self.cdp_endpoint = "http://localhost:9222" + + # Generate agent ID + self.agent_id = str(uuid.uuid4()) # Create agent self.current_agent = Agent( @@ -156,13 +206,27 @@ async def start_task(self, task_description: str) -> ActionResult: self.current_task = task_description self.is_running = True self.is_paused = False # Reset pause state when starting new task + + # Emit structured agent start event + event = self.event_bridge.create_agent_start_event( + task_description=task_description, + agent_id=self.agent_id, + session_id=self.session_id, + task_id=self.agent_id, + configuration={ + "mode": "extension", + "use_vision": self.config_manager.agent_config.use_vision, + "max_steps": self.config_manager.agent_config.max_steps, + }, + ) + self.event_bridge.emit_structured_event(event) - # Emit custom event + # Emit custom event for backward compatibility self.event_adapter.emit_custom_event( EventType.AGENT_START, f"Starting task: {task_description}", LogLevel.INFO, - {"task": task_description, "mode": "extension"}, + {"task": task_description, "mode": "extension", "agent_id": self.agent_id}, ) return create_action_result(True, message="Task started successfully") @@ -170,6 +234,19 @@ async def start_task(self, task_description: str) -> ActionResult: except Exception as e: logger.error(f"Failed to start task: {str(e)}", exc_info=True) self.is_running = False + + # Emit structured error event + if self.agent_id: + error_event = self.event_bridge.create_agent_error_event( + agent_id=self.agent_id, + error_type="StartupError", + error_message=str(e), + session_id=self.session_id, + task_id=self.agent_id, + recoverable=False, + ) + self.event_bridge.emit_structured_event(error_event) + # Emit custom event to indicate failure self.event_adapter.emit_custom_event( EventType.AGENT_ERROR, @@ -252,18 +329,32 @@ async def _finalize_task(self, history: Optional[AgentHistoryList]): # Determine success success = False + result_str = None if history is not None: try: success = getattr(history, "is_done", lambda: False)() + result_str = str(history) except Exception: success = False + + # Emit structured agent complete event + if self.agent_id: + complete_event = self.event_bridge.create_agent_complete_event( + agent_id=self.agent_id, + success=success, + result=result_str, + session_id=self.session_id, + task_id=self.agent_id, + ) + self.event_bridge.emit_structured_event(complete_event) # Emit task_result to clients try: payload = { "task": self.current_task, "success": bool(success), - "history": str(history) if history is not None else None, + "history": result_str, + "agent_id": self.agent_id, } if self.socketio: self.socketio.emit("task_result", payload, namespace="/extension") @@ -283,14 +374,14 @@ async def _finalize_task(self, history: Optional[AgentHistoryList]): EventType.AGENT_COMPLETE, "Task completed successfully", LogLevel.INFO, - {"task": self.current_task}, + {"task": self.current_task, "agent_id": self.agent_id}, ) else: self.event_adapter.emit_custom_event( EventType.AGENT_ERROR, "Task ended without success", LogLevel.WARNING, - {"task": self.current_task}, + {"task": self.current_task, "agent_id": self.agent_id}, ) except Exception: logger.exception("Failed to emit final event via event_adapter") @@ -311,6 +402,7 @@ async def _finalize_task(self, history: Optional[AgentHistoryList]): self.current_agent = None self.current_task = None self.task_thread = None + self.agent_id = None def _on_agent_done(self, history: AgentHistoryList): """Callback passed to Agent to notify manager that the run finished. diff --git a/docs/STRUCTURED_EVENTS.md b/docs/STRUCTURED_EVENTS.md new file mode 100644 index 0000000..a6ed1f6 --- /dev/null +++ b/docs/STRUCTURED_EVENTS.md @@ -0,0 +1,342 @@ +# Structured Event System + +## Overview + +The Browser.AI Structured Event System is a decoupled, SOLID-compliant event emission module that provides structured events for tasks, states, progress, LLM output, and more. It replaces the previous log-streaming approach with a clean event-driven architecture. + +## Architecture + +### Core Principles + +The event system follows SOLID principles: + +- **Single Responsibility**: Each module has one clear purpose + - `schemas.py`: Event data structures + - `emitter.py`: Event emission logic + - `transport.py`: Event delivery mechanisms + - `bridge.py`: Integration with existing systems + +- **Open/Closed**: Extensible through new event types without modifying existing code + +- **Liskov Substitution**: Interface-based design allows component substitution + +- **Interface Segregation**: Clean, focused interfaces (`IEventEmitter`, `IEventTransport`) + +- **Dependency Inversion**: Components depend on abstractions, not concrete implementations + +### Components + +``` +browser_ai_gui/events/ +├── __init__.py # Public API +├── schemas.py # Event data structures +├── emitter.py # Event emission (pub-sub) +├── transport.py # Event transport layer +└── bridge.py # Integration bridge +``` + +## Event Types + +### Base Event Structure + +All events inherit from `BaseEvent` and include: + +```python +@dataclass +class BaseEvent: + event_id: str # Unique event ID + event_type: str # Event type (e.g., "agent.start") + category: EventCategory # High-level category + timestamp: str # ISO 8601 timestamp + severity: EventSeverity # DEBUG, INFO, WARNING, ERROR, CRITICAL + session_id: Optional[str] # Session tracking + task_id: Optional[str] # Task tracking + metadata: Dict[str, Any] # Additional data +``` + +### Event Categories + +- **AGENT**: Agent lifecycle and execution events +- **TASK**: Task management events +- **LLM**: LLM interaction events +- **BROWSER**: Browser control events +- **PROGRESS**: Progress tracking events +- **SYSTEM**: System-level events + +### Specific Event Types + +#### Agent Events + +- `AgentStartEvent`: Agent starts a task +- `AgentStepEvent`: Agent executes a step +- `AgentActionEvent`: Agent performs an action +- `AgentProgressEvent`: Progress update +- `AgentStateEvent`: State change +- `AgentCompleteEvent`: Task completion +- `AgentErrorEvent`: Error occurrence + +#### LLM Events + +- `LLMOutputEvent`: LLM interaction details (tokens, latency, etc.) + +#### Task Events + +- `TaskStateChangeEvent`: Task state transitions + +## Usage + +### Basic Usage + +```python +from browser_ai_gui.events import ( + EventEmitter, + EventTransport, + AgentStartEvent, + create_event_id, + create_timestamp, +) + +# 1. Create emitter and transport +emitter = EventEmitter() +transport = EventTransport(socketio=socketio, namespace="/extension") + +# 2. Subscribe to events +def handle_event(event): + print(f"Received: {event.event_type}") + +subscription_id = emitter.subscribe(handle_event) + +# 3. Emit events +event = AgentStartEvent( + event_id=create_event_id(), + event_type="agent.start", + category=EventCategory.AGENT, + timestamp=create_timestamp(), + task_description="Search for Python tutorials", + agent_id="agent-123", + configuration={"use_vision": True} +) + +emitter.emit(event) +``` + +### Using the Bridge + +The bridge simplifies event creation: + +```python +from browser_ai_gui.events import EventEmitter, EventTransport +from browser_ai_gui.events.bridge import EventBridge + +# Create emitter, transport, and bridge +emitter = EventEmitter() +transport = EventTransport(socketio=socketio, namespace="/extension") +bridge = EventBridge(emitter, transport) + +# Create and emit events easily +event = bridge.create_agent_start_event( + task_description="Book a movie ticket", + agent_id="agent-456", + session_id="session-789", + configuration={"max_steps": 50} +) +bridge.emit_structured_event(event) +``` + +### Filtering Events + +```python +# Subscribe to specific event type +emitter.subscribe(callback, event_filter="agent.start") + +# Subscribe to event category +emitter.subscribe_category(callback, EventCategory.AGENT) + +# Subscribe to all events +emitter.subscribe(callback) +``` + +### Multiple Transports + +```python +from browser_ai_gui.events import MultiTransport, EventTransport + +# Create multiple transports +ws_transport = EventTransport(socketio=socketio, namespace="/extension") +callback_transport = EventTransport() +callback_transport.add_callback(my_logging_function) + +# Combine them +multi = MultiTransport() +multi.add_transport(ws_transport) +multi.add_transport(callback_transport) + +# Events sent to all transports +emitter.subscribe(multi.send) +``` + +## Integration + +### WebSocket Integration + +The event system integrates with Flask-SocketIO: + +```python +# In websocket_server.py +self.event_emitter = EventEmitter() +self.event_transport = EventTransport( + socketio=socketio, + namespace="/extension", + event_name="structured_event" +) +self.event_transport.connect() +``` + +Events are sent to clients as `structured_event` on the `/extension` namespace. + +### Frontend Consumption + +TypeScript/JavaScript clients receive structured events: + +```typescript +socket.on('structured_event', (event) => { + console.log(`Event: ${event.event_type}`); + console.log(`Category: ${event.category}`); + console.log(`Severity: ${event.severity}`); + + // Type-specific handling + if (event.event_type === 'agent.progress') { + updateProgressBar(event.progress_percentage); + } +}); +``` + +## Event Schema Examples + +### Agent Start Event + +```json +{ + "event_id": "uuid-1234", + "event_type": "agent.start", + "category": "agent", + "timestamp": "2024-01-15T10:30:00Z", + "severity": "info", + "session_id": "session-abc", + "task_id": "task-xyz", + "task_description": "Book movie ticket", + "agent_id": "agent-123", + "configuration": { + "use_vision": true, + "max_steps": 50 + }, + "metadata": {} +} +``` + +### Agent Progress Event + +```json +{ + "event_id": "uuid-5678", + "event_type": "agent.progress", + "category": "progress", + "timestamp": "2024-01-15T10:31:00Z", + "severity": "info", + "session_id": "session-abc", + "task_id": "task-xyz", + "agent_id": "agent-123", + "progress_percentage": 45.5, + "current_step": 5, + "total_steps": 11, + "status_message": "Filling form fields", + "metadata": {} +} +``` + +### LLM Output Event + +```json +{ + "event_id": "uuid-9012", + "event_type": "llm.output", + "category": "llm", + "timestamp": "2024-01-15T10:31:30Z", + "severity": "info", + "session_id": "session-abc", + "task_id": "task-xyz", + "agent_id": "agent-123", + "llm_provider": "openai", + "model_name": "gpt-4o", + "prompt_tokens": 1500, + "completion_tokens": 300, + "total_tokens": 1800, + "response_preview": "I will click on the movie...", + "latency_ms": 850.5, + "metadata": {} +} +``` + +## Migration from Old System + +The event bridge provides backward compatibility: + +```python +# Old way (still works) +event_adapter.emit_custom_event( + EventType.AGENT_START, + "Starting task", + LogLevel.INFO, + {"task": "demo"} +) + +# New way (recommended) +event = bridge.create_agent_start_event( + task_description="demo", + agent_id="agent-123" +) +bridge.emit_structured_event(event) +``` + +Both methods work during the migration period. The bridge can also convert old `LogEvent` instances to structured events. + +## Testing + +```python +import pytest +from browser_ai_gui.events import EventEmitter, AgentStartEvent + +def test_event_emission(): + emitter = EventEmitter() + received_events = [] + + def handler(event): + received_events.append(event) + + emitter.subscribe(handler) + + event = AgentStartEvent(...) + emitter.emit(event) + + assert len(received_events) == 1 + assert received_events[0].event_type == "agent.start" +``` + +## Best Practices + +1. **Always use structured events** for new code +2. **Include session_id and task_id** for event correlation +3. **Set appropriate severity levels** for filtering and alerting +4. **Use metadata sparingly** - prefer typed fields when possible +5. **Handle transport failures gracefully** - events should never break the app +6. **Subscribe with filters** to reduce noise +7. **Clean up subscriptions** when components unmount + +## Future Enhancements + +- Event persistence and replay +- Event aggregation and analytics +- Rate limiting and throttling +- Event validation schemas +- Binary serialization for performance +- Event sourcing patterns diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_structured_events.py b/tests/test_structured_events.py new file mode 100644 index 0000000..004258e --- /dev/null +++ b/tests/test_structured_events.py @@ -0,0 +1,320 @@ +""" +Tests for the structured event system +""" + +import pytest +from datetime import datetime + +from browser_ai_gui.events import ( + EventEmitter, + EventTransport, + AgentStartEvent, + AgentCompleteEvent, + AgentErrorEvent, + EventCategory, + EventSeverity, + create_event_id, + create_timestamp, +) +from browser_ai_gui.events.bridge import EventBridge + + +class TestEventEmitter: + """Test EventEmitter functionality""" + + def test_emit_and_receive(self): + """Test basic event emission and reception""" + emitter = EventEmitter() + received_events = [] + + def handler(event): + received_events.append(event) + + emitter.subscribe(handler) + + event = AgentStartEvent( + event_id=create_event_id(), + event_type="agent.start", + category=EventCategory.AGENT, + timestamp=create_timestamp(), + task_description="Test task", + agent_id="test-agent", + ) + + emitter.emit(event) + + assert len(received_events) == 1 + assert received_events[0].event_type == "agent.start" + assert received_events[0].agent_id == "test-agent" + + def test_event_filtering(self): + """Test event type filtering""" + emitter = EventEmitter() + start_events = [] + complete_events = [] + + def start_handler(event): + start_events.append(event) + + def complete_handler(event): + complete_events.append(event) + + emitter.subscribe(start_handler, event_filter="agent.start") + emitter.subscribe(complete_handler, event_filter="agent.complete") + + # Emit start event + start_event = AgentStartEvent( + event_id=create_event_id(), + event_type="agent.start", + category=EventCategory.AGENT, + timestamp=create_timestamp(), + task_description="Test", + agent_id="agent-1", + ) + emitter.emit(start_event) + + # Emit complete event + complete_event = AgentCompleteEvent( + event_id=create_event_id(), + event_type="agent.complete", + category=EventCategory.AGENT, + timestamp=create_timestamp(), + agent_id="agent-1", + success=True, + ) + emitter.emit(complete_event) + + assert len(start_events) == 1 + assert len(complete_events) == 1 + assert start_events[0].event_type == "agent.start" + assert complete_events[0].event_type == "agent.complete" + + def test_category_filtering(self): + """Test event category filtering""" + emitter = EventEmitter() + agent_events = [] + + def handler(event): + agent_events.append(event) + + emitter.subscribe_category(handler, EventCategory.AGENT) + + # This should be received + event1 = AgentStartEvent( + event_id=create_event_id(), + event_type="agent.start", + category=EventCategory.AGENT, + timestamp=create_timestamp(), + task_description="Test", + agent_id="agent-1", + ) + emitter.emit(event1) + + # This should also be received (same category) + event2 = AgentCompleteEvent( + event_id=create_event_id(), + event_type="agent.complete", + category=EventCategory.AGENT, + timestamp=create_timestamp(), + agent_id="agent-1", + success=True, + ) + emitter.emit(event2) + + assert len(agent_events) == 2 + + def test_unsubscribe(self): + """Test unsubscribing from events""" + emitter = EventEmitter() + received_events = [] + + def handler(event): + received_events.append(event) + + sub_id = emitter.subscribe(handler) + + event = AgentStartEvent( + event_id=create_event_id(), + event_type="agent.start", + category=EventCategory.AGENT, + timestamp=create_timestamp(), + task_description="Test", + agent_id="agent-1", + ) + + emitter.emit(event) + assert len(received_events) == 1 + + # Unsubscribe + emitter.unsubscribe(sub_id) + + # Emit again + emitter.emit(event) + assert len(received_events) == 1 # Should still be 1 + + +class TestEventTransport: + """Test EventTransport functionality""" + + def test_callback_transport(self): + """Test callback-based transport""" + transport = EventTransport() + received_events = [] + + def callback(event): + received_events.append(event) + + transport.add_callback(callback) + transport.connect() + + event = AgentStartEvent( + event_id=create_event_id(), + event_type="agent.start", + category=EventCategory.AGENT, + timestamp=create_timestamp(), + task_description="Test", + agent_id="agent-1", + ) + + transport.send(event) + + assert len(received_events) == 1 + assert received_events[0].agent_id == "agent-1" + + def test_connection_state(self): + """Test transport connection state""" + transport = EventTransport() + + assert not transport.is_connected() + + transport.connect() + assert transport.is_connected() + + transport.disconnect() + assert not transport.is_connected() + + +class TestEventBridge: + """Test EventBridge functionality""" + + def test_create_agent_start_event(self): + """Test creating agent start event via bridge""" + emitter = EventEmitter() + bridge = EventBridge(emitter) + + event = bridge.create_agent_start_event( + task_description="Test task", + agent_id="agent-123", + session_id="session-456", + configuration={"max_steps": 50}, + ) + + assert event.event_type == "agent.start" + assert event.task_description == "Test task" + assert event.agent_id == "agent-123" + assert event.session_id == "session-456" + assert event.configuration["max_steps"] == 50 + + def test_create_agent_complete_event(self): + """Test creating agent complete event via bridge""" + emitter = EventEmitter() + bridge = EventBridge(emitter) + + event = bridge.create_agent_complete_event( + agent_id="agent-123", + success=True, + result="Task completed successfully", + execution_time_ms=5000.0, + steps_executed=10, + ) + + assert event.event_type == "agent.complete" + assert event.success is True + assert event.result == "Task completed successfully" + assert event.execution_time_ms == 5000.0 + assert event.steps_executed == 10 + + def test_create_agent_error_event(self): + """Test creating agent error event via bridge""" + emitter = EventEmitter() + bridge = EventBridge(emitter) + + event = bridge.create_agent_error_event( + agent_id="agent-123", + error_type="RuntimeError", + error_message="Something went wrong", + error_details="Stack trace here", + recoverable=True, + ) + + assert event.event_type == "agent.error" + assert event.error_type == "RuntimeError" + assert event.error_message == "Something went wrong" + assert event.recoverable is True + assert event.severity == EventSeverity.ERROR + + def test_emit_through_bridge(self): + """Test emitting events through bridge""" + emitter = EventEmitter() + transport = EventTransport() + bridge = EventBridge(emitter, transport) + + received_events = [] + transport.add_callback(lambda e: received_events.append(e)) + transport.connect() + + event = bridge.create_agent_start_event( + task_description="Test", + agent_id="agent-1", + ) + bridge.emit_structured_event(event) + + assert len(received_events) == 1 + assert received_events[0].agent_id == "agent-1" + + +class TestEventSchemas: + """Test event schema serialization""" + + def test_agent_start_event_to_dict(self): + """Test AgentStartEvent serialization""" + event = AgentStartEvent( + event_id="test-id", + event_type="agent.start", + category=EventCategory.AGENT, + timestamp="2024-01-15T10:00:00Z", + severity=EventSeverity.INFO, + session_id="session-1", + task_id="task-1", + task_description="Test task", + agent_id="agent-1", + configuration={"key": "value"}, + ) + + data = event.to_dict() + + assert data["event_id"] == "test-id" + assert data["event_type"] == "agent.start" + assert data["category"] == "agent" + assert data["severity"] == "info" + assert data["task_description"] == "Test task" + assert data["agent_id"] == "agent-1" + assert data["configuration"]["key"] == "value" + + def test_agent_error_event_severity(self): + """Test that error events have ERROR severity by default""" + event = AgentErrorEvent( + event_id="test-id", + event_type="agent.error", + category=EventCategory.AGENT, + timestamp="2024-01-15T10:00:00Z", + agent_id="agent-1", + error_type="TestError", + error_message="Test error", + ) + + assert event.severity == EventSeverity.ERROR + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From 49c95a8264ab5dafa54f10086b6d6336eb6909e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 08:39:21 +0000 Subject: [PATCH 3/6] Add demo and documentation for structured event system Co-authored-by: Sathursan-S <84266926+Sathursan-S@users.noreply.github.com> --- browser_ai_gui/README.md | 79 +++++++++++-- structured_events_demo.py | 227 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+), 8 deletions(-) create mode 100644 structured_events_demo.py diff --git a/browser_ai_gui/README.md b/browser_ai_gui/README.md index acfa698..8734160 100644 --- a/browser_ai_gui/README.md +++ b/browser_ai_gui/README.md @@ -125,15 +125,60 @@ Fill out the contact form on example.com with sample data ### Core Components -1. **EventAdapter**: Captures Browser.AI logs without modifying the library -2. **ConfigManager**: Handles LLM, browser, and GUI configuration -3. **WebApp**: Flask-based web application with WebSocket support -4. **BrowserAIGUI**: Tkinter desktop application -5. **TaskManager**: Manages Browser.AI agent lifecycle +1. **EventAdapter**: Captures Browser.AI logs without modifying the library (legacy) +2. **Structured Event System**: New SOLID-compliant event emission system +3. **ConfigManager**: Handles LLM, browser, and GUI configuration +4. **WebApp**: Flask-based web application with WebSocket support +5. **BrowserAIGUI**: Tkinter desktop application +6. **TaskManager**: Manages Browser.AI agent lifecycle ### Event System -The event system provides non-intrusive log streaming: +#### New Structured Event System (Recommended) + +The new structured event system provides a decoupled, SOLID-compliant architecture for emitting and consuming events: + +```python +from browser_ai_gui.events import ( + EventEmitter, + EventTransport, + AgentStartEvent, + EventCategory +) +from browser_ai_gui.events.bridge import EventBridge + +# Create event system +emitter = EventEmitter() +transport = EventTransport(socketio=socketio, namespace="/extension") +bridge = EventBridge(emitter, transport) + +# Subscribe to events +def handle_event(event): + print(f"Event: {event.event_type}") + +emitter.subscribe(handle_event) + +# Emit structured events +event = bridge.create_agent_start_event( + task_description="Book a ticket", + agent_id="agent-123", + configuration={"max_steps": 50} +) +bridge.emit_structured_event(event) +``` + +**Features:** +- Type-safe event schemas +- Event filtering by type and category +- Multiple transport support (WebSocket, callbacks) +- Session and task ID tracking +- Progress and LLM output events + +**See:** [Structured Events Documentation](../docs/STRUCTURED_EVENTS.md) + +#### Legacy Event Adapter + +The original event adapter is still supported for backward compatibility: ```python from browser_ai_gui import EventAdapter, LogEvent @@ -155,10 +200,21 @@ adapter.start() ```python from browser_ai_gui import ConfigManager, TaskManager, EventAdapter +from browser_ai_gui.events import EventEmitter, EventTransport +from browser_ai_gui.events.bridge import EventBridge # Setup components config = ConfigManager() adapter = EventAdapter() + +# Setup structured events +emitter = EventEmitter() +transport = EventTransport() +bridge = EventBridge(emitter, transport) + +# Subscribe to structured events +emitter.subscribe(lambda event: print(f"Event: {event.event_type}")) + task_manager = TaskManager(config, adapter) # Start a task programmatically @@ -172,12 +228,19 @@ task_manager.start_task("Your task description here") ``` browser_ai_gui/ ├── __init__.py # Package initialization -├── event_adapter.py # Log capture and event streaming +├── event_adapter.py # Legacy log capture and event streaming +├── events/ # New structured event system +│ ├── __init__.py # Public event system API +│ ├── schemas.py # Event data structures +│ ├── emitter.py # Event emission (pub-sub) +│ ├── transport.py # Event transport layer +│ └── bridge.py # Integration bridge ├── config.py # Configuration management +├── protocol.py # WebSocket protocol definitions +├── websocket_server.py # Extension WebSocket server ├── web_app.py # Flask web application ├── tkinter_gui.py # Desktop GUI application ├── main.py # Main entry points -├── requirements.txt # Additional dependencies └── templates/ └── index.html # Web interface template ``` diff --git a/structured_events_demo.py b/structured_events_demo.py new file mode 100644 index 0000000..5ed305f --- /dev/null +++ b/structured_events_demo.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +""" +Example: Using the Browser.AI Structured Event System + +This example demonstrates how to use the structured event system +to track agent execution, progress, and results. +""" + +import json +from browser_ai_gui.events import ( + EventEmitter, + EventTransport, + EventCategory, + EventSeverity, + AgentActionEvent, + AgentProgressEvent, + LLMOutputEvent, + create_event_id, + create_timestamp, +) +from browser_ai_gui.events.bridge import EventBridge + + +def main(): + """Demonstrate structured event system usage""" + + print("=" * 60) + print("Browser.AI Structured Event System Demo") + print("=" * 60) + print() + + # 1. Create event emitter and transport + print("1. Setting up event system...") + emitter = EventEmitter() + transport = EventTransport() # In-memory transport with callbacks + bridge = EventBridge(emitter, transport) + transport.connect() + print(" ✓ Event system initialized") + print() + + # 2. Subscribe to all events + print("2. Subscribing to events...") + all_events = [] + + def log_all_events(event): + all_events.append(event) + print(f" 📡 Event: {event.event_type}") + print(f" Category: {event.category.value}") + print(f" Severity: {event.severity.value}") + if hasattr(event, 'agent_id'): + print(f" Agent ID: {event.agent_id}") + print() + + emitter.subscribe(log_all_events) + print(" ✓ Subscribed to all events") + print() + + # 3. Subscribe to specific event types + print("3. Setting up filtered subscriptions...") + error_events = [] + + def log_errors(event): + error_events.append(event) + print(f" ❌ ERROR: {event.error_message}") + print(f" Type: {event.error_type}") + print(f" Recoverable: {event.recoverable}") + print() + + emitter.subscribe(log_errors, event_filter="agent.error") + print(" ✓ Subscribed to error events") + print() + + # 4. Subscribe to category + print("4. Subscribing to AGENT category...") + agent_events = [] + emitter.subscribe_category(lambda e: agent_events.append(e), EventCategory.AGENT) + print(" ✓ Subscribed to AGENT category events") + print() + + # 5. Simulate agent lifecycle with events + print("5. Simulating agent task execution...") + print() + + # Agent starts + print(" 🚀 Starting agent task...") + event = bridge.create_agent_start_event( + task_description="Book a movie ticket for 'Inception' in Colombo tomorrow", + agent_id="agent-demo-001", + session_id="session-abc123", + task_id="task-xyz789", + configuration={ + "use_vision": True, + "max_steps": 50, + "browser": "chromium" + } + ) + bridge.emit_structured_event(event) + + # Agent performs actions + print(" 🔍 Agent performing actions...") + + action_event = AgentActionEvent( + event_id=create_event_id(), + event_type="agent.action", + category=EventCategory.AGENT, + timestamp=create_timestamp(), + session_id="session-abc123", + task_id="task-xyz789", + agent_id="agent-demo-001", + action_type="navigate", + action_description="Navigate to movie booking website", + action_parameters={"url": "https://example-cinema.com"}, + action_result="success" + ) + emitter.emit(action_event) + + # Progress update + print(" 📊 Reporting progress...") + + progress_event = AgentProgressEvent( + event_id=create_event_id(), + event_type="agent.progress", + category=EventCategory.PROGRESS, + timestamp=create_timestamp(), + session_id="session-abc123", + task_id="task-xyz789", + agent_id="agent-demo-001", + progress_percentage=45.5, + current_step=5, + total_steps=11, + status_message="Filling booking form" + ) + emitter.emit(progress_event) + + # LLM interaction + print(" 🤖 LLM processing...") + + llm_event = LLMOutputEvent( + event_id=create_event_id(), + event_type="llm.output", + category=EventCategory.LLM, + timestamp=create_timestamp(), + session_id="session-abc123", + task_id="task-xyz789", + agent_id="agent-demo-001", + llm_provider="openai", + model_name="gpt-4o", + prompt_tokens=1500, + completion_tokens=300, + total_tokens=1800, + response_preview="I will click on the 'Book Now' button...", + latency_ms=850.5 + ) + emitter.emit(llm_event) + + # Simulating an error (recoverable) + print(" ⚠️ Encountering a recoverable error...") + error_event = bridge.create_agent_error_event( + agent_id="agent-demo-001", + error_type="ElementNotFoundError", + error_message="Could not find the 'Confirm' button", + session_id="session-abc123", + task_id="task-xyz789", + error_details="Selector: button.confirm, Timeout: 5000ms", + recoverable=True + ) + bridge.emit_structured_event(error_event) + + # Agent completes successfully + print(" ✅ Task completed...") + complete_event = bridge.create_agent_complete_event( + agent_id="agent-demo-001", + success=True, + result="Movie ticket booked successfully. Booking ID: ABC123", + session_id="session-abc123", + task_id="task-xyz789", + execution_time_ms=15000.0, + steps_executed=11 + ) + bridge.emit_structured_event(complete_event) + + print() + + # 6. Display statistics + print("=" * 60) + print("Event Statistics") + print("=" * 60) + print(f"Total events emitted: {len(all_events)}") + print(f"Agent category events: {len(agent_events)}") + print(f"Error events: {len(error_events)}") + print() + + # 7. Show event details + print("=" * 60) + print("Event Details (First Event)") + print("=" * 60) + if all_events: + first_event = all_events[0] + event_dict = first_event.to_dict() + print(json.dumps(event_dict, indent=2)) + print() + + # 8. Demonstrate filtering + print("=" * 60) + print("Filtered Events") + print("=" * 60) + print(f"\nError events captured: {len(error_events)}") + for err in error_events: + print(f" - {err.error_type}: {err.error_message}") + print() + + # 9. Cleanup + print("=" * 60) + print("Cleanup") + print("=" * 60) + emitter.clear_all_subscriptions() + transport.disconnect() + print("✓ Event system cleaned up") + print() + + print("=" * 60) + print("Demo Complete!") + print("=" * 60) + + +if __name__ == "__main__": + main() From f6c003fdc4014ea80d521a47b989ef3d3ceda0e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 08:41:39 +0000 Subject: [PATCH 4/6] Add TypeScript definitions and frontend integration guide Co-authored-by: Sathursan-S <84266926+Sathursan-S@users.noreply.github.com> --- .../browse_ai/src/types/protocol.ts | 2 + .../browse_ai/src/types/structured-events.ts | 313 +++++++++++ docs/FRONTEND_INTEGRATION.md | 519 ++++++++++++++++++ 3 files changed, 834 insertions(+) create mode 100644 browser_ai_extension/browse_ai/src/types/structured-events.ts create mode 100644 docs/FRONTEND_INTEGRATION.md diff --git a/browser_ai_extension/browse_ai/src/types/protocol.ts b/browser_ai_extension/browse_ai/src/types/protocol.ts index 305f111..1ae0d2f 100644 --- a/browser_ai_extension/browse_ai/src/types/protocol.ts +++ b/browser_ai_extension/browse_ai/src/types/protocol.ts @@ -206,7 +206,9 @@ export interface ServerToClientEvents { log_event: (event: LogEvent) => void task_started: (data: TaskStartedPayload) => void task_action_result: (result: ActionResult) => void + task_result: (result: { task: string; success: boolean; history?: string; agent_id?: string }) => void error: (error: ErrorPayload) => void + structured_event: (event: any) => void // New structured event system connect: () => void disconnect: () => void } diff --git a/browser_ai_extension/browse_ai/src/types/structured-events.ts b/browser_ai_extension/browse_ai/src/types/structured-events.ts new file mode 100644 index 0000000..33d321d --- /dev/null +++ b/browser_ai_extension/browse_ai/src/types/structured-events.ts @@ -0,0 +1,313 @@ +/** + * Structured Event System - TypeScript Definitions + * + * Type definitions for the new structured event system. + * These match the Python event schemas in browser_ai_gui/events/schemas.py + */ + +// ============================================================================ +// Enums +// ============================================================================ + +export enum EventCategory { + AGENT = 'agent', + TASK = 'task', + LLM = 'llm', + BROWSER = 'browser', + PROGRESS = 'progress', + SYSTEM = 'system', +} + +export enum EventSeverity { + DEBUG = 'debug', + INFO = 'info', + WARNING = 'warning', + ERROR = 'error', + CRITICAL = 'critical', +} + +// ============================================================================ +// Base Event +// ============================================================================ + +export interface BaseEvent { + event_id: string + event_type: string + category: EventCategory + timestamp: string // ISO 8601 format + severity: EventSeverity + session_id?: string + task_id?: string + metadata?: Record +} + +// ============================================================================ +// Agent Events +// ============================================================================ + +export interface AgentStartEvent extends BaseEvent { + event_type: 'agent.start' + category: EventCategory.AGENT + task_description: string + agent_id: string + configuration: Record +} + +export interface AgentStepEvent extends BaseEvent { + event_type: 'agent.step' + category: EventCategory.AGENT + step_number: number + step_description: string + agent_id: string + total_steps?: number +} + +export interface AgentActionEvent extends BaseEvent { + event_type: 'agent.action' + category: EventCategory.AGENT + action_type: string + action_description: string + agent_id: string + action_parameters: Record + action_result?: string +} + +export interface AgentProgressEvent extends BaseEvent { + event_type: 'agent.progress' + category: EventCategory.PROGRESS + agent_id: string + progress_percentage: number // 0.0 to 100.0 + current_step: number + total_steps?: number + status_message?: string +} + +export interface AgentStateEvent extends BaseEvent { + event_type: 'agent.state_change' + category: EventCategory.AGENT + agent_id: string + old_state: string + new_state: string + state_data: Record +} + +export interface AgentCompleteEvent extends BaseEvent { + event_type: 'agent.complete' + category: EventCategory.AGENT + agent_id: string + success: boolean + result?: string + execution_time_ms?: number + steps_executed?: number +} + +export interface AgentErrorEvent extends BaseEvent { + event_type: 'agent.error' + category: EventCategory.AGENT + severity: EventSeverity.ERROR + agent_id: string + error_type: string + error_message: string + error_details?: string + recoverable: boolean +} + +// ============================================================================ +// LLM Events +// ============================================================================ + +export interface LLMOutputEvent extends BaseEvent { + event_type: 'llm.output' + category: EventCategory.LLM + agent_id: string + llm_provider: string + prompt_tokens?: number + completion_tokens?: number + total_tokens?: number + model_name?: string + response_preview?: string // First 200 chars + latency_ms?: number +} + +// ============================================================================ +// Task Events +// ============================================================================ + +export interface TaskStateChangeEvent extends BaseEvent { + event_type: 'task.state_change' + category: EventCategory.TASK + task_description: string + old_state: string + new_state: string + agent_id?: string +} + +// ============================================================================ +// Union Type for All Events +// ============================================================================ + +export type StructuredEvent = + | AgentStartEvent + | AgentStepEvent + | AgentActionEvent + | AgentProgressEvent + | AgentStateEvent + | AgentCompleteEvent + | AgentErrorEvent + | LLMOutputEvent + | TaskStateChangeEvent + +// ============================================================================ +// Event Handlers +// ============================================================================ + +export type EventHandler = (event: T) => void + +export interface EventSubscription { + id: string + handler: EventHandler + filter?: string // Event type filter + categoryFilter?: EventCategory // Category filter +} + +// ============================================================================ +// Type Guards +// ============================================================================ + +export function isStructuredEvent(obj: any): obj is StructuredEvent { + return ( + obj && + typeof obj.event_id === 'string' && + typeof obj.event_type === 'string' && + typeof obj.category === 'string' && + typeof obj.timestamp === 'string' && + typeof obj.severity === 'string' + ) +} + +export function isAgentStartEvent(event: StructuredEvent): event is AgentStartEvent { + return event.event_type === 'agent.start' +} + +export function isAgentStepEvent(event: StructuredEvent): event is AgentStepEvent { + return event.event_type === 'agent.step' +} + +export function isAgentActionEvent(event: StructuredEvent): event is AgentActionEvent { + return event.event_type === 'agent.action' +} + +export function isAgentProgressEvent(event: StructuredEvent): event is AgentProgressEvent { + return event.event_type === 'agent.progress' +} + +export function isAgentCompleteEvent(event: StructuredEvent): event is AgentCompleteEvent { + return event.event_type === 'agent.complete' +} + +export function isAgentErrorEvent(event: StructuredEvent): event is AgentErrorEvent { + return event.event_type === 'agent.error' +} + +export function isLLMOutputEvent(event: StructuredEvent): event is LLMOutputEvent { + return event.event_type === 'llm.output' +} + +export function isTaskStateChangeEvent(event: StructuredEvent): event is TaskStateChangeEvent { + return event.event_type === 'task.state_change' +} + +// ============================================================================ +// Event Filtering Helpers +// ============================================================================ + +export function filterByCategory(events: StructuredEvent[], category: EventCategory): StructuredEvent[] { + return events.filter(event => event.category === category) +} + +export function filterByType(events: StructuredEvent[], eventType: string): StructuredEvent[] { + return events.filter(event => event.event_type === eventType) +} + +export function filterBySeverity(events: StructuredEvent[], severity: EventSeverity): StructuredEvent[] { + return events.filter(event => event.severity === severity) +} + +export function filterByAgentId(events: StructuredEvent[], agentId: string): StructuredEvent[] { + return events.filter(event => { + return 'agent_id' in event && event.agent_id === agentId + }) +} + +// ============================================================================ +// Event Formatting Helpers +// ============================================================================ + +export function formatEventTimestamp(event: StructuredEvent): string { + return new Date(event.timestamp).toLocaleString() +} + +export function getEventIcon(event: StructuredEvent): string { + if (isAgentStartEvent(event)) return '🚀' + if (isAgentStepEvent(event)) return '📍' + if (isAgentActionEvent(event)) return '🔧' + if (isAgentProgressEvent(event)) return '📊' + if (isAgentCompleteEvent(event)) return event.success ? '✅' : '⚠️' + if (isAgentErrorEvent(event)) return '❌' + if (isLLMOutputEvent(event)) return '🤖' + if (isTaskStateChangeEvent(event)) return '🔄' + return '📡' +} + +export function getSeverityColor(severity: EventSeverity): string { + switch (severity) { + case EventSeverity.DEBUG: + return '#888888' + case EventSeverity.INFO: + return '#0066cc' + case EventSeverity.WARNING: + return '#ff9900' + case EventSeverity.ERROR: + return '#cc0000' + case EventSeverity.CRITICAL: + return '#990000' + default: + return '#000000' + } +} + +// ============================================================================ +// Example Usage +// ============================================================================ + +/* +// Subscribe to structured events +socket.on('structured_event', (event: StructuredEvent) => { + console.log(`Event: ${event.event_type}`) + + if (isAgentProgressEvent(event)) { + updateProgressBar(event.progress_percentage) + } + + if (isAgentCompleteEvent(event)) { + if (event.success) { + showNotification('Task completed successfully!') + } + } + + if (isAgentErrorEvent(event)) { + console.error(`Error: ${event.error_message}`) + } +}) + +// Filter events by category +const agentEvents = filterByCategory(allEvents, EventCategory.AGENT) + +// Filter events by agent ID +const myAgentEvents = filterByAgentId(allEvents, 'agent-123') + +// Display with icons +events.forEach(event => { + console.log(`${getEventIcon(event)} ${event.event_type}`) +}) +*/ diff --git a/docs/FRONTEND_INTEGRATION.md b/docs/FRONTEND_INTEGRATION.md new file mode 100644 index 0000000..2c3be89 --- /dev/null +++ b/docs/FRONTEND_INTEGRATION.md @@ -0,0 +1,519 @@ +# Frontend Integration Guide - Structured Events + +This guide shows how to integrate the structured event system into the Browser.AI extension frontend. + +## Overview + +The new structured event system provides rich, type-safe events that replace log parsing with proper event handling. + +## TypeScript Definitions + +Import the structured event types: + +```typescript +import { + StructuredEvent, + AgentProgressEvent, + AgentCompleteEvent, + AgentErrorEvent, + isAgentProgressEvent, + isAgentCompleteEvent, + isAgentErrorEvent, + getEventIcon, + getSeverityColor +} from '@/types/structured-events' +``` + +## WebSocket Event Handling + +### Basic Setup + +```typescript +import { io } from 'socket.io-client' +import { StructuredEvent } from '@/types/structured-events' + +// Connect to server +const socket = io('http://localhost:5000/extension') + +// Subscribe to structured events +socket.on('structured_event', (event: StructuredEvent) => { + handleStructuredEvent(event) +}) +``` + +### Event Handlers + +```typescript +function handleStructuredEvent(event: StructuredEvent) { + // Log for debugging + console.log(`Event: ${event.event_type}`, event) + + // Handle specific event types + if (isAgentProgressEvent(event)) { + updateProgressUI(event) + } else if (isAgentCompleteEvent(event)) { + handleTaskCompletion(event) + } else if (isAgentErrorEvent(event)) { + handleError(event) + } +} +``` + +## UI Components + +### Progress Display + +```typescript +function updateProgressUI(event: AgentProgressEvent) { + // Update progress bar + const progressBar = document.getElementById('progress-bar') + if (progressBar) { + progressBar.style.width = `${event.progress_percentage}%` + progressBar.setAttribute('aria-valuenow', event.progress_percentage.toString()) + } + + // Update status message + const statusText = document.getElementById('status-text') + if (statusText && event.status_message) { + statusText.textContent = event.status_message + } + + // Update step counter + const stepCounter = document.getElementById('step-counter') + if (stepCounter && event.total_steps) { + stepCounter.textContent = `Step ${event.current_step} of ${event.total_steps}` + } +} +``` + +### Task Completion + +```typescript +function handleTaskCompletion(event: AgentCompleteEvent) { + if (event.success) { + // Show success notification + showNotification({ + type: 'success', + title: 'Task Completed', + message: event.result || 'Task completed successfully', + duration: 5000 + }) + + // Update UI + updateTaskStatus('completed') + + // Show execution stats + if (event.execution_time_ms && event.steps_executed) { + const stats = `Completed in ${(event.execution_time_ms / 1000).toFixed(1)}s (${event.steps_executed} steps)` + displayStats(stats) + } + } else { + // Show warning + showNotification({ + type: 'warning', + title: 'Task Incomplete', + message: 'Task ended without full completion', + duration: 5000 + }) + } +} +``` + +### Error Handling + +```typescript +function handleError(event: AgentErrorEvent) { + // Show error notification + showNotification({ + type: 'error', + title: event.error_type, + message: event.error_message, + duration: 0 // Don't auto-dismiss errors + }) + + // Log details for debugging + if (event.error_details) { + console.error('Error details:', event.error_details) + } + + // If recoverable, show retry option + if (event.recoverable) { + showRetryButton() + } +} +``` + +## React Integration + +### Custom Hook + +```typescript +import { useEffect, useState } from 'react' +import { StructuredEvent } from '@/types/structured-events' + +export function useStructuredEvents(socket: any) { + const [events, setEvents] = useState([]) + + useEffect(() => { + if (!socket) return + + const handleEvent = (event: StructuredEvent) => { + setEvents(prev => [...prev, event]) + } + + socket.on('structured_event', handleEvent) + + return () => { + socket.off('structured_event', handleEvent) + } + }, [socket]) + + return events +} +``` + +### Progress Component + +```typescript +import React from 'react' +import { AgentProgressEvent } from '@/types/structured-events' + +interface ProgressProps { + event: AgentProgressEvent | null +} + +export function Progress({ event }: ProgressProps) { + if (!event) return null + + return ( +
+
+
+
+
+ + {event.progress_percentage.toFixed(1)}% + + {event.status_message && ( + + {event.status_message} + + )} + {event.total_steps && ( + + Step {event.current_step} / {event.total_steps} + + )} +
+
+ ) +} +``` + +### Event Feed Component + +```typescript +import React from 'react' +import { StructuredEvent, getEventIcon, getSeverityColor } from '@/types/structured-events' + +interface EventFeedProps { + events: StructuredEvent[] +} + +export function EventFeed({ events }: EventFeedProps) { + return ( +
+ {events.map(event => ( +
+ {getEventIcon(event)} +
+
{event.event_type}
+
+ {new Date(event.timestamp).toLocaleTimeString()} +
+
+
+ ))} +
+ ) +} +``` + +## Event Filtering + +### Filter by Category + +```typescript +import { EventCategory, filterByCategory } from '@/types/structured-events' + +// Get only agent events +const agentEvents = filterByCategory(allEvents, EventCategory.AGENT) + +// Get only progress events +const progressEvents = filterByCategory(allEvents, EventCategory.PROGRESS) +``` + +### Filter by Agent ID + +```typescript +import { filterByAgentId } from '@/types/structured-events' + +// Get events for specific agent +const myAgentEvents = filterByAgentId(allEvents, currentAgentId) +``` + +### Filter by Severity + +```typescript +import { EventSeverity, filterBySeverity } from '@/types/structured-events' + +// Get only errors +const errors = filterBySeverity(allEvents, EventSeverity.ERROR) + +// Get warnings and errors +const issues = allEvents.filter(e => + e.severity === EventSeverity.WARNING || + e.severity === EventSeverity.ERROR +) +``` + +## State Management + +### Redux/Zustand Integration + +```typescript +// Zustand store example +import create from 'zustand' +import { StructuredEvent, AgentProgressEvent } from '@/types/structured-events' + +interface EventStore { + events: StructuredEvent[] + currentProgress: AgentProgressEvent | null + addEvent: (event: StructuredEvent) => void + clearEvents: () => void +} + +export const useEventStore = create((set) => ({ + events: [], + currentProgress: null, + + addEvent: (event) => set((state) => { + const newEvents = [...state.events, event] + + // Update current progress if it's a progress event + const currentProgress = event.event_type === 'agent.progress' + ? event as AgentProgressEvent + : state.currentProgress + + return { events: newEvents, currentProgress } + }), + + clearEvents: () => set({ events: [], currentProgress: null }) +})) +``` + +## Best Practices + +### 1. Type Guards + +Always use type guards to safely access event-specific fields: + +```typescript +// ❌ Bad - might cause runtime errors +const progress = event.progress_percentage + +// ✅ Good - type-safe +if (isAgentProgressEvent(event)) { + const progress = event.progress_percentage +} +``` + +### 2. Event Batching + +Batch UI updates to avoid performance issues: + +```typescript +const [eventBatch, setEventBatch] = useState([]) + +useEffect(() => { + const timer = setInterval(() => { + if (eventBatch.length > 0) { + processEvents(eventBatch) + setEventBatch([]) + } + }, 100) // Process every 100ms + + return () => clearInterval(timer) +}, [eventBatch]) +``` + +### 3. Event Cleanup + +Limit stored events to prevent memory issues: + +```typescript +const MAX_EVENTS = 1000 + +function addEvent(event: StructuredEvent) { + setEvents(prev => { + const newEvents = [...prev, event] + // Keep only last MAX_EVENTS + return newEvents.slice(-MAX_EVENTS) + }) +} +``` + +### 4. Error Boundaries + +Wrap event handlers in error boundaries: + +```typescript +function safeHandleEvent(event: StructuredEvent) { + try { + handleStructuredEvent(event) + } catch (error) { + console.error('Error handling event:', error, event) + // Fallback UI or notification + } +} +``` + +## Migration from Log Events + +### Before (Log Parsing) + +```typescript +socket.on('log_event', (log) => { + if (log.message.includes('Step')) { + // Parse step number from message + const match = log.message.match(/Step (\d+)/) + if (match) { + updateStep(parseInt(match[1])) + } + } +}) +``` + +### After (Structured Events) + +```typescript +socket.on('structured_event', (event) => { + if (isAgentStepEvent(event)) { + updateStep(event.step_number) + } +}) +``` + +## Complete Example + +```typescript +import { useEffect, useState } from 'react' +import { io } from 'socket.io-client' +import { + StructuredEvent, + AgentProgressEvent, + isAgentStartEvent, + isAgentProgressEvent, + isAgentCompleteEvent, + isAgentErrorEvent, + getEventIcon +} from '@/types/structured-events' + +export function TaskMonitor() { + const [socket, setSocket] = useState(null) + const [events, setEvents] = useState([]) + const [progress, setProgress] = useState(null) + const [status, setStatus] = useState<'idle' | 'running' | 'complete' | 'error'>('idle') + + useEffect(() => { + const socket = io('http://localhost:5000/extension') + + socket.on('structured_event', (event: StructuredEvent) => { + // Add to event list + setEvents(prev => [...prev, event]) + + // Update status based on event type + if (isAgentStartEvent(event)) { + setStatus('running') + } else if (isAgentProgressEvent(event)) { + setProgress(event) + } else if (isAgentCompleteEvent(event)) { + setStatus(event.success ? 'complete' : 'error') + } else if (isAgentErrorEvent(event)) { + setStatus('error') + } + }) + + setSocket(socket) + + return () => { + socket.close() + } + }, []) + + return ( +
+
+ Status: {status} +
+ + {progress && ( +
+
+
+ {progress.status_message || `${progress.progress_percentage.toFixed(0)}%`} +
+
+ )} + +
+ {events.map(event => ( +
+ {getEventIcon(event)} + {event.event_type} + {new Date(event.timestamp).toLocaleTimeString()} +
+ ))} +
+
+ ) +} +``` + +## Testing + +```typescript +import { describe, it, expect } from 'vitest' +import { isAgentProgressEvent, AgentProgressEvent, EventCategory, EventSeverity } from '@/types/structured-events' + +describe('Structured Events', () => { + it('should identify progress events', () => { + const event: AgentProgressEvent = { + event_id: 'test-123', + event_type: 'agent.progress', + category: EventCategory.PROGRESS, + timestamp: new Date().toISOString(), + severity: EventSeverity.INFO, + agent_id: 'agent-1', + progress_percentage: 50, + current_step: 5, + total_steps: 10 + } + + expect(isAgentProgressEvent(event)).toBe(true) + }) +}) +``` + +## Resources + +- [Structured Events Documentation](../docs/STRUCTURED_EVENTS.md) +- [Python Event Schemas](../browser_ai_gui/events/schemas.py) +- [Demo](../structured_events_demo.py) From 909328d44e63b659c89d1e8f75a2c55b17271dcc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 08:42:58 +0000 Subject: [PATCH 5/6] Add implementation summary and finalize structured event system Co-authored-by: Sathursan-S <84266926+Sathursan-S@users.noreply.github.com> --- docs/IMPLEMENTATION_SUMMARY.md | 341 +++++++++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 docs/IMPLEMENTATION_SUMMARY.md diff --git a/docs/IMPLEMENTATION_SUMMARY.md b/docs/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..6de1daf --- /dev/null +++ b/docs/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,341 @@ +# Structured Event System - Implementation Summary + +## Overview + +Successfully implemented a decoupled, SOLID-compliant structured event emission system for Browser.AI that replaces log streaming with properly structured events. + +## Problem Solved + +**Before:** +- Log-based event streaming using logging.Handler +- String parsing required in frontend to extract information +- No type safety or structure +- Difficult to filter and process events +- Limited metadata + +**After:** +- Structured event emission following SOLID principles +- Type-safe events with rich metadata +- Easy filtering by type, category, severity +- Complete session and task tracking +- Progress and performance metrics included + +## Architecture + +### Backend (Python) + +``` +browser_ai_gui/events/ +├── __init__.py # Public API +├── schemas.py # Event data structures (9 event types) +├── emitter.py # Event emission with pub-sub pattern +├── transport.py # Transport abstraction (WebSocket, callbacks) +└── bridge.py # Integration bridge with backward compatibility +``` + +**Key Components:** +- `IEventEmitter`: Interface for event emission +- `EventEmitter`: Default pub-sub implementation +- `IEventTransport`: Interface for event transport +- `EventTransport`: WebSocket and callback transport +- `EventBridge`: Helper for creating and emitting events + +### Frontend (TypeScript) + +``` +browser_ai_extension/browse_ai/src/types/ +├── protocol.ts # WebSocket protocol (updated) +└── structured-events.ts # Event type definitions and helpers +``` + +**Features:** +- Complete TypeScript type definitions +- Type guards for safe event handling +- Helper functions for filtering and formatting +- Event icons and severity colors + +## Event Types + +### Agent Events +- `agent.start` - Task initiation with configuration +- `agent.step` - Step execution with step number +- `agent.action` - Action performed with parameters and result +- `agent.progress` - Progress update with percentage and steps +- `agent.state_change` - State transitions +- `agent.complete` - Task completion with result and timing +- `agent.error` - Error with type, message, and recovery info + +### LLM Events +- `llm.output` - LLM interaction with token counts and latency + +### Task Events +- `task.state_change` - Task state transitions + +## Event Schema + +All events include: +```python +{ + "event_id": str, # Unique identifier + "event_type": str, # Event type (e.g., "agent.start") + "category": str, # Category (agent/task/llm/progress/system) + "timestamp": str, # ISO 8601 timestamp + "severity": str, # debug/info/warning/error/critical + "session_id": str, # Session tracking + "task_id": str, # Task tracking + "metadata": dict, # Additional data + # ... type-specific fields +} +``` + +## Usage Examples + +### Backend (Python) + +```python +from browser_ai_gui.events import EventEmitter, EventTransport +from browser_ai_gui.events.bridge import EventBridge + +# Setup +emitter = EventEmitter() +transport = EventTransport(socketio=socketio, namespace="/extension") +bridge = EventBridge(emitter, transport) + +# Create and emit events +event = bridge.create_agent_start_event( + task_description="Book a movie ticket", + agent_id="agent-123", + session_id="session-456", + configuration={"max_steps": 50} +) +bridge.emit_structured_event(event) + +# Subscribe to events +def handler(event): + print(f"Event: {event.event_type}") + +emitter.subscribe(handler) +emitter.subscribe(handler, event_filter="agent.error") # Filter by type +emitter.subscribe_category(handler, EventCategory.AGENT) # Filter by category +``` + +### Frontend (TypeScript) + +```typescript +import { StructuredEvent, isAgentProgressEvent } from '@/types/structured-events' + +// Subscribe to events +socket.on('structured_event', (event: StructuredEvent) => { + if (isAgentProgressEvent(event)) { + updateProgressBar(event.progress_percentage) + updateStepCounter(event.current_step, event.total_steps) + } +}) +``` + +## SOLID Principles Applied + +1. **Single Responsibility Principle (SRP)** + - `schemas.py`: Only event data structures + - `emitter.py`: Only event emission logic + - `transport.py`: Only transport mechanisms + - `bridge.py`: Only integration helpers + +2. **Open/Closed Principle (OCP)** + - Extensible through new event types + - Closed for modification of existing events + - New transports can be added without changing emitter + +3. **Liskov Substitution Principle (LSP)** + - All events inherit from `BaseEvent` + - All emitters implement `IEventEmitter` + - All transports implement `IEventTransport` + +4. **Interface Segregation Principle (ISP)** + - Clean, focused interfaces + - `IEventEmitter` for emission + - `IEventTransport` for transport + +5. **Dependency Inversion Principle (DIP)** + - Components depend on abstractions (interfaces) + - Concrete implementations injected + - Bridge uses interfaces, not concrete classes + +## Integration Points + +### WebSocket Server + +Updated `browser_ai_gui/websocket_server.py`: +- Added structured event system initialization +- Emits events on agent lifecycle +- Backward compatible with old event_adapter + +```python +# In ExtensionTaskManager.__init__ +self.event_emitter = EventEmitter() +self.event_transport = EventTransport(socketio=socketio, namespace="/extension") +self.event_bridge = EventBridge(self.event_emitter, self.event_transport) +``` + +### Task Manager + +Events emitted during task execution: +- Start: When agent begins task +- Progress: During execution (could be added) +- Complete: When task finishes with success/failure +- Error: When errors occur + +## Testing + +### Unit Tests + +`tests/test_structured_events.py`: +- Event emission and reception +- Event filtering (by type and category) +- Subscription management +- Transport functionality +- Bridge helpers +- Event serialization + +### Demo + +`structured_events_demo.py`: +- Full lifecycle simulation +- Event filtering examples +- Statistics and formatting +- All features demonstrated + +Run: `python structured_events_demo.py` + +## Documentation + +### Main Documentation +- `docs/STRUCTURED_EVENTS.md` - Complete backend guide +- `docs/FRONTEND_INTEGRATION.md` - Frontend integration guide +- `browser_ai_gui/README.md` - Updated with event system info + +### Examples +- Event creation and emission +- Filtering and subscription +- React component integration +- State management +- Migration from log parsing + +## Migration Guide + +### For Backend Code + +**Old (still works):** +```python +event_adapter.emit_custom_event( + EventType.AGENT_START, + "Starting task", + LogLevel.INFO +) +``` + +**New (recommended):** +```python +event = bridge.create_agent_start_event( + task_description="Starting task", + agent_id="agent-123" +) +bridge.emit_structured_event(event) +``` + +### For Frontend Code + +**Old (log parsing):** +```typescript +socket.on('log_event', (log) => { + if (log.message.includes('Step')) { + const match = log.message.match(/Step (\d+)/) + // ... + } +}) +``` + +**New (structured):** +```typescript +socket.on('structured_event', (event) => { + if (isAgentStepEvent(event)) { + updateStep(event.step_number) + } +}) +``` + +## Benefits Achieved + +✅ **Type Safety**: Full type checking in both Python and TypeScript +✅ **Rich Metadata**: Session ID, task ID, agent ID, timestamps +✅ **Easy Filtering**: Filter by type, category, severity, agent +✅ **Progress Tracking**: Real-time progress percentage and step counts +✅ **Error Details**: Structured errors with recovery hints +✅ **Performance Metrics**: Execution time, token usage, latency +✅ **Extensibility**: Easy to add new event types +✅ **Backward Compatible**: Old event_adapter still works +✅ **Testable**: Clean interfaces enable easy testing +✅ **Decoupled**: SOLID principles ensure maintainability + +## Performance Considerations + +- Events are emitted asynchronously +- No blocking operations +- Failed transports don't break emission +- Subscriber errors are caught and ignored +- Event batching can be added if needed + +## Future Enhancements + +Potential additions: +- Event persistence and replay +- Event aggregation and analytics +- Rate limiting and throttling +- Binary serialization for performance +- Event sourcing patterns +- More event types (browser, network, etc.) + +## Files Changed/Added + +### Added +- `browser_ai_gui/events/__init__.py` +- `browser_ai_gui/events/schemas.py` +- `browser_ai_gui/events/emitter.py` +- `browser_ai_gui/events/transport.py` +- `browser_ai_gui/events/bridge.py` +- `browser_ai_extension/browse_ai/src/types/structured-events.ts` +- `docs/STRUCTURED_EVENTS.md` +- `docs/FRONTEND_INTEGRATION.md` +- `tests/test_structured_events.py` +- `structured_events_demo.py` + +### Modified +- `browser_ai_gui/websocket_server.py` +- `browser_ai_gui/README.md` +- `browser_ai_extension/browse_ai/src/types/protocol.ts` + +## Conclusion + +The structured event system successfully replaces log streaming with a robust, type-safe, SOLID-compliant event architecture. It provides rich metadata, easy filtering, and seamless integration with both backend and frontend components while maintaining backward compatibility. + +## Quick Start + +1. **Backend**: Import and use the bridge + ```python + from browser_ai_gui.events.bridge import EventBridge + ``` + +2. **Frontend**: Import TypeScript definitions + ```typescript + import { StructuredEvent } from '@/types/structured-events' + ``` + +3. **Subscribe**: Listen to `structured_event` on WebSocket + +4. **Handle**: Use type guards to handle events safely + +5. **Demo**: Run `python structured_events_demo.py` + +For detailed documentation, see: +- [Structured Events Documentation](docs/STRUCTURED_EVENTS.md) +- [Frontend Integration Guide](docs/FRONTEND_INTEGRATION.md) From 0e57432e338fb1582dcfb5c085164d3ac6930ef1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 08:44:02 +0000 Subject: [PATCH 6/6] Add events module README and finalize documentation Co-authored-by: Sathursan-S <84266926+Sathursan-S@users.noreply.github.com> --- browser_ai_gui/events/README.md | 99 +++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 browser_ai_gui/events/README.md diff --git a/browser_ai_gui/events/README.md b/browser_ai_gui/events/README.md new file mode 100644 index 0000000..41ed875 --- /dev/null +++ b/browser_ai_gui/events/README.md @@ -0,0 +1,99 @@ +# Browser.AI Structured Event System + +A decoupled, SOLID-compliant event emission system for Browser.AI. + +## Overview + +This module provides a structured event system that replaces log-based event streaming with properly typed, rich events for tasks, states, progress, LLM output, and more. + +## Quick Start + +```python +from browser_ai_gui.events import EventEmitter, EventTransport +from browser_ai_gui.events.bridge import EventBridge + +# Setup +emitter = EventEmitter() +transport = EventTransport(socketio=socketio, namespace="/extension") +bridge = EventBridge(emitter, transport) + +# Create and emit events +event = bridge.create_agent_start_event( + task_description="Book a movie ticket", + agent_id="agent-123", + configuration={"max_steps": 50} +) +bridge.emit_structured_event(event) + +# Subscribe to events +emitter.subscribe(lambda e: print(e.event_type)) +``` + +## Event Types + +- **Agent Events:** start, step, action, progress, state_change, complete, error +- **LLM Events:** output (tokens, latency, model info) +- **Task Events:** state_change + +## Features + +✅ **Type-safe** - Full type checking with dataclasses +✅ **Rich metadata** - Session ID, task ID, timestamps +✅ **Event filtering** - By type, category, severity +✅ **Multiple transports** - WebSocket, callbacks +✅ **SOLID design** - Decoupled, extensible, testable +✅ **Backward compatible** - Works with existing event_adapter + +## Architecture + +``` +events/ +├── __init__.py # Public API +├── schemas.py # Event data structures +├── emitter.py # Event emission (pub-sub) +├── transport.py # Transport layer +└── bridge.py # Integration helpers +``` + +## Example Event + +```json +{ + "event_id": "uuid-123", + "event_type": "agent.progress", + "category": "progress", + "timestamp": "2024-01-15T10:30:00Z", + "severity": "info", + "session_id": "session-abc", + "task_id": "task-xyz", + "agent_id": "agent-123", + "progress_percentage": 45.5, + "current_step": 5, + "total_steps": 11, + "status_message": "Filling form fields" +} +``` + +## Documentation + +- [Complete Backend Guide](../../docs/STRUCTURED_EVENTS.md) +- [Frontend Integration](../../docs/FRONTEND_INTEGRATION.md) +- [Implementation Summary](../../docs/IMPLEMENTATION_SUMMARY.md) + +## Demo + +Run the demo: `python ../../structured_events_demo.py` + +## Testing + +```bash +pytest ../../tests/test_structured_events.py -v +``` + +## SOLID Principles + +- **Single Responsibility:** Each module has one purpose +- **Open/Closed:** Extensible via new event types +- **Liskov Substitution:** Interface-based design +- **Interface Segregation:** Focused interfaces +- **Dependency Inversion:** Depends on abstractions