sos-bot/core/action_loader.py
bachko 3309a47e10 Initial commit: SoS bot framework
- ADB client (connect, tap, swipe, screencap, app control)
- Vision module (in-memory screenshot + OpenCV template matching)
- Action loader (auto-discovers .py files in actions/)
- Engine (screenshot → detect → act loop with YAML playbook)
- 12 built-in actions: tap, click, wait, swipe, find,
  find_and_click, wait_until, click_seq, keyevent, app, log
- Example custom action template
- Debug screenshot tool for template creation
- Detailed plan.md with 20 actions across 9 phases
- Sample playbook with 10 steps
2026-07-09 13:52:31 +00:00

80 lines
2.6 KiB
Python

"""
Action Loader — dynamically discovers and loads action modules
from the actions/ directory. Each .py file is a self-contained action.
"""
import importlib.util
import logging
import os
import sys
logger = logging.getLogger("sos.actions")
class ActionRegistry:
"""Holds all registered actions by name."""
def __init__(self):
self._actions = {}
def register(self, action):
"""Register an action instance."""
self._actions[action.name] = action
def get(self, name: str):
"""Get an action by name, or None."""
return self._actions.get(name)
def list(self):
"""Return all registered action names."""
return list(self._actions.keys())
def run(self, name: str, ctx: dict, params: dict):
"""Run an action by name. Raises KeyError if not found."""
action = self.get(name)
if action is None:
raise KeyError(f"Unknown action: '{name}' (available: {self.list()})")
return action.run(ctx, params)
def load_actions(actions_dir: str) -> ActionRegistry:
"""
Scan actions_dir for .py files (except __init__.py) and load each.
Each module must export a class named 'Action' with:
- name: str (the playbook-visible name)
- def run(self, ctx: dict, params: dict) -> any
Returns an ActionRegistry with all loaded actions.
"""
registry = ActionRegistry()
if not os.path.isdir(actions_dir):
logger.warning("Actions directory not found: %s", actions_dir)
return registry
sys.path.insert(0, os.path.dirname(actions_dir))
for fname in sorted(os.listdir(actions_dir)):
if not fname.endswith(".py") or fname == "__init__.py":
continue
mod_path = os.path.join(actions_dir, fname)
mod_name = f"sos_actions_{fname[:-3]}"
spec = importlib.util.spec_from_file_location(mod_name, mod_path)
if spec is None or spec.loader is None:
logger.warning("Cannot load action module: %s", fname)
continue
try:
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
action_class = getattr(mod, "Action", None)
if action_class is None:
logger.warning("Action %s has no 'Action' class — skipping", fname)
continue
instance = action_class()
registry.register(instance)
logger.info("Loaded action: %s (%s)", instance.name, fname)
except Exception as e:
logger.warning("Failed to load action %s: %s", fname, e)
sys.path.pop(0)
return registry