feat: add Pydantic models and EventEmitter for v2 core

- 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>
This commit is contained in:
2026-05-04 00:00:21 +03:00
parent 1064276e81
commit d292d32afa
4 changed files with 960 additions and 0 deletions

129
meeting_room/events.py Normal file
View File

@@ -0,0 +1,129 @@
"""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()