114 lines
3.0 KiB
Python
114 lines
3.0 KiB
Python
|
|
"""
|
||
|
|
Python Runtime
|
||
|
|
|
||
|
|
Native Python tool execution runtime.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
import importlib.util
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any, Callable, Dict
|
||
|
|
|
||
|
|
from tools.runtime.base import BaseRuntime
|
||
|
|
|
||
|
|
|
||
|
|
class PythonRuntime(BaseRuntime):
|
||
|
|
"""Python runtime for executing Python-based tools"""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self._executors: Dict[str, Callable] = {}
|
||
|
|
self._modules: Dict[str, Any] = {}
|
||
|
|
|
||
|
|
def get_name(self) -> str:
|
||
|
|
return "python"
|
||
|
|
|
||
|
|
async def validate(self, entry: str) -> bool:
|
||
|
|
"""Validate Python tool entry point"""
|
||
|
|
path = Path(entry)
|
||
|
|
if not path.exists():
|
||
|
|
return False
|
||
|
|
if path.suffix != ".py":
|
||
|
|
return False
|
||
|
|
return True
|
||
|
|
|
||
|
|
async def execute(
|
||
|
|
self,
|
||
|
|
entry: str,
|
||
|
|
command: str,
|
||
|
|
parameters: Dict[str, Any],
|
||
|
|
timeout: int,
|
||
|
|
) -> Dict[str, Any]:
|
||
|
|
"""Execute a Python tool"""
|
||
|
|
try:
|
||
|
|
# Load module dynamically
|
||
|
|
module = self._load_module(entry, command)
|
||
|
|
if module is None:
|
||
|
|
return {
|
||
|
|
"status": "error",
|
||
|
|
"error": f"Failed to load module from {entry}",
|
||
|
|
}
|
||
|
|
|
||
|
|
# Get the execute function
|
||
|
|
if not hasattr(module, "execute"):
|
||
|
|
return {
|
||
|
|
"status": "error",
|
||
|
|
"error": "Module does not have 'execute' function",
|
||
|
|
}
|
||
|
|
|
||
|
|
execute_func = module.execute
|
||
|
|
|
||
|
|
# Run in executor to avoid blocking
|
||
|
|
loop = asyncio.get_event_loop()
|
||
|
|
result = await asyncio.wait_for(
|
||
|
|
loop.run_in_executor(
|
||
|
|
None,
|
||
|
|
lambda: execute_func(command, parameters),
|
||
|
|
),
|
||
|
|
timeout=timeout / 1000,
|
||
|
|
)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"result": result,
|
||
|
|
}
|
||
|
|
|
||
|
|
except asyncio.TimeoutError:
|
||
|
|
return {
|
||
|
|
"status": "error",
|
||
|
|
"error": f"Execution timed out after {timeout}ms",
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
return {
|
||
|
|
"status": "error",
|
||
|
|
"error": str(e),
|
||
|
|
}
|
||
|
|
|
||
|
|
def _load_module(self, entry: str, command: str) -> Any:
|
||
|
|
"""Load Python module from file path"""
|
||
|
|
cache_key = f"{entry}:{command}"
|
||
|
|
if cache_key in self._modules:
|
||
|
|
return self._modules[cache_key]
|
||
|
|
|
||
|
|
try:
|
||
|
|
path = Path(entry)
|
||
|
|
module_name = path.stem
|
||
|
|
|
||
|
|
spec = importlib.util.spec_from_file_location(module_name, entry)
|
||
|
|
if spec is None or spec.loader is None:
|
||
|
|
return None
|
||
|
|
|
||
|
|
module = importlib.util.module_from_spec(spec)
|
||
|
|
sys.modules[module_name] = module
|
||
|
|
spec.loader.exec_module(module)
|
||
|
|
|
||
|
|
self._modules[cache_key] = module
|
||
|
|
return module
|
||
|
|
|
||
|
|
except Exception:
|
||
|
|
return None
|
||
|
|
|
||
|
|
def clear_cache(self) -> None:
|
||
|
|
"""Clear module cache"""
|
||
|
|
self._modules.clear()
|