import logging from typing import Dict, Any, List, Optional, Tuple from modules.interfaces.interfaceChatModel import ChatDocument from modules.services.serviceDocument.documentExtraction import DocumentExtractionService from modules.interfaces.interfaceAiModel import AiCallRequest, AiCallOptions from modules.interfaces.interfaceAiObjects import AiObjects logger = logging.getLogger(__name__) # Model registry is now provided by interfaces via AiModels class AiService: """Centralized AI service orchestrating documents, model selection and failover. The concrete connector instances (OpenAI/Anthropic) are injected by the interface layer. """ def __init__(self, aiObjects: AiObjects | None = None) -> None: # Only depend on interfaces self.aiObjects = aiObjects or AiObjects() self.documentExtractor = DocumentExtractionService() async def callAi( self, prompt: str, documents: Optional[List[ChatDocument]] = None, processDocumentsIndividually: bool = False, options: Optional[AiCallOptions] = None, ) -> str: try: documentContent = "" if documents: documentContent = await self._processDocumentsForAi( documents, options.operationType if options else "general", options.compressContext if options else True, processDocumentsIndividually, ) effectiveOptions = options or AiCallOptions() request = AiCallRequest( prompt=prompt, context=documentContent or None, options=effectiveOptions, ) response = await self.aiObjects.call(request) return response.content except Exception as e: logger.error(f"Error in centralized AI call: {str(e)}") return f"Error: {str(e)}" # Model selection now handled by interface AiObjects # Cost estimation handled by interface for model selection async def _processDocumentsForAi( self, documents: List[ChatDocument], operationType: str, compressDocuments: bool, processIndividually: bool, ) -> str: if not documents: return "" processedContents: List[str] = [] for doc in documents: try: extracted = await self.documentExtractor.processFileData( doc.fileData, doc.fileName, doc.mimeType, prompt=f"Extract relevant content for {operationType}", documentId=doc.id, enableAI=True, ) docContent: List[str] = [] for contentItem in extracted.contents: if contentItem.data and contentItem.data.strip(): docContent.append(contentItem.data) if docContent: combinedDocContent = "\n\n".join(docContent) if ( compressDocuments and len(combinedDocContent.encode("utf-8")) > 10000 ): combinedDocContent = await self._compressContent( combinedDocContent, 10000, "document" ) processedContents.append( f"Document: {doc.fileName}\n{combinedDocContent}" ) except Exception as e: logger.warning( f"Error processing document {doc.fileName}: {str(e)}" ) processedContents.append( f"Document: {doc.fileName}\n[Error processing document: {str(e)}]" ) return "\n\n---\n\n".join(processedContents) # Prompt/context optimization (compression) handled by interface async def _compressContent(self, content: str, targetSize: int, contentType: str) -> str: if len(content.encode("utf-8")) <= targetSize: return content try: compressionPrompt = f""" Komprimiere den folgenden {contentType} auf maximal {targetSize} Zeichen, behalte aber alle wichtigen Informationen bei: {content} Gib nur den komprimierten Inhalt zurück, ohne zusätzliche Erklärungen. """ # Service must not call connectors directly; use simple truncation fallback here data = content.encode("utf-8") return data[:targetSize].decode("utf-8", errors="ignore") + "... [truncated]" except Exception as e: logger.warning(f"AI compression failed, using truncation: {str(e)}") return content[:targetSize] + "... [truncated]" # Failover logic now centralized in interface via model selection; service delegates a single call # Fallback selection moved to interface; service doesn't select models directly