- models.py: ProviderConfig, RoleConfig, DiscussionConfig, ScenarioConfig, Message, ToolCallRecord, Session — compatible with config.yaml format - events.py: Event constants, Event dataclass, EventEmitter with on/off/emit - 67 tests passing (37 models + 30 events) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
129 lines
3.9 KiB
Python
129 lines
3.9 KiB
Python
"""EventEmitter and event types for the Meeting Room discussion engine.
|
|
|
|
Synchronous event bus. In Phase 2, an AsyncEventBridge will forward
|
|
events to WebSocket clients.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from typing import Any, Callable
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Event type constants
|
|
# ---------------------------------------------------------------------------
|
|
|
|
DISCUSSION_START = "discussion_start"
|
|
ROUND_START = "round_start"
|
|
AGENT_TURN_START = "agent_turn_start"
|
|
AGENT_MESSAGE = "agent_message"
|
|
TOOL_CALL = "tool_call"
|
|
TOOL_RESULT = "tool_result"
|
|
AGENT_ERROR = "agent_error"
|
|
ROUND_END = "round_end"
|
|
DISCUSSION_END = "discussion_end"
|
|
BOSS_TURN = "boss_turn"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Event dataclass
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class Event:
|
|
"""A single event emitted by the discussion engine."""
|
|
|
|
type: str
|
|
data: dict[str, Any] = field(default_factory=dict)
|
|
timestamp: datetime = field(default_factory=datetime.now)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# EventEmitter
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Type alias for handler callbacks
|
|
Handler = Callable[[Event], None]
|
|
|
|
|
|
class EventEmitter:
|
|
"""Synchronous pub/sub event bus.
|
|
|
|
Usage::
|
|
|
|
bus = EventEmitter()
|
|
|
|
def on_message(event: Event):
|
|
print(event.data)
|
|
|
|
bus.on(AGENT_MESSAGE, on_message)
|
|
bus.emit(AGENT_MESSAGE, text="Hello", agent="Alice")
|
|
bus.off(AGENT_MESSAGE, on_message)
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self._handlers: dict[str, list[Handler]] = {}
|
|
|
|
# -- subscription --------------------------------------------------------
|
|
|
|
def on(self, event_type: str, handler: Handler) -> Handler:
|
|
"""Subscribe *handler* to *event_type*.
|
|
|
|
Returns the handler so callers can use ``off`` with the same
|
|
reference, or chain calls.
|
|
"""
|
|
self._handlers.setdefault(event_type, []).append(handler)
|
|
return handler
|
|
|
|
def off(self, event_type: str, handler: Handler) -> None:
|
|
"""Unsubscribe *handler* from *event_type*.
|
|
|
|
Silently ignores unknown event types or missing handlers.
|
|
"""
|
|
handlers = self._handlers.get(event_type)
|
|
if handlers is None:
|
|
return
|
|
try:
|
|
handlers.remove(handler)
|
|
except ValueError:
|
|
pass
|
|
# Clean up empty lists to avoid unbounded growth
|
|
if not handlers:
|
|
del self._handlers[event_type]
|
|
|
|
# -- emission ------------------------------------------------------------
|
|
|
|
def emit(self, event_type: str, **data: Any) -> Event:
|
|
"""Create an Event and call every subscribed handler synchronously.
|
|
|
|
Handlers that raise exceptions are caught and logged; the emitter
|
|
never crashes due to a bad handler.
|
|
|
|
Returns the Event that was emitted (useful for testing).
|
|
"""
|
|
event = Event(type=event_type, data=data)
|
|
handlers = self._handlers.get(event_type, [])
|
|
for handler in handlers:
|
|
try:
|
|
handler(event)
|
|
except Exception:
|
|
logger.exception(
|
|
"Handler %r raised for event %s",
|
|
handler,
|
|
event_type,
|
|
)
|
|
return event
|
|
|
|
# -- introspection -------------------------------------------------------
|
|
|
|
def handlers(self, event_type: str) -> list[Handler]:
|
|
"""Return a *copy* of the handler list for *event_type*."""
|
|
return list(self._handlers.get(event_type, []))
|
|
|
|
def clear(self) -> None:
|
|
"""Remove all subscriptions."""
|
|
self._handlers.clear() |