305 lines
9.2 KiB
Python
305 lines
9.2 KiB
Python
"""
|
|
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()
|