""" RAG Chat Utility Handles document search and chat completions for character interactions. """ from typing import List, Dict, Any, Optional from qdrant_client import QdrantClient from lib.embedding_providers.base import BaseProvider class RAGChatService: """ Service for character-based RAG chat. Searches across document collections and generates answers. """ def __init__( self, qdrant_client: QdrantClient, embedding_provider: BaseProvider, completion_provider: BaseProvider ): """ Initialize RAG chat service. Args: qdrant_client: Qdrant client instance embedding_provider: Provider for generating embeddings completion_provider: Provider for completions """ self.qdrant = qdrant_client self.embedding_provider = embedding_provider self.completion_provider = completion_provider # Available document collections self.doc_collections = [ "docs_crumbforest_", "docs_rz_nullfeld_" ] def search_documents( self, query: str, limit: int = 5, collections: Optional[List[str]] = None ) -> List[Dict[str, Any]]: """ Search across document collections. Args: query: Search query limit: Maximum results per collection collections: Optional list of collections to search (defaults to all) Returns: List of search results with scores """ if collections is None: collections = self.doc_collections # Generate query embedding query_embedding = self.embedding_provider.get_embeddings([query])[0] all_results = [] # Search each collection for collection_name in collections: try: # Check if collection exists existing_collections = [ col.name for col in self.qdrant.get_collections().collections ] if collection_name not in existing_collections: continue # Search in Qdrant using new Query API response = self.qdrant.query_points( collection_name=collection_name, query=query_embedding, limit=limit ) search_results = response.points # Format results for hit in search_results: all_results.append({ 'collection': collection_name, 'content': hit.payload.get('content', ''), 'file_path': hit.payload.get('file_path', ''), 'header': hit.payload.get('header', ''), 'score': hit.score }) except Exception as e: # Log error but continue with other collections print(f"Error searching collection {collection_name}: {str(e)}") continue # Sort all results by score (descending) all_results.sort(key=lambda x: x['score'], reverse=True) # Return top results return all_results[:limit] def chat_with_context( self, question: str, character_name: str, character_prompt: str, context_limit: int = 3, lang: str = "de", model_override: Optional[str] = None ) -> Dict[str, Any]: """ Answer a question using RAG with character persona. Args: question: User's question character_name: Name of the character (e.g., "Krümeleule") character_prompt: Character system prompt context_limit: Number of context chunks to retrieve lang: Language code model_override: Optional model name to override default Returns: Dictionary with answer and sources """ # Retrieve relevant context search_results = self.search_documents( query=question, limit=context_limit ) # Build context from search results if search_results: context_parts = [] for idx, result in enumerate(search_results, 1): header = result['header'] if result['header'] else result['file_path'] context_parts.append( f"[Quelle {idx}: {header}]\n{result['content']}" ) context = "\n\n".join(context_parts) else: context = "" # Build system message with character persona + context if context: system_message = ( f"{character_prompt}\n\n" f"Nutze folgende Informationen aus der Wissensdatenbank, um die Frage zu beantworten:\n\n" f"{context}" ) else: system_message = ( f"{character_prompt}\n\n" f"Hinweis: Keine relevanten Informationen in der Wissensdatenbank gefunden. " f"Antworte trotzdem hilfreich basierend auf deinem Charakter." ) # Generate answer with character persona answer = self._get_completion_with_system( system_message=system_message, user_message=question, model_override=model_override ) # Format sources sources = [ { 'collection': r['collection'], 'file_path': r['file_path'], 'header': r['header'], 'score': r['score'] } for r in search_results ] return { 'answer': answer, 'sources': sources, 'context_found': len(search_results) > 0, 'provider': self.completion_provider.provider_name, 'model': model_override or self.completion_provider.model_name } def _get_completion_with_system( self, system_message: str, user_message: str, model_override: Optional[str] = None ) -> str: """ Get completion with explicit system message. Handles both context-based and direct completion. Args: system_message: System prompt with character + context user_message: User's question model_override: Optional model name to use Returns: Generated response """ # For providers that support system messages natively # We'll use the get_completion method with context # The system_message already contains the character prompt + context # Call completion provider # Note: We pass the system message as context # Assuming provider.get_completion supports model argument? # If not, we might need to check BaseProvider. # But get_completion usually takes keyword args passed to API. return self.completion_provider.get_completion( prompt=user_message, context=system_message, model=model_override )