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.interfaces.interfaceAppObjects import getInterface as getAppObjects from modules.workflow.managerDocument import DocumentManager from modules.workflow.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.interfaceApp = getAppObjects(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(type(methodInstance), predicate=inspect.iscoroutinefunction): if not methodName.startswith('_') and methodName not in ['execute', 'validateParameters']: # Bind the method to the instance bound_method = method.__get__(methodInstance, type(methodInstance)) sig = inspect.signature(method) params = {} for paramName, param in sig.parameters.items(): if paramName not in ['self', 'authData']: # Get parameter type paramType = param.annotation if param.annotation != param.empty else Any # Get parameter description from docstring or default paramDesc = None if param.default != param.empty and hasattr(param.default, '__doc__'): paramDesc = param.default.__doc__ params[paramName] = { 'type': paramType, 'required': param.default == param.empty, 'description': paramDesc, 'default': param.default if param.default != param.empty else None } actions[methodName] = { 'description': method.__doc__ or '', 'parameters': params, 'method': bound_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)}", exc_info=True) 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 in the required format""" methodList = [] for methodName, method in self.methods.items(): methodInstance = method['instance'] for actionName, action in method['actions'].items(): # Use the new signature format from MethodBase signature = methodInstance.getActionSignature(actionName) if signature: 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) if doc_ref: doc_info = { "documentReference": doc_ref, "datetime": message.publishedAt, "actionMethod": message.actionMethod, "actionName": message.actionName, "documentCount": len(message.documents) } # 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, "actionMethod": None, "actionName": None, "documentCount": 1 } # 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__