# Copyright (c) 2025 Patrick Motsch # All rights reserved. # methodDiscovery.py # Method discovery and management for workflow execution import logging import importlib import pkgutil import inspect from typing import Any, Dict, List from modules.workflows.methods.methodBase import MethodBase # Set up logger logger = logging.getLogger(__name__) # Global methods catalog - moved from serviceCenter methods = {} def discoverMethods(serviceCenter): """Dynamically discover all method classes and their actions in modules methods package. CRITICAL: If methods are already discovered, updates their Services reference to ensure they use the current workflow (self.services.workflow). This prevents stale workflow IDs from being used when a new workflow starts. """ try: # Import the methods package methodsPackage = importlib.import_module('modules.workflows.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.workflows.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): # Check if method already exists in cache shortName = itemName.replace('Method', '').lower() if itemName in methods or shortName in methods: # Method already discovered - update Services reference to use current workflow existingMethodInfo = methods.get(itemName) or methods.get(shortName) if existingMethodInfo and existingMethodInfo.get('instance'): existingMethodInfo['instance'].services = serviceCenter logger.debug(f"Updated Services reference for cached method {itemName} to use current workflow") else: # Method exists but instance is missing - recreate it methodInstance = item(serviceCenter) actions = methodInstance.actions methodInfo = { 'instance': methodInstance, 'actions': actions, 'description': item.__doc__ or f"Method {itemName}" } methods[itemName] = methodInfo methods[shortName] = methodInfo logger.info(f"Recreated method {itemName} (short: {shortName}) with {len(actions)} actions") else: # Method not discovered yet - create new instance methodInstance = item(serviceCenter) # Use the actions property from MethodBase which handles @action decorator actions = methodInstance.actions # Create method info methodInfo = { 'instance': methodInstance, 'actions': actions, 'description': item.__doc__ or f"Method {itemName}" } # Store the method with full class name methods[itemName] = methodInfo # Also store with short name for action executor access methods[shortName] = methodInfo logger.info(f"Discovered method {itemName} (short: {shortName}) with {len(actions)} actions") except Exception as e: logger.error(f"Error discovering method {name}: {str(e)}") continue logger.info(f"Discovered/updated {len(methods)} method entries total") except Exception as e: logger.error(f"Error discovering methods: {str(e)}") def getMethodsList(serviceCenter): """Get a list of available methods with their signatures""" if not methods: discoverMethods(serviceCenter) methodsList = [] for methodName, methodInfo in methods.items(): methodDescription = methodInfo['description'] actionsList = [] for actionName, actionInfo in methodInfo['actions'].items(): actionDescription = actionInfo['description'] parameters = actionInfo['parameters'] # Build parameter signature paramSig = [] for paramName, paramInfo in parameters.items(): paramType = paramInfo['type'] paramRequired = paramInfo['required'] paramDefault = paramInfo['default'] if paramRequired: paramSig.append(f"{paramName}: {paramType}") else: defaultStr = f" = {paramDefault}" if paramDefault is not None else " = None" paramSig.append(f"{paramName}: {paramType}{defaultStr}") paramSignature = f"({', '.join(paramSig)})" if paramSig else "()" actionsList.append(f"- {actionName}{paramSignature}: {actionDescription}") actionsStr = "\n".join(actionsList) methodsList.append(f"**{methodName}**: {methodDescription}\n{actionsStr}") return "\n\n".join(methodsList) def getActionParameterList(methodName: str, actionName: str, methods: Dict[str, Any]) -> str: """Get action parameter list from method docstring for AI parameter generation (list only).""" try: if not methods or methodName not in methods: return "" methodInstance = methods[methodName]['instance'] if actionName not in methodInstance.actions: return "" action_info = methodInstance.actions[actionName] # Extract parameter descriptions from docstring docstring = action_info.get('description', '') paramDescriptions, paramTypes = methodInstance._extractParameterDetails(docstring) param_list = [] for paramName, paramDesc in paramDescriptions.items(): paramType = paramTypes.get(paramName, 'Any') if paramDesc: param_list.append(f"- {paramName} ({paramType}): {paramDesc}") else: param_list.append(f"- {paramName} ({paramType})") # Return list only, without leading headings or trailing text return "\n".join(param_list) except Exception as e: logger.error(f"Error getting action parameter signature for {methodName}.{actionName}: {str(e)}") return ""