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 interfaceAiCalls from modules.interfaces.interfaceChatObjects import getInterface as getChatObjects 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 = interfaceAiCalls() 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.extractContent(prompt, document) 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__