""" Trigger handlers - concrete implementations for common trigger types. Provides ready-to-use trigger handlers for: - Agent execution (WebSocket user messages) - Lambda/callable execution - Data update triggers - Indicator updates """ import logging import time from typing import Any, Awaitable, Callable, Optional from .coordinator import CommitCoordinator from .types import CommitIntent, Priority, Trigger logger = logging.getLogger(__name__) class AgentTriggerHandler(Trigger): """ Trigger for agent execution from WebSocket user messages. Wraps the Gateway's agent execution flow and captures any store modifications as commit intents. Priority tuple: (USER_AGENT, message_timestamp, queue_seq) """ def __init__( self, session_id: str, message_content: str, message_timestamp: Optional[int] = None, attachments: Optional[list] = None, gateway_handler: Optional[Callable] = None, coordinator: Optional[CommitCoordinator] = None, ): """ Initialize agent trigger. Args: session_id: User session ID message_content: User message content message_timestamp: When user sent message (unix timestamp, defaults to now) attachments: Optional message attachments gateway_handler: Callable to route to Gateway (set during integration) coordinator: CommitCoordinator for accessing stores """ if message_timestamp is None: message_timestamp = int(time.time()) # Priority tuple: sort by USER_AGENT priority, then message timestamp super().__init__( name=f"agent_{session_id}", priority=Priority.USER_AGENT, priority_tuple=(Priority.USER_AGENT.value, message_timestamp) ) self.session_id = session_id self.message_content = message_content self.message_timestamp = message_timestamp self.attachments = attachments or [] self.gateway_handler = gateway_handler self.coordinator = coordinator async def execute(self) -> list[CommitIntent]: """ Execute agent interaction. This will call into the Gateway, which will run the agent. The agent may read from stores and generate responses. Any store modifications are captured as commit intents. Returns: List of commit intents (typically empty for now, as agent modifies stores via tools which will be integrated later) """ if not self.gateway_handler: logger.error("No gateway_handler configured for AgentTriggerHandler") return [] logger.info( f"Agent trigger executing: session={self.session_id}, " f"content='{self.message_content[:50]}...'" ) try: # Call Gateway to handle message # In future, Gateway/agent tools will use coordinator stores await self.gateway_handler( self.session_id, self.message_content, self.attachments, ) # For now, agent doesn't directly modify stores # Future: agent tools will return commit intents return [] except Exception as e: logger.error(f"Agent execution error: {e}", exc_info=True) raise class LambdaHandler(Trigger): """ Generic trigger that executes an arbitrary async callable. Useful for custom triggers, one-off tasks, or testing. """ def __init__( self, name: str, func: Callable[[], Awaitable[list[CommitIntent]]], priority: Priority = Priority.SYSTEM, ): """ Initialize lambda handler. Args: name: Descriptive name for this trigger func: Async callable that returns commit intents priority: Execution priority """ super().__init__(name, priority) self.func = func async def execute(self) -> list[CommitIntent]: """Execute the callable""" logger.info(f"Lambda trigger executing: {self.name}") return await self.func() class DataUpdateTrigger(Trigger): """ Trigger for DataSource bar updates. Fired when new market data arrives. Can update indicators, trigger strategy logic, or notify the agent of market events. Priority tuple: (DATA_SOURCE, event_time, queue_seq) Ensures older bars process before newer ones. """ def __init__( self, source_name: str, symbol: str, resolution: str, bar_data: dict, coordinator: Optional[CommitCoordinator] = None, ): """ Initialize data update trigger. Args: source_name: Name of data source (e.g., "binance") symbol: Trading pair symbol resolution: Time resolution bar_data: Bar data dict (time, open, high, low, close, volume) coordinator: CommitCoordinator for accessing stores """ event_time = bar_data.get('time', int(time.time())) # Priority tuple: sort by DATA_SOURCE priority, then event time super().__init__( name=f"data_{source_name}_{symbol}_{resolution}", priority=Priority.DATA_SOURCE, priority_tuple=(Priority.DATA_SOURCE.value, event_time) ) self.source_name = source_name self.symbol = symbol self.resolution = resolution self.bar_data = bar_data self.coordinator = coordinator async def execute(self) -> list[CommitIntent]: """ Process bar update. Future implementations will: - Update indicator values - Check strategy conditions - Trigger alerts/notifications Returns: Commit intents for any store updates """ logger.info( f"Data update trigger: {self.source_name}:{self.symbol}@{self.resolution}, " f"time={self.bar_data.get('time')}" ) # TODO: Update indicators # TODO: Check strategy conditions # TODO: Notify agent of significant events # For now, just log return [] class IndicatorUpdateTrigger(Trigger): """ Trigger for updating indicator values. Can be fired by cron (periodic recalculation) or by data updates. """ def __init__( self, indicator_id: str, force_full_recalc: bool = False, coordinator: Optional[CommitCoordinator] = None, priority: Priority = Priority.SYSTEM, ): """ Initialize indicator update trigger. Args: indicator_id: ID of indicator to update force_full_recalc: If True, recalculate entire history coordinator: CommitCoordinator for accessing stores priority: Execution priority """ super().__init__(f"indicator_{indicator_id}", priority) self.indicator_id = indicator_id self.force_full_recalc = force_full_recalc self.coordinator = coordinator async def execute(self) -> list[CommitIntent]: """ Update indicator value. Reads from IndicatorStore, recalculates, prepares commit. Returns: Commit intents for updated indicator data """ if not self.coordinator: logger.error("No coordinator configured") return [] # Get indicator store indicator_store = self.coordinator.get_store("IndicatorStore") if not indicator_store: logger.error("IndicatorStore not registered") return [] # Read snapshot snapshot_seq, indicator_data = indicator_store.read_snapshot() logger.info( f"Indicator update trigger: {self.indicator_id}, " f"snapshot_seq={snapshot_seq}, force_full={self.force_full_recalc}" ) # TODO: Implement indicator recalculation logic # For now, just return empty (no changes) return [] class CronTrigger(Trigger): """ Trigger fired by APScheduler on a schedule. Wraps another trigger or callable to execute periodically. Priority tuple: (TIMER, scheduled_time, queue_seq) Ensures jobs scheduled for earlier times run first. """ def __init__( self, name: str, inner_trigger: Trigger, scheduled_time: Optional[int] = None, ): """ Initialize cron trigger. Args: name: Descriptive name (e.g., "hourly_sync") inner_trigger: Trigger to execute on schedule scheduled_time: When this was scheduled to run (defaults to now) """ if scheduled_time is None: scheduled_time = int(time.time()) # Priority tuple: sort by TIMER priority, then scheduled time super().__init__( name=f"cron_{name}", priority=Priority.TIMER, priority_tuple=(Priority.TIMER.value, scheduled_time) ) self.inner_trigger = inner_trigger self.scheduled_time = scheduled_time async def execute(self) -> list[CommitIntent]: """Execute the wrapped trigger""" logger.info(f"Cron trigger firing: {self.name}") return await self.inner_trigger.execute()