gateway/modules/workflow/serviceContainer.py
2025-06-12 17:52:51 +02:00

362 lines
15 KiB
Python

import logging
import importlib
import pkgutil
import inspect
from typing import Dict, Any, List, Optional
from modules.interfaces.serviceAppClass import User
from modules.interfaces.serviceChatModel import (
TaskStatus, ChatDocument, TaskItem, TaskAction, TaskResult,
ChatStat, ChatLog, ChatMessage, ChatWorkflow, UserConnection
)
from modules.interfaces.interfaceAi import interfaceAi
from modules.interfaces.serviceChatClass import getInterface as getChatInterface
from modules.interfaces.serviceManagementClass import getInterface as getFileInterface
from modules.workflow.managerDocument import DocumentManager
from modules.methods.methodBase import MethodBase
import uuid
import base64
logger = logging.getLogger(__name__)
class ServiceContainer:
"""Service container that provides access to all services and their functions"""
def __init__(self, currentUser: User, workflow: ChatWorkflow):
# Core services
self.user = currentUser
self.workflow = workflow
self.tasks = workflow.tasks
self.statusEnums = TaskStatus
self.currentTask = None # Initialize current task as None
# Initialize managers
self.interfaceChat = getChatInterface(currentUser)
self.interfaceFiles = getFileInterface(currentUser)
self.interfaceAi = interfaceAi()
self.documentManager = DocumentManager(self)
# Initialize methods catalog
self.methods = None
# Discover additional methods
self._discoverMethods()
def _discoverMethods(self):
"""Dynamically discover all method classes in modules.methods package"""
try:
# Import the methods package
methodsPackage = importlib.import_module('modules.methods')
# Discover all modules in the package
for _, name, isPkg in pkgutil.iter_modules(methodsPackage.__path__):
if not isPkg and name.startswith('method'):
try:
# Import the module
module = importlib.import_module(f'modules.methods.{name}')
# Find all classes in the module that inherit from MethodBase
for itemName, item in inspect.getmembers(module):
if (inspect.isclass(item) and
issubclass(item, MethodBase) and
item != MethodBase):
# Instantiate the method and add to service
methodInstance = item()
self.methods[methodInstance.name] = methodInstance
logger.info(f"Discovered method: {methodInstance.name}")
except Exception as e:
logger.error(f"Error loading method module {name}: {str(e)}")
except Exception as e:
logger.error(f"Error discovering methods: {str(e)}")
# ===== Functions =====
def extractContent(self, prompt: str, document: ChatDocument) -> str:
"""Extract content from document using prompt"""
return self.documentManager.extractContent(prompt, document)
def getMethodsCatalog(self) -> Dict[str, Any]:
"""Get catalog of available methods"""
return self.methods
def getMethodsList(self) -> List[str]:
"""Get list of available methods with their signatures"""
methodList = []
for methodName, method in self.methods.items():
for actionName, action in method.actions.items():
# Get parameter types and return type from action signature
paramTypes = []
for paramName, param in action.parameters.items():
paramTypes.append(f"{paramName}:{param.type}")
# Format: method.action([param1:type, param2:type])->resultLabel:type # description
signature = f"{methodName}.{actionName}([{', '.join(paramTypes)}])->{action.resultLabel}:{action.resultType}"
if action.description:
signature += f" # {action.description}"
methodList.append(signature)
return methodList
def getDocumentReferenceList(self) -> Dict[str, List[Dict[str, str]]]:
"""Get list of document references sorted by datetime, categorized by chat round"""
chat_refs = []
history_refs = []
# Process messages in reverse order to find current chat round
for message in reversed(self.workflow.messages):
# Get document references from message
if message.documents:
# For messages with action context, use documentList reference
if message.actionId and message.documentsLabel:
doc_ref = self.getDocumentReferenceFromMessage(message)
doc_info = {
"documentReference": doc_ref,
"datetime": message.publishedAt
}
# Add to appropriate list based on message status
if message.status == "first":
chat_refs.append(doc_info)
break # Stop after finding first message
elif message.status == "step":
chat_refs.append(doc_info)
else:
history_refs.append(doc_info)
# For regular messages, use individual document references
else:
for doc in message.documents:
doc_ref = self.getDocumentReferenceFromChatDocument(doc)
doc_info = {
"documentReference": doc_ref,
"datetime": message.publishedAt
}
# Add to appropriate list based on message status
if message.status == "first":
chat_refs.append(doc_info)
break # Stop after finding first message
elif message.status == "step":
chat_refs.append(doc_info)
else:
history_refs.append(doc_info)
# Stop processing if we hit a first message
if message.status == "first":
break
# Sort both lists by datetime in descending order
chat_refs.sort(key=lambda x: x["datetime"], reverse=True)
history_refs.sort(key=lambda x: x["datetime"], reverse=True)
return {
"chat": chat_refs,
"history": history_refs
}
def getDocumentReferenceFromChatDocument(self, document: ChatDocument) -> str:
"""Get document reference from ChatDocument"""
return f"document_{document.id}_{document.filename}"
def getDocumentReferenceFromMessage(self, message: ChatMessage) -> str:
"""Get document reference from ChatMessage with action context"""
if not message.actionId or not message.documentsLabel:
return None
# If documentsLabel already contains the full reference format, return it
if message.documentsLabel.startswith("documentList_"):
return message.documentsLabel
# Otherwise construct the reference
return f"documentList_{message.actionId}_{message.documentsLabel}"
def getChatDocumentsFromDocumentReference(self, documentReference: str) -> List[ChatDocument]:
"""Get ChatDocuments from document reference"""
try:
# Parse reference format
parts = documentReference.split('_', 2) # Split into max 3 parts
if len(parts) < 3:
return []
ref_type = parts[0]
ref_id = parts[1]
ref_label = parts[2] # Keep the full label
if ref_type == "document":
# Handle ChatDocument reference: document_<id>_<filename>
# Find document in workflow messages
for message in self.workflow.messages:
if message.documents:
for doc in message.documents:
if doc.id == ref_id:
return [doc]
elif ref_type == "documentList":
# Handle document list reference: documentList_<action.id>_<label>
# Find message with matching action ID and documents label
for message in self.workflow.messages:
if (message.actionId == ref_id and
message.documentsLabel == documentReference and # Compare full reference
message.documents):
return message.documents
return []
except Exception as e:
logger.error(f"Error getting documents from reference {documentReference}: {str(e)}")
return []
def getConnectionReferenceList(self) -> List[Dict[str, str]]:
"""Get list of connection references sorted by authority"""
return self._getConnectionReferences()
def getConnectionReferenceFromUserConnection(self, connection: UserConnection) -> str:
"""Get connection reference from UserConnection"""
return f"connection_{connection.id}_{connection.authority}"
def getUserConnectionFromConnectionReference(self, reference: str) -> UserConnection:
"""Get UserConnection from connection reference"""
return self._getUserConnectionByReference(reference)
def getMessageSummary(self, message: ChatMessage) -> Dict[str, List[Dict[str, Any]]]:
"""Get message summary"""
return {
"chat": self._getChatMessageSummaries(),
"history": self._getHistoryMessageSummaries()
}
def getFileData(self, fileId: str) -> bytes:
"""Get file data by ID"""
return self.interfaceFiles.getFileData(fileId)
def callAiBasic(self, prompt: str, context: str = None, complexityFlag: bool = False) -> str:
"""Call basic AI service"""
return self.interfaceAi.callAiBasic(prompt, context, complexityFlag)
def callAiImage(self, imageData: bytes, mimeType: str, prompt: str) -> str:
"""Call AI image service"""
return self.interfaceAi.callAiImage(imageData, mimeType, prompt)
def createFile(self, fileName: str, mimeType: str, content: str, base64encoded: bool = False) -> str:
"""Create new file and return its ID"""
# Convert content to bytes based on base64 flag
if base64encoded:
content_bytes = base64.b64decode(content)
else:
content_bytes = content.encode('utf-8')
# First create the file metadata
file_item = self.interfaceFiles.createFile(
name=fileName,
mimeType=mimeType,
size=len(content_bytes)
)
# Then store the file data
self.interfaceFiles.createFileData(file_item.id, content_bytes)
return file_item.id
def createDocument(self, fileName: str, mimeType: str, content: str, base64encoded: bool = True) -> ChatDocument:
"""Create document from file data object created by AI call"""
# First create the file and get its ID
file_id = self.createFile(fileName, mimeType, content, base64encoded)
# Get file info for metadata
file_info = self.interfaceFiles.getFile(file_id)
# Create document with file reference
return ChatDocument(
id=str(uuid.uuid4()),
fileId=file_id,
filename=fileName,
fileSize=file_info.fileSize,
mimeType=mimeType
)
def getFileInfo(self, fileId: str) -> Dict[str, Any]:
"""Get file information"""
return self.interfaceFiles.getFileInfo(fileId)
# ===== Private Methods =====
def _executeMethodAction(self, parameters: Dict[str, Any]) -> Any:
"""Execute method action with parameters"""
method = parameters.get('method')
action = parameters.get('action')
if method in self.methods and action in self.methods[method]:
return self.methods[method][action](**parameters.get('parameters', {}))
raise ValueError(f"Unknown method or action: {method}.{action}")
def _executeForEach(self, items: List[Any], action: callable) -> List[Any]:
"""Execute forEach operation"""
results = []
for item in items:
try:
result = action(item)
results.append(result)
except Exception as e:
logger.error(f"Error executing forEach action: {str(e)}")
results.append(None)
return results
def _executeAiCall(self, prompt: str, documents: List[Dict[str, Any]]) -> List[Any]:
"""Execute AI call with documents"""
try:
# Process each document
results = []
for doc in documents:
content = self.extractContent(prompt, doc)
results.append(content)
return results
except Exception as e:
logger.error(f"Error executing AI call: {str(e)}")
return []
def _executeSharePointQuery(self, connection: str, site_query: str, file_query: str, content_query: str) -> List[Dict[str, str]]:
"""Execute SharePoint query"""
# TODO: Implement SharePoint query
return []
def _executeSharePointDownload(self, connection: str, filepath: str) -> str:
"""Execute SharePoint download"""
# TODO: Implement SharePoint download
return ""
def _getChatDocumentReferences(self) -> List[Dict[str, str]]:
"""Get chat document references"""
# TODO: Implement chat document references
return []
def _getHistoryDocumentReferences(self) -> List[Dict[str, str]]:
"""Get history document references"""
# TODO: Implement history document references
return []
def _getConnectionReferences(self) -> List[Dict[str, str]]:
"""Get connection references"""
# TODO: Implement connection references
return []
def _getUserConnectionByReference(self, reference: str) -> UserConnection:
"""Get user connection by reference"""
# TODO: Implement user connection lookup
pass
def _getChatMessageSummaries(self) -> List[Dict[str, Any]]:
"""Get chat message summaries"""
# TODO: Implement chat message summaries
return []
def _getHistoryMessageSummaries(self) -> List[Dict[str, Any]]:
"""Get history message summaries"""
# TODO: Implement history message summaries
return []
# Create singleton instance
serviceObject = None
def initializeServiceContainer(currentUser: User, workflow: ChatWorkflow) -> ServiceContainer:
"""Initialize the service container singleton"""
global serviceObject
if serviceObject is None:
serviceObject = ServiceContainer(currentUser, workflow)
return serviceObject