137 lines
5 KiB
Python
137 lines
5 KiB
Python
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
|
|
|
|
|