gateway/modules/services/serviceAi/mainServiceAi.py
2025-09-24 23:18:10 +02:00

143 lines
5.2 KiB
Python

import logging
from typing import Dict, Any, List, Optional, Tuple
from modules.interfaces.interfaceChatModel import ChatDocument
from modules.services.serviceDocument.mainServiceDocumentExtraction 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, 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 = 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