gateway/modules/services/serviceAi/mainServiceAi.py
2025-09-26 23:36:56 +02:00

229 lines
8.2 KiB
Python

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]"