forked from OpenBMB/ChatDev
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfunction_manager.py
More file actions
executable file
·134 lines (108 loc) · 5.25 KB
/
function_manager.py
File metadata and controls
executable file
·134 lines (108 loc) · 5.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""Unified function management."""
import importlib.util
import inspect
import os
from pathlib import Path
from typing import Any, Callable, Dict, Optional
_MODULE_PREFIX = "_dynamic_functions"
_FUNCTION_CALLING_ENV = "MAC_FUNCTIONS_DIR"
_EDGE_FUNCTION_ENV = "MAC_EDGE_FUNCTIONS_DIR"
_EDGE_PROCESSOR_FUNCTION_ENV = "MAC_EDGE_PROCESSOR_FUNCTIONS_DIR"
_REPO_ROOT = Path(__file__).resolve().parents[1]
_DEFAULT_FUNCTIONS_ROOT = Path("functions")
_DEFAULT_FUNCTION_CALLING_DIR = _DEFAULT_FUNCTIONS_ROOT / "function_calling"
_DEFAULT_EDGE_FUNCTION_DIR = _DEFAULT_FUNCTIONS_ROOT / "edge"
_DEFAULT_EDGE_PROCESSOR_DIR = _DEFAULT_FUNCTIONS_ROOT / "edge_processor"
def _resolve_dir(default: Path, env_var: str | None = None) -> Path:
"""Resolve a directory path with optional environment override."""
override = os.environ.get(env_var) if env_var else None
if override:
return Path(override).expanduser()
if default.is_absolute():
return default
return _REPO_ROOT / default
FUNCTION_CALLING_DIR = _resolve_dir(_DEFAULT_FUNCTION_CALLING_DIR, _FUNCTION_CALLING_ENV).resolve()
EDGE_FUNCTION_DIR = _resolve_dir(_DEFAULT_EDGE_FUNCTION_DIR, _EDGE_FUNCTION_ENV).resolve()
EDGE_PROCESSOR_FUNCTION_DIR = _resolve_dir(_DEFAULT_EDGE_PROCESSOR_DIR, _EDGE_PROCESSOR_FUNCTION_ENV).resolve()
class FunctionManager:
"""Unified function manager for loading and managing functions across the project."""
def __init__(self, functions_dir: str | Path = "functions") -> None:
self.functions_dir = Path(functions_dir)
self.functions: Dict[str, Callable] = {}
self._loaded = False
def load_functions(self) -> None:
"""Load all Python functions from functions directory."""
if self._loaded:
return
if not self.functions_dir.exists():
raise ValueError(f"Functions directory does not exist: {self.functions_dir}")
for file in self.functions_dir.rglob("*.py"):
if file.name.startswith("_") or file.name == "__init__.py":
continue
if "__pycache__" in file.parts:
continue
module_name = self._build_module_name(file)
try:
# Import module dynamically
spec = importlib.util.spec_from_file_location(module_name, file)
if spec is None or spec.loader is None:
continue
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
current_file = file.resolve()
# Get all functions defined in the module
for name, obj in inspect.getmembers(module, inspect.isfunction):
if name.startswith("_"):
continue
# Only register functions defined in the current module/file
if getattr(obj, "__module__", None) != module.__name__:
code = getattr(obj, "__code__", None)
source_path = Path(code.co_filename).resolve() if code else None
if source_path != current_file:
continue
self.functions[name] = obj
except Exception as e:
print(f"Error loading module {module_name}: {e}")
self._loaded = True
def _build_module_name(self, filepath: Path) -> str:
"""Create a unique module name for a function file."""
relative = filepath.relative_to(self.functions_dir)
parts = "_".join(relative.with_suffix("").parts) or "module"
unique_suffix = f"{abs(hash(filepath.as_posix())) & 0xFFFFFFFF:X}"
return f"{_MODULE_PREFIX}.{parts}_{unique_suffix}"
def get_function(self, name: str) -> Optional[Callable]:
"""Get a function by name."""
if not self._loaded:
self.load_functions()
return self.functions.get(name)
def has_function(self, name: str) -> bool:
"""Check if a function exists."""
if not self._loaded:
self.load_functions()
return name in self.functions
def call_function(self, name: str, *args, **kwargs) -> Any:
"""Call a function by name with given arguments."""
func = self.get_function(name)
if func is None:
raise ValueError(f"Function {name} not found")
return func(*args, **kwargs)
def list_functions(self) -> Dict[str, Callable]:
"""List all available functions."""
if not self._loaded:
self.load_functions()
return self.functions.copy()
def reload_functions(self) -> None:
"""Reload all functions from the functions directory."""
self.functions.clear()
self._loaded = False
self.load_functions()
# Global function manager registry keyed by directory
_function_managers: Dict[Path, FunctionManager] = {}
def get_function_manager(functions_dir: str | Path) -> FunctionManager:
"""Get or create the global function manager instance for a directory."""
directory = Path(functions_dir).resolve()
manager = _function_managers.get(directory)
if manager is None:
manager = FunctionManager(directory)
_function_managers[directory] = manager
return manager