""" Context Models for Breakpilot Agents Data classes for conversation messages and context management. """ from typing import Dict, Any, List, Optional from dataclasses import dataclass, field from datetime import datetime, timezone from enum import Enum import logging logger = logging.getLogger(__name__) class MessageRole(Enum): """Message roles in a conversation""" SYSTEM = "system" USER = "user" ASSISTANT = "assistant" TOOL = "tool" @dataclass class Message: """Represents a message in a conversation""" role: MessageRole content: str timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) metadata: Dict[str, Any] = field(default_factory=dict) def to_dict(self) -> Dict[str, Any]: return { "role": self.role.value, "content": self.content, "timestamp": self.timestamp.isoformat(), "metadata": self.metadata } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "Message": return cls( role=MessageRole(data["role"]), content=data["content"], timestamp=datetime.fromisoformat(data["timestamp"]) if "timestamp" in data else datetime.now(timezone.utc), metadata=data.get("metadata", {}) ) @dataclass class ConversationContext: """ Context for a running conversation. Maintains: - Message history with automatic compression - Extracted entities - Intent history - Conversation summary """ messages: List[Message] = field(default_factory=list) entities: Dict[str, Any] = field(default_factory=dict) intent_history: List[str] = field(default_factory=list) summary: Optional[str] = None max_messages: int = 50 system_prompt: Optional[str] = None metadata: Dict[str, Any] = field(default_factory=dict) def add_message( self, role: MessageRole, content: str, metadata: Optional[Dict[str, Any]] = None ) -> Message: """ Adds a message to the conversation. Args: role: Message role content: Message content metadata: Optional message metadata Returns: The created Message """ message = Message( role=role, content=content, metadata=metadata or {} ) self.messages.append(message) # Compress if needed if len(self.messages) > self.max_messages: self._compress_history() return message def add_user_message( self, content: str, metadata: Optional[Dict[str, Any]] = None ) -> Message: """Convenience method to add a user message""" return self.add_message(MessageRole.USER, content, metadata) def add_assistant_message( self, content: str, metadata: Optional[Dict[str, Any]] = None ) -> Message: """Convenience method to add an assistant message""" return self.add_message(MessageRole.ASSISTANT, content, metadata) def add_system_message( self, content: str, metadata: Optional[Dict[str, Any]] = None ) -> Message: """Convenience method to add a system message""" return self.add_message(MessageRole.SYSTEM, content, metadata) def add_intent(self, intent: str) -> None: """ Records an intent in the history. Args: intent: The detected intent """ self.intent_history.append(intent) # Keep last 20 intents if len(self.intent_history) > 20: self.intent_history = self.intent_history[-20:] def set_entity(self, name: str, value: Any) -> None: """ Sets an entity value. Args: name: Entity name value: Entity value """ self.entities[name] = value def get_entity(self, name: str, default: Any = None) -> Any: """ Gets an entity value. Args: name: Entity name default: Default value if not found Returns: Entity value or default """ return self.entities.get(name, default) def get_last_message(self, role: Optional[MessageRole] = None) -> Optional[Message]: """ Gets the last message, optionally filtered by role. Args: role: Optional role filter Returns: The last matching message or None """ if not self.messages: return None if role is None: return self.messages[-1] for msg in reversed(self.messages): if msg.role == role: return msg return None def get_messages_for_llm(self) -> List[Dict[str, str]]: """ Gets messages formatted for LLM API calls. Returns: List of message dicts with role and content """ result = [] # Add system prompt first if self.system_prompt: result.append({ "role": "system", "content": self.system_prompt }) # Add summary if we have one and history was compressed if self.summary: result.append({ "role": "system", "content": f"Previous conversation summary: {self.summary}" }) # Add recent messages for msg in self.messages: result.append({ "role": msg.role.value, "content": msg.content }) return result def _compress_history(self) -> None: """ Compresses older messages to save context window space. Keeps: - System messages - Last 20 messages - Creates summary of compressed middle messages """ # Keep system messages system_msgs = [m for m in self.messages if m.role == MessageRole.SYSTEM] # Keep last 20 messages recent_msgs = self.messages[-20:] # Middle messages to summarize middle_start = len(system_msgs) middle_end = len(self.messages) - 20 middle_msgs = self.messages[middle_start:middle_end] if middle_msgs: # Create a basic summary (can be enhanced with LLM-based summarization) self.summary = self._create_summary(middle_msgs) # Combine self.messages = system_msgs + recent_msgs logger.debug( f"Compressed conversation: {middle_end - middle_start} messages summarized" ) def _create_summary(self, messages: List[Message]) -> str: """ Creates a summary of messages. This is a basic implementation - can be enhanced with LLM-based summarization. Args: messages: Messages to summarize Returns: Summary string """ # Count message types user_count = sum(1 for m in messages if m.role == MessageRole.USER) assistant_count = sum(1 for m in messages if m.role == MessageRole.ASSISTANT) # Extract key topics (simplified - could use NLP) topics = set() for msg in messages: # Simple keyword extraction words = msg.content.lower().split() # Filter common words keywords = [w for w in words if len(w) > 5][:3] topics.update(keywords) topics_str = ", ".join(list(topics)[:5]) return ( f"Earlier conversation: {user_count} user messages, " f"{assistant_count} assistant responses. " f"Topics discussed: {topics_str}" ) def clear(self) -> None: """Clears all context""" self.messages.clear() self.entities.clear() self.intent_history.clear() self.summary = None def to_dict(self) -> Dict[str, Any]: """Serializes context to dict""" return { "messages": [m.to_dict() for m in self.messages], "entities": self.entities, "intent_history": self.intent_history, "summary": self.summary, "max_messages": self.max_messages, "system_prompt": self.system_prompt, "metadata": self.metadata } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "ConversationContext": """Deserializes context from dict""" ctx = cls( messages=[Message.from_dict(m) for m in data.get("messages", [])], entities=data.get("entities", {}), intent_history=data.get("intent_history", []), summary=data.get("summary"), max_messages=data.get("max_messages", 50), system_prompt=data.get("system_prompt"), metadata=data.get("metadata", {}) ) return ctx