import logging import importlib import pkgutil import inspect from typing import Dict, Any, List, Optional from modules.interfaces.interfaceAppModel import User, UserConnection from modules.interfaces.interfaceChatModel import ( TaskStatus, ChatDocument, TaskItem, TaskAction, TaskResult, ChatStat, ChatLog, ChatMessage, ChatWorkflow ) from modules.interfaces.interfaceAiCalls import AiCalls from modules.interfaces.interfaceChatObjects import getInterface as getChatObjects from modules.interfaces.interfaceChatModel import ActionResult from modules.interfaces.interfaceComponentObjects import getInterface as getComponentObjects 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 = getChatObjects(currentUser) self.interfaceComponent = getComponentObjects(currentUser) self.interfaceAiCalls = AiCalls() self.documentManager = DocumentManager(self) # Initialize methods catalog self.methods = {} # Discover additional methods self._discoverMethods() def _discoverMethods(self): """Dynamically discover all method classes and their actions 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 methodInstance = item(self) # Discover actions from public methods actions = {} for methodName, method in inspect.getmembers(methodInstance, predicate=inspect.isfunction): # Skip private methods and inherited methods if not methodName.startswith('_') and methodName not in ['execute', 'actions', 'validateParameters']: # Get method signature sig = inspect.signature(method) params = {} # Convert parameters to action definition for paramName, param in sig.parameters.items(): if paramName not in ['self', 'authData']: params[paramName] = { 'type': param.annotation if param.annotation != param.empty else Any, 'required': param.default == param.empty, 'description': param.default.__doc__ if hasattr(param.default, '__doc__') else None } # Add action definition actions[methodName] = { 'description': method.__doc__ or '', 'parameters': params, 'method': method } # Add method instance with discovered actions self.methods[methodInstance.name] = { 'instance': methodInstance, 'description': methodInstance.description, 'actions': actions } logger.info(f"Discovered method: {methodInstance.name} with {len(actions)} actions") 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.extractContentFromDocument(prompt, document) def extractContentFromFileData(self, prompt: str, fileData: bytes, filename: str, mimeType: str, base64Encoded: bool = False) -> str: """Extract content from file data directly using prompt""" return self.documentManager.extractContentFromFileData(prompt, fileData, filename, mimeType, base64Encoded) def getMethodsCatalog(self) -> Dict[str, Any]: """Get catalog of available methods and their actions""" catalog = {} for methodName, method in self.methods.items(): catalog[methodName] = { 'description': method['description'], 'actions': { actionName: { 'description': action['description'], 'parameters': action['parameters'] } for actionName, action in method['actions'].items() } } return catalog 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 from action signature paramTypes = [] for paramName, param in action['parameters'].items(): paramTypes.append(f"{paramName}:{param['type']}") # Format: method.action([param1:type, param2:type]) # description signature = f"{methodName}.{actionName}([{', '.join(paramTypes)}])" 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__ # 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__