129 lines
4.0 KiB
Python
129 lines
4.0 KiB
Python
"""
|
|
Events - Decoupled event emission and queuing for cross-thread communication.
|
|
|
|
This module provides three complementary patterns for thread-safe communication
|
|
between the GUI main thread and background workers:
|
|
|
|
1. EventEmitter: Pub/sub pattern for synchronous event broadcast
|
|
- Used for: API lifecycle events (request_start, response_received, tool_execution)
|
|
- Thread-safe: Callbacks execute on emitter's thread
|
|
- Example: ai_client.py emits 'request_start' and 'response_received' events
|
|
|
|
2. SyncEventQueue: Producer-consumer pattern via queue.Queue
|
|
- Used for: Decoupled task submission where consumer polls at its own pace
|
|
- Thread-safe: Built on Python's thread-safe queue.Queue
|
|
- Example: Background workers submit tasks, main thread drains queue
|
|
|
|
3. UserRequestEvent: Structured payload for AI request data
|
|
- Used for: Bundling prompt, context, files, and base_dir into single object
|
|
- Immutable data transfer object for cross-thread handoff
|
|
|
|
Integration Points:
|
|
- ai_client.py: EventEmitter for API lifecycle events
|
|
- gui_2.py: Consumes events via _process_event_queue()
|
|
- multi_agent_conductor.py: Uses SyncEventQueue for state updates
|
|
- api_hooks.py: Pushes events to _api_event_queue for external visibility
|
|
|
|
Thread Safety:
|
|
- EventEmitter: NOT thread-safe for concurrent on/emit (use from single thread)
|
|
- SyncEventQueue: FULLY thread-safe (built on queue.Queue)
|
|
- UserRequestEvent: Immutable, safe for concurrent access
|
|
"""
|
|
import queue
|
|
from typing import Callable, Any, Dict, List, Tuple
|
|
|
|
class EventEmitter:
|
|
"""
|
|
Simple event emitter for decoupled communication between modules.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
"""Initializes the EventEmitter with an empty listener map."""
|
|
self._listeners: Dict[str, List[Callable[..., Any]]] = {}
|
|
|
|
def on(self, event_name: str, callback: Callable[..., Any]) -> None:
|
|
"""
|
|
Registers a callback for a specific event.
|
|
|
|
Args:
|
|
event_name: The name of the event to listen for.
|
|
callback: The function to call when the event is emitted.
|
|
"""
|
|
if event_name not in self._listeners:
|
|
self._listeners[event_name] = []
|
|
self._listeners[event_name].append(callback)
|
|
|
|
def emit(self, event_name: str, *args: Any, **kwargs: Any) -> None:
|
|
"""
|
|
Emits an event, calling all registered callbacks.
|
|
|
|
Args:
|
|
event_name: The name of the event to emit.
|
|
*args: Positional arguments to pass to callbacks.
|
|
**kwargs: Keyword arguments to pass to callbacks.
|
|
"""
|
|
if event_name in self._listeners:
|
|
for callback in self._listeners[event_name]:
|
|
callback(*args, **kwargs)
|
|
|
|
def clear(self) -> None:
|
|
"""Clears all registered listeners."""
|
|
self._listeners.clear()
|
|
|
|
class SyncEventQueue:
|
|
"""
|
|
Synchronous event queue for decoupled communication using queue.Queue.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
"""Initializes the SyncEventQueue with an internal queue.Queue."""
|
|
self._queue: queue.Queue[Tuple[str, Any]] = queue.Queue()
|
|
|
|
def put(self, event_name: str, payload: Any = None) -> None:
|
|
"""
|
|
Puts an event into the queue.
|
|
|
|
Args:
|
|
event_name: The name of the event.
|
|
payload: Optional data associated with the event.
|
|
"""
|
|
self._queue.put((event_name, payload))
|
|
|
|
def get(self) -> Tuple[str, Any]:
|
|
"""
|
|
Gets an event from the queue.
|
|
|
|
Returns:
|
|
A tuple containing (event_name, payload).
|
|
"""
|
|
return self._queue.get()
|
|
|
|
def task_done(self) -> None:
|
|
"""Signals that a formerly enqueued task is complete."""
|
|
self._queue.task_done()
|
|
|
|
def join(self) -> None:
|
|
"""Blocks until all items in the queue have been gotten and processed."""
|
|
self._queue.join()
|
|
|
|
class UserRequestEvent:
|
|
"""
|
|
Payload for a user request event.
|
|
"""
|
|
|
|
def __init__(self, prompt: str, stable_md: str, file_items: List[Any], disc_text: str, base_dir: str) -> None:
|
|
self.prompt = prompt
|
|
self.stable_md = stable_md
|
|
self.file_items = file_items
|
|
self.disc_text = disc_text
|
|
self.base_dir = base_dir
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"prompt": self.prompt,
|
|
"stable_md": self.stable_md,
|
|
"file_items": self.file_items,
|
|
"disc_text": self.disc_text,
|
|
"base_dir": self.base_dir
|
|
}
|