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, DocumentExchange ) 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 import hashlib 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) async def extractContentFromFileData(self, prompt: str, fileData: bytes, filename: str, mimeType: str, base64Encoded: bool = False, documentId: str = None) -> str: """Extract content from file data directly using prompt""" extracted_content = await self.documentManager.extractContentFromFileData(prompt, fileData, filename, mimeType, base64Encoded, documentId) # Convert ExtractedContent to string for backward compatibility if hasattr(extracted_content, 'contents'): return "\n".join([item.data for item in extracted_content.contents]) return str(extracted_content) 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[DocumentExchange]]: """Get list of document exchanges sorted by datetime, categorized by chat round""" chat_exchanges = [] history_exchanges = [] # 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, create DocumentExchange with docList reference if message.actionId and message.documentsLabel: doc_ref = self.getDocumentReferenceFromMessage(message) if doc_ref: # Create DocumentExchange with single docList reference doc_exchange = DocumentExchange( documentsLabel=message.documentsLabel, documents=[doc_ref] ) # Add to appropriate list based on message status if message.status == "first": chat_exchanges.append(doc_exchange) break # Stop after finding first message elif message.status == "step": chat_exchanges.append(doc_exchange) else: history_exchanges.append(doc_exchange) # For regular messages, create DocumentExchange with individual docItem references else: doc_refs = [] for doc in message.documents: doc_ref = self.getDocumentReferenceFromChatDocument(doc) doc_refs.append(doc_ref) if doc_refs: # Create DocumentExchange with individual document references doc_exchange = DocumentExchange( documentsLabel=f"{message.id}:documents", documents=doc_refs ) # Add to appropriate list based on message status if message.status == "first": chat_exchanges.append(doc_exchange) break # Stop after finding first message elif message.status == "step": chat_exchanges.append(doc_exchange) else: history_exchanges.append(doc_exchange) # Stop processing if we hit a first message if message.status == "first": break # Sort both lists by datetime in descending order chat_exchanges.sort(key=lambda x: x.documentsLabel, reverse=True) history_exchanges.sort(key=lambda x: x.documentsLabel, reverse=True) return { "chat": chat_exchanges, "history": history_exchanges } def getDocumentReferenceFromChatDocument(self, document: ChatDocument) -> str: """Get document reference from ChatDocument""" return f"docItem:{document.id}:{document.filename}" def getDocumentReferenceFromMessage(self, message: ChatMessage) -> str: """Get document reference from ChatMessage""" # If documentsLabel already contains the full reference format, return it if message.documentsLabel.startswith("docList:"): return message.documentsLabel # Otherwise construct the reference using the message ID and documents label return f"docList:{message.id}:{message.documentsLabel}" def getChatDocumentsFromDocumentList(self, documentList: List[str]) -> List[ChatDocument]: """Get ChatDocuments from a list of document references""" try: all_documents = [] for doc_ref in documentList: # Parse reference format parts = doc_ref.split(':', 2) # Split into max 3 parts if len(parts) < 3: continue ref_type = parts[0] ref_id = parts[1] ref_label = parts[2] # Keep the full label if ref_type == "docItem": # Handle ChatDocument reference: docItem:: # Find document in workflow messages for message in self.workflow.messages: if message.documents: for doc in message.documents: if doc.id == ref_id: all_documents.append(doc) break if any(doc.id == ref_id for doc in message.documents): break elif ref_type == "docList": # Handle document list reference: docList::