""" Conversation model for tracking dialogue between user and Claude. """ from datetime import datetime from typing import Optional, List, Dict, Any from sqlalchemy import String, Text, Integer, DateTime, JSON, ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.sql import func from .base import Base, TimestampMixin class Conversation(Base, TimestampMixin): """ Represents a conversation exchange between user and Claude. Each conversation entry captures either a user prompt or Claude's response, along with context about tools used and files affected. """ __tablename__ = "conversations" # Primary key id: Mapped[int] = mapped_column(primary_key=True) # Foreign key to session session_id: Mapped[int] = mapped_column(ForeignKey("sessions.id"), nullable=False, index=True) # Timing timestamp: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=func.now(), index=True ) # Conversation content user_prompt: Mapped[Optional[str]] = mapped_column(Text, nullable=True) claude_response: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # Context and metadata tools_used: Mapped[Optional[List[str]]] = mapped_column(JSON, nullable=True) files_affected: Mapped[Optional[List[str]]] = mapped_column(JSON, nullable=True) context: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True) # Token estimates for analysis tokens_input: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) tokens_output: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # Exchange type for categorization exchange_type: Mapped[str] = mapped_column(String(50), nullable=False) # user_prompt, claude_response # Relationships session: Mapped["Session"] = relationship("Session", back_populates="conversations") def __repr__(self) -> str: content_preview = "" if self.user_prompt: content_preview = self.user_prompt[:50] + "..." if len(self.user_prompt) > 50 else self.user_prompt elif self.claude_response: content_preview = self.claude_response[:50] + "..." if len(self.claude_response) > 50 else self.claude_response return f"" @property def is_user_prompt(self) -> bool: """Check if this is a user prompt.""" return self.exchange_type == "user_prompt" @property def is_claude_response(self) -> bool: """Check if this is a Claude response.""" return self.exchange_type == "claude_response" @property def content_length(self) -> int: """Get the total character length of the conversation content.""" user_length = len(self.user_prompt) if self.user_prompt else 0 claude_length = len(self.claude_response) if self.claude_response else 0 return user_length + claude_length @property def estimated_tokens(self) -> int: """Estimate total tokens in this conversation exchange.""" if self.tokens_input and self.tokens_output: return self.tokens_input + self.tokens_output # Rough estimation: ~4 characters per token return self.content_length // 4 def get_intent_category(self) -> Optional[str]: """Extract intent category from context if available.""" if self.context and "intent" in self.context: return self.context["intent"] return None def get_complexity_level(self) -> Optional[str]: """Extract complexity level from context if available.""" if self.context and "complexity" in self.context: return self.context["complexity"] return None def has_file_operations(self) -> bool: """Check if this conversation involved file operations.""" if not self.tools_used: return False file_tools = {"Edit", "Write", "Read"} return any(tool in file_tools for tool in self.tools_used) def has_code_execution(self) -> bool: """Check if this conversation involved code execution.""" if not self.tools_used: return False execution_tools = {"Bash", "Task"} return any(tool in execution_tools for tool in self.tools_used)