uchill/chatnext/backend/apps/chatbot/services/chat_session_service.py

481 lines
15 KiB
Python

"""
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,
}
}