""" Chat Session Service Handles all chat session operations including creation, retrieval, updates, and analytics. Integrates with LangGraph's thread_id concept. Usage: from apps.chatbot.services import ChatSessionService # Create new session session = ChatSessionService.create_session( user=request.user, title="Python Help" ) # Get user sessions sessions = ChatSessionService.get_user_sessions(user) # Update analytics ChatSessionService.update_session_analytics( session_id=session.id, message_count=1, tokens_used=150 ) """ from typing import Optional, Dict, Any, List from uuid import UUID from django.db.models import QuerySet, Q, Count, Sum from django.core.exceptions import ObjectDoesNotExist from django.utils import timezone from django.core.cache import cache from apps.chatbot.models import ChatSession from apps.accounts.models import CustomUser class ChatSessionService: """Service for managing chat sessions and threads.""" @staticmethod def create_session( user: CustomUser, title: str = "New Conversation", model_name: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None, enable_summarization: Optional[bool] = None, custom_system_prompt: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None ) -> ChatSession: """ Create a new chat session. Args: user: The user creating the session title: Session title (default: "New Conversation") model_name: AI model to use (defaults to user preference) temperature: Model temperature (defaults to user preference) max_tokens: Max tokens (defaults to user preference) enable_summarization: Enable auto-summarization (defaults to user pref) custom_system_prompt: Custom system prompt (optional) metadata: Additional metadata (optional) Returns: Created ChatSession instance Example: session = ChatSessionService.create_session( user=request.user, title="Python Help", model_name="gpt-4o", temperature=0.7 ) """ # Get user preferences for defaults user_prefs = user.ai_preferences session = ChatSession.objects.create( user=user, title=title, model_name=model_name or user_prefs.default_model, temperature=temperature if temperature is not None else user_prefs.default_temperature, max_tokens=max_tokens or user_prefs.default_max_tokens, enable_summarization=enable_summarization if enable_summarization is not None else user_prefs.enable_auto_summarization, custom_system_prompt=custom_system_prompt or user_prefs.custom_system_prompt, metadata=metadata or {} ) return session @staticmethod def get_session(session_id: UUID, user: Optional[CustomUser] = None) -> ChatSession: """ Get a chat session by ID. Args: session_id: Session UUID user: Optional user for permission check Returns: ChatSession instance Raises: ObjectDoesNotExist: If session not found or user mismatch Example: session = ChatSessionService.get_session( session_id=uuid.UUID("..."), user=request.user ) """ query = ChatSession.objects.filter(id=session_id) if user: query = query.filter(user=user) try: return query.select_related('user').get() except ChatSession.DoesNotExist: raise ObjectDoesNotExist( f"Chat session {session_id} not found or access denied" ) @staticmethod def get_user_sessions( user: CustomUser, active_only: bool = True, archived: bool = False, limit: Optional[int] = None, search_query: Optional[str] = None ) -> QuerySet: """ Get all chat sessions for a user. Args: user: The user active_only: Only active sessions (default: True) archived: Include archived sessions (default: False) limit: Limit number of results (optional) search_query: Search in title and metadata (optional) Returns: QuerySet of ChatSession objects Example: sessions = ChatSessionService.get_user_sessions( user=request.user, active_only=True, limit=50 ) """ query = ChatSession.objects.filter(user=user) if active_only: query = query.filter(is_active=True) if not archived: query = query.filter(is_archived=False) if search_query: query = query.filter( Q(title__icontains=search_query) | Q(metadata__icontains=search_query) ) query = query.select_related('user').order_by('-updated_at') if limit: query = query[:limit] return query @staticmethod def update_session( session_id: UUID, user: CustomUser, **kwargs ) -> ChatSession: """ Update a chat session. Args: session_id: Session UUID user: User for permission check **kwargs: Fields to update (title, model_name, etc.) Returns: Updated ChatSession instance Example: session = ChatSessionService.update_session( session_id=session.id, user=request.user, title="Updated Title", is_pinned=True ) """ session = ChatSessionService.get_session(session_id, user) for field, value in kwargs.items(): if hasattr(session, field): setattr(session, field, value) session.save() # Invalidate cache cache.delete(f'chat_session_{session_id}') return session @staticmethod def update_session_analytics( session_id: UUID, message_count: Optional[int] = None, tokens_used: Optional[int] = None, cost: Optional[float] = None ) -> ChatSession: """ Update session analytics (message count, tokens, cost). Args: session_id: Session UUID message_count: Increment message count by this amount tokens_used: Increment total tokens by this amount cost: Increment total cost by this amount Returns: Updated ChatSession instance Example: ChatSessionService.update_session_analytics( session_id=session.id, message_count=1, tokens_used=150, cost=0.002 ) """ session = ChatSession.objects.get(id=session_id) if message_count: session.message_count += message_count if tokens_used: session.total_tokens_used += tokens_used if cost: session.total_cost += cost session.save(update_fields=[ 'message_count', 'total_tokens_used', 'total_cost', 'updated_at' ]) return session @staticmethod def archive_session(session_id: UUID, user: CustomUser) -> ChatSession: """ Archive a chat session. Args: session_id: Session UUID user: User for permission check Returns: Updated ChatSession instance Example: session = ChatSessionService.archive_session( session_id=session.id, user=request.user ) """ return ChatSessionService.update_session( session_id=session_id, user=user, is_archived=True, is_active=False ) @staticmethod def delete_session(session_id: UUID, user: CustomUser) -> None: """ Soft delete a chat session. Args: session_id: Session UUID user: User for permission check Example: ChatSessionService.delete_session( session_id=session.id, user=request.user ) """ session = ChatSessionService.get_session(session_id, user) session.is_active = False session.is_archived = True session.save() # Invalidate cache cache.delete(f'chat_session_{session_id}') @staticmethod def hard_delete_session(session_id: UUID, user: CustomUser) -> None: """ Permanently delete a chat session. WARNING: This also deletes LangGraph checkpoints! Args: session_id: Session UUID user: User for permission check Example: ChatSessionService.hard_delete_session( session_id=session.id, user=request.user ) """ session = ChatSessionService.get_session(session_id, user) # TODO: Delete LangGraph checkpoints for this thread_id # This requires checkpointer service integration session.delete() # Invalidate cache cache.delete(f'chat_session_{session_id}') @staticmethod def get_session_statistics(session_id: UUID, user: CustomUser) -> Dict[str, Any]: """ Get detailed statistics for a session. Args: session_id: Session UUID user: User for permission check Returns: Dictionary with statistics Example: stats = ChatSessionService.get_session_statistics( session_id=session.id, user=request.user ) """ session = ChatSessionService.get_session(session_id, user) # Get token usage stats token_stats = session.token_usage.aggregate( total_cost=Sum('total_cost'), total_tokens=Sum('total_tokens'), avg_response_time=Sum('response_time_ms') / Count('id') ) # Get feedback stats feedback_stats = session.message_feedback.aggregate( feedback_count=Count('id'), thumbs_up=Count('id', filter=Q(rating='thumbs_up')), thumbs_down=Count('id', filter=Q(rating='thumbs_down')) ) return { 'session_id': str(session.id), 'title': session.title, 'created_at': session.created_at, 'updated_at': session.updated_at, 'message_count': session.message_count, 'total_tokens': token_stats['total_tokens'] or 0, 'total_cost': float(token_stats['total_cost'] or 0), 'avg_response_time_ms': float(token_stats['avg_response_time'] or 0), 'feedback_count': feedback_stats['feedback_count'], 'satisfaction_rate': ( (feedback_stats['thumbs_up'] / feedback_stats['feedback_count'] * 100) if feedback_stats['feedback_count'] > 0 else 0 ) } @staticmethod def get_user_statistics(user: CustomUser) -> Dict[str, Any]: """ Get overall statistics for a user across all sessions. Args: user: The user Returns: Dictionary with user-level statistics Example: stats = ChatSessionService.get_user_statistics( user=request.user ) """ sessions = ChatSession.objects.filter(user=user) total_stats = sessions.aggregate( total_sessions=Count('id'), active_sessions=Count('id', filter=Q(is_active=True)), archived_sessions=Count('id', filter=Q(is_archived=True)), total_messages=Sum('message_count'), total_tokens=Sum('total_tokens_used'), total_cost=Sum('total_cost') ) return { 'user_id': str(user.id), 'total_sessions': total_stats['total_sessions'] or 0, 'active_sessions': total_stats['active_sessions'] or 0, 'archived_sessions': total_stats['archived_sessions'] or 0, 'total_messages': total_stats['total_messages'] or 0, 'total_tokens': total_stats['total_tokens'] or 0, 'total_cost': float(total_stats['total_cost'] or 0) } @staticmethod def pin_session(session_id: UUID, user: CustomUser) -> ChatSession: """ Pin a session to top of list. Args: session_id: Session UUID user: User for permission check Returns: Updated ChatSession instance """ return ChatSessionService.update_session( session_id=session_id, user=user, is_pinned=True ) @staticmethod def unpin_session(session_id: UUID, user: CustomUser) -> ChatSession: """ Unpin a session. Args: session_id: Session UUID user: User for permission check Returns: Updated ChatSession instance """ return ChatSessionService.update_session( session_id=session_id, user=user, is_pinned=False ) @staticmethod def get_thread_config(session: ChatSession) -> Dict[str, Any]: """ Get LangGraph configuration for this session. Args: session: ChatSession instance Returns: LangGraph config dict with thread_id and user settings Example: config = ChatSessionService.get_thread_config(session) response = agent.invoke({"messages": [msg]}, config) """ return { "configurable": { "thread_id": str(session.id), "user_id": str(session.user.id), "model_name": session.model_name, "temperature": float(session.temperature), "max_tokens": session.max_tokens, } }