229 lines
8.2 KiB
Python
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]"
|
|
|