import logging from typing import Dict, Any, List, Optional, Tuple, Union from modules.datamodels.datamodelChat import ChatDocument from modules.services.serviceDocument.mainServiceDocumentExtraction import DocumentExtractionService from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions from modules.datamodels.datamodelWeb import ( WebSearchRequest, WebCrawlRequest, WebScrapeRequest, WebSearchActionResult, WebCrawlActionResult, WebScrapeActionResult, ) 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, failover, and web operations. """ def __init__(self, serviceCenter=None) -> None: """Initialize AI service with service center access. Args: serviceCenter: Service center instance for accessing other services """ self.serviceCenter = serviceCenter # Only depend on interfaces self.aiObjects = None # Will be initialized in create() self.documentExtractor = DocumentExtractionService() @classmethod async def create(cls, serviceCenter=None) -> "AiService": """Create AiService instance with all connectors initialized.""" instance = cls(serviceCenter) instance.aiObjects = await AiObjects.create() return instance # AI Text Generation async def callAiText( self, prompt: str, documents: Optional[List[ChatDocument]] = None, processDocumentsIndividually: bool = False, options: Optional[AiCallOptions] = None, ) -> str: """Call AI for text generation using interface.call().""" 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 AI text generation: {str(e)}") return f"Error: {str(e)}" # AI Image Analysis async def callAiImage( self, prompt: str, imageData: Union[str, bytes], mimeType: str = None, options: Optional[AiCallOptions] = None, ) -> str: """Call AI for image analysis using interface.callImage().""" try: return await self.aiObjects.callImage(prompt, imageData, mimeType, options) except Exception as e: logger.error(f"Error in AI image analysis: {str(e)}") return f"Error: {str(e)}" # AI Image Generation async def generateImage( self, prompt: str, size: str = "1024x1024", quality: str = "standard", style: str = "vivid", options: Optional[AiCallOptions] = None, ) -> Dict[str, Any]: """Generate an image using AI using interface.generateImage().""" try: return await self.aiObjects.generateImage(prompt, size, quality, style, options) except Exception as e: logger.error(f"Error in AI image generation: {str(e)}") return {"success": False, "error": str(e)} # Web Research (using LangDoc AI) async def webResearch( self, query: str, context: str = "", options: Optional[AiCallOptions] = None, ) -> str: """Perform web research using LangDoc AI via interface.webQuery().""" try: return await self.aiObjects.webQuery(query, context, options) except Exception as e: logger.error(f"Error in web research: {str(e)}") return f"Error: {str(e)}" # Web Search (using Tavily) async def webSearch( self, request: WebSearchRequest, ) -> WebSearchActionResult: """Perform web search using Tavily via interface.webSearch().""" try: return await self.aiObjects.webSearch(request) except Exception as e: logger.error(f"Error in web search: {str(e)}") return WebSearchActionResult(success=False, error=str(e)) # Web Crawl (using Tavily) async def webCrawl( self, request: WebCrawlRequest, ) -> WebCrawlActionResult: """Crawl web pages using Tavily via interface.webCrawl().""" try: return await self.aiObjects.webCrawl(request) except Exception as e: logger.error(f"Error in web crawl: {str(e)}") return WebCrawlActionResult(success=False, error=str(e)) # Web Scrape (using Tavily) async def webScrape( self, request: WebScrapeRequest, ) -> WebScrapeActionResult: """Scrape web content using Tavily via interface.webScrape().""" try: return await self.aiObjects.webScrape(request) except Exception as e: logger.error(f"Error in web scrape: {str(e)}") return WebScrapeActionResult(success=False, error=str(e)) 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) 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]"