from __future__ import annotations from abc import ABC, abstractmethod from datetime import datetime from enum import Enum from typing import Type from pydantic import BaseModel, ConfigDict from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text, text from sqlalchemy import Enum as SQLEnum from storage.base import Base from openhands.events.observation.agent import AgentStateChangedObservation from openhands.utils.import_utils import get_impl class ConversationCallbackProcessor(BaseModel, ABC): """ Abstract base class for conversation callback processors. Conversation processors are invoked when events occur in a conversation to perform additional processing, notifications, or integrations. """ model_config = ConfigDict( # Allow extra fields for flexibility extra='allow', # Allow arbitrary types arbitrary_types_allowed=True, ) @abstractmethod async def __call__( self, callback: ConversationCallback, observation: AgentStateChangedObservation, ) -> None: """ Process a conversation event. Args: conversation_id: The ID of the conversation to process observation: The AgentStateChangedObservation that triggered the callback callback: The conversation callback """ class CallbackStatus(Enum): """Status of a conversation callback.""" ACTIVE = 'ACTIVE' COMPLETED = 'COMPLETED' ERROR = 'ERROR' class ConversationCallback(Base): # type: ignore """ Model for storing conversation callbacks that process conversation events. """ __tablename__ = 'conversation_callbacks' id = Column(Integer, primary_key=True, autoincrement=True) conversation_id = Column( String, ForeignKey('conversation_metadata.conversation_id'), nullable=False, index=True, ) status = Column( SQLEnum(CallbackStatus), nullable=False, default=CallbackStatus.ACTIVE ) processor_type = Column(String, nullable=False) processor_json = Column(Text, nullable=False) created_at = Column( DateTime, server_default=text('CURRENT_TIMESTAMP'), nullable=False, ) updated_at = Column( DateTime, server_default=text('CURRENT_TIMESTAMP'), onupdate=datetime.now, nullable=False, ) def get_processor(self) -> ConversationCallbackProcessor: """ Get the processor instance from the stored processor type and JSON data. Returns: ConversationCallbackProcessor: The processor instance """ # Import the processor class dynamically processor_type: Type[ConversationCallbackProcessor] = get_impl( ConversationCallbackProcessor, self.processor_type ) processor = processor_type.model_validate_json(self.processor_json) return processor def set_processor(self, processor: ConversationCallbackProcessor) -> None: """ Set the processor instance, storing its type and JSON representation. Args: processor: The ConversationCallbackProcessor instance to store """ self.processor_type = ( f'{processor.__class__.__module__}.{processor.__class__.__name__}' ) self.processor_json = processor.model_dump_json()