""" ConversationContext service layer for business logic and database operations. Handles all database operations for conversation contexts, providing context recall and retrieval functionality for Claude's memory system. """ import json from typing import List, Optional from uuid import UUID from fastapi import HTTPException, status from sqlalchemy import or_ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from api.models.conversation_context import ConversationContext from api.schemas.conversation_context import ConversationContextCreate, ConversationContextUpdate from api.utils.context_compression import format_for_injection def get_conversation_contexts( db: Session, skip: int = 0, limit: int = 100 ) -> tuple[list[ConversationContext], int]: """ Retrieve a paginated list of conversation contexts. Args: db: Database session skip: Number of records to skip (for pagination) limit: Maximum number of records to return Returns: tuple: (list of conversation contexts, total count) """ # Get total count total = db.query(ConversationContext).count() # Get paginated results, ordered by relevance and recency contexts = ( db.query(ConversationContext) .order_by(ConversationContext.relevance_score.desc(), ConversationContext.created_at.desc()) .offset(skip) .limit(limit) .all() ) return contexts, total def get_conversation_context_by_id(db: Session, context_id: UUID) -> ConversationContext: """ Retrieve a single conversation context by its ID. Args: db: Database session context_id: UUID of the conversation context to retrieve Returns: ConversationContext: The conversation context object Raises: HTTPException: 404 if conversation context not found """ context = db.query(ConversationContext).filter(ConversationContext.id == str(context_id)).first() if not context: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"ConversationContext with ID {context_id} not found" ) return context def get_conversation_contexts_by_project( db: Session, project_id: UUID, skip: int = 0, limit: int = 100 ) -> tuple[list[ConversationContext], int]: """ Retrieve conversation contexts for a specific project. Args: db: Database session project_id: UUID of the project skip: Number of records to skip limit: Maximum number of records to return Returns: tuple: (list of conversation contexts, total count) """ # Get total count for project total = db.query(ConversationContext).filter( ConversationContext.project_id == str(project_id) ).count() # Get paginated results contexts = ( db.query(ConversationContext) .filter(ConversationContext.project_id == str(project_id)) .order_by(ConversationContext.relevance_score.desc(), ConversationContext.created_at.desc()) .offset(skip) .limit(limit) .all() ) return contexts, total def get_conversation_contexts_by_session( db: Session, session_id: UUID, skip: int = 0, limit: int = 100 ) -> tuple[list[ConversationContext], int]: """ Retrieve conversation contexts for a specific session. Args: db: Database session session_id: UUID of the session skip: Number of records to skip limit: Maximum number of records to return Returns: tuple: (list of conversation contexts, total count) """ # Get total count for session total = db.query(ConversationContext).filter( ConversationContext.session_id == str(session_id) ).count() # Get paginated results contexts = ( db.query(ConversationContext) .filter(ConversationContext.session_id == str(session_id)) .order_by(ConversationContext.created_at.desc()) .offset(skip) .limit(limit) .all() ) return contexts, total def get_recall_context( db: Session, project_id: Optional[UUID] = None, tags: Optional[List[str]] = None, limit: int = 10, min_relevance_score: float = 5.0 ) -> str: """ Get relevant contexts formatted for Claude prompt injection. This is the main context recall function that retrieves the most relevant contexts and formats them for efficient injection into Claude's prompt. Args: db: Database session project_id: Optional project ID to filter by tags: Optional list of tags to filter by limit: Maximum number of contexts to retrieve (default 10) min_relevance_score: Minimum relevance score threshold (default 5.0) Returns: str: Token-efficient markdown string ready for prompt injection """ # Build query query = db.query(ConversationContext) # Filter by project if specified if project_id: query = query.filter(ConversationContext.project_id == str(project_id)) # Filter by minimum relevance score query = query.filter(ConversationContext.relevance_score >= min_relevance_score) # Filter by tags if specified if tags: # Check if any of the provided tags exist in the JSON tags field # This uses PostgreSQL's JSON operators tag_filters = [] for tag in tags: tag_filters.append(ConversationContext.tags.contains(f'"{tag}"')) if tag_filters: query = query.filter(or_(*tag_filters)) # Order by relevance score and get top results contexts = query.order_by( ConversationContext.relevance_score.desc() ).limit(limit).all() # Convert to dictionary format for formatting context_dicts = [] for ctx in contexts: context_dict = { "content": ctx.dense_summary or ctx.title, "type": ctx.context_type, "tags": json.loads(ctx.tags) if ctx.tags else [], "relevance_score": ctx.relevance_score } context_dicts.append(context_dict) # Use compression utility to format for injection return format_for_injection(context_dicts) def create_conversation_context( db: Session, context_data: ConversationContextCreate ) -> ConversationContext: """ Create a new conversation context. Args: db: Database session context_data: Conversation context creation data Returns: ConversationContext: The created conversation context object Raises: HTTPException: 500 if database error occurs """ try: # Create new conversation context instance db_context = ConversationContext(**context_data.model_dump()) # Add to database db.add(db_context) db.commit() db.refresh(db_context) return db_context except IntegrityError as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Database error: {str(e)}" ) except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to create conversation context: {str(e)}" ) def update_conversation_context( db: Session, context_id: UUID, context_data: ConversationContextUpdate ) -> ConversationContext: """ Update an existing conversation context. Args: db: Database session context_id: UUID of the conversation context to update context_data: Conversation context update data Returns: ConversationContext: The updated conversation context object Raises: HTTPException: 404 if conversation context not found HTTPException: 500 if database error occurs """ # Get existing context context = get_conversation_context_by_id(db, context_id) try: # Update only provided fields update_data = context_data.model_dump(exclude_unset=True) # Apply updates for field, value in update_data.items(): setattr(context, field, value) db.commit() db.refresh(context) return context except HTTPException: db.rollback() raise except IntegrityError as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Database error: {str(e)}" ) except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update conversation context: {str(e)}" ) def delete_conversation_context(db: Session, context_id: UUID) -> dict: """ Delete a conversation context by its ID. Args: db: Database session context_id: UUID of the conversation context to delete Returns: dict: Success message Raises: HTTPException: 404 if conversation context not found HTTPException: 500 if database error occurs """ # Get existing context (raises 404 if not found) context = get_conversation_context_by_id(db, context_id) try: db.delete(context) db.commit() return { "message": "ConversationContext deleted successfully", "context_id": str(context_id) } except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to delete conversation context: {str(e)}" )