481 lines
15 KiB
Python
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,
|
|
}
|
|
}
|