Added many tools
This commit is contained in:
@@ -13,12 +13,18 @@ WORKSPACE_ROOT = Path(os.getenv("WORKSPACE_ROOT", Path.cwd())).resolve()
|
|||||||
|
|
||||||
LOG_DIR = WORKSPACE_ROOT / "logs"
|
LOG_DIR = WORKSPACE_ROOT / "logs"
|
||||||
TMP_DIR = WORKSPACE_ROOT / "tmp"
|
TMP_DIR = WORKSPACE_ROOT / "tmp"
|
||||||
|
|
||||||
CONFIG_DIR = WORKSPACE_ROOT / "config"
|
CONFIG_DIR = WORKSPACE_ROOT / "config"
|
||||||
|
|
||||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
TMP_DIR.mkdir(parents=True, exist_ok=True)
|
TMP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# OLLAMA CONFIG
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
OLLAMA_URL = "http://localhost:11434"
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# SYSTEM LIMITS
|
# SYSTEM LIMITS
|
||||||
|
|||||||
2
main.py
2
main.py
@@ -167,7 +167,7 @@ def shutdown_gracefully() -> None:
|
|||||||
"""Clean shutdown of executor and other resources."""
|
"""Clean shutdown of executor and other resources."""
|
||||||
logger.info("Initiating graceful shutdown...")
|
logger.info("Initiating graceful shutdown...")
|
||||||
try:
|
try:
|
||||||
executor.shutdown()
|
executor.shutdown() # type: ignore[attr-defined]
|
||||||
logger.info("Executor shut down successfully.")
|
logger.info("Executor shut down successfully.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error during executor shutdown: {e}")
|
logger.error(f"Error during executor shutdown: {e}")
|
||||||
|
|||||||
1
memory_store.json
Normal file
1
memory_store.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
@@ -1,16 +1,36 @@
|
|||||||
"""
|
"""
|
||||||
Tool bootstrap.
|
Tool bootstrap module.
|
||||||
|
|
||||||
Importing this module forces
|
This package previously used explicit imports to force tool registration,
|
||||||
tool registration into registry.
|
but has since moved to a dynamic discovery system.
|
||||||
|
|
||||||
|
Current system behavior:
|
||||||
|
- tools are loaded via tools.discovery.load_all_tools()
|
||||||
|
- each tool self-registers into the global registry
|
||||||
|
- MCP bindings happen after registry population
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from tools.ping import PingTool
|
# ------------------------------------------------------------
|
||||||
from tools.filesystem import FilesystemTool
|
# LEGACY APPROACH (STATIC IMPORT REGISTRATION)
|
||||||
from tools.subprocess import SubprocessTool
|
# ------------------------------------------------------------
|
||||||
|
# These imports were previously used to force tool registration
|
||||||
|
# at import-time. This approach was replaced to support scalable
|
||||||
|
# plugin discovery via pkgutil-based loading.
|
||||||
|
#
|
||||||
|
# from tools.ping import PingTool
|
||||||
|
# from tools.filesystem import FilesystemTool
|
||||||
|
# from tools.subprocess import SubprocessTool
|
||||||
|
#
|
||||||
|
# __all__ = [
|
||||||
|
# "PingTool",
|
||||||
|
# "FilesystemTool",
|
||||||
|
# "SubprocessTool",
|
||||||
|
# ]
|
||||||
|
|
||||||
__all__ = [
|
# ------------------------------------------------------------
|
||||||
"PingTool",
|
# CURRENT DESIGN
|
||||||
"FilesystemTool",
|
# ------------------------------------------------------------
|
||||||
"SubprocessTool",
|
# Tool registration is now fully dynamic.
|
||||||
]
|
# See: tools.discovery.load_all_tools()
|
||||||
|
#
|
||||||
|
# No explicit imports are required here.
|
||||||
178
tools/agent_loop.py
Normal file
178
tools/agent_loop.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.events import bus
|
||||||
|
|
||||||
|
|
||||||
|
class AgentLoop:
|
||||||
|
"""
|
||||||
|
Minimal deterministic agent loop.
|
||||||
|
|
||||||
|
Orchestrates tools like:
|
||||||
|
- search
|
||||||
|
- intelligent_search
|
||||||
|
- crawler
|
||||||
|
- research
|
||||||
|
|
||||||
|
Can later be upgraded with LLM-based planning.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ctx):
|
||||||
|
self.ctx = ctx
|
||||||
|
self.max_steps = 5
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# ENTRY POINT
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def run(self, goal: str) -> dict[str, Any]:
|
||||||
|
state = {
|
||||||
|
"goal": goal,
|
||||||
|
"steps": [],
|
||||||
|
"memory": [],
|
||||||
|
"final": None
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"AGENT",
|
||||||
|
"agent_loop_start",
|
||||||
|
"INFO",
|
||||||
|
{"goal": goal}
|
||||||
|
)
|
||||||
|
|
||||||
|
for step in range(self.max_steps):
|
||||||
|
action = self._decide(state)
|
||||||
|
|
||||||
|
if action["type"] == "stop":
|
||||||
|
state["final"] = action.get("result")
|
||||||
|
break
|
||||||
|
|
||||||
|
result = self._execute(action, state)
|
||||||
|
state["steps"].append(action)
|
||||||
|
state["memory"].append(result)
|
||||||
|
|
||||||
|
# simple convergence heuristic
|
||||||
|
if self._is_satisfied(state):
|
||||||
|
state["final"] = result
|
||||||
|
break
|
||||||
|
|
||||||
|
if not state["final"]:
|
||||||
|
state["final"] = state["memory"][-1] if state["memory"] else {}
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# DECISION LOGIC (RULE-BASED FOR NOW)
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def _decide(self, state: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Very simple heuristic planner.
|
||||||
|
|
||||||
|
Later upgrade point: replace with LLM planner.
|
||||||
|
"""
|
||||||
|
|
||||||
|
goal = state["goal"]
|
||||||
|
memory = state["memory"]
|
||||||
|
|
||||||
|
if not memory:
|
||||||
|
return {
|
||||||
|
"type": "tool",
|
||||||
|
"tool": "research",
|
||||||
|
"input": {
|
||||||
|
"query": goal,
|
||||||
|
"depth": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
last = memory[-1]
|
||||||
|
|
||||||
|
# if research returned sources, refine or stop
|
||||||
|
if isinstance(last, dict) and "sources" in last:
|
||||||
|
if len(last["sources"]) >= 2:
|
||||||
|
return {
|
||||||
|
"type": "stop",
|
||||||
|
"result": last
|
||||||
|
}
|
||||||
|
|
||||||
|
# if weak results, deepen search
|
||||||
|
return {
|
||||||
|
"type": "tool",
|
||||||
|
"tool": "research",
|
||||||
|
"input": {
|
||||||
|
"query": goal,
|
||||||
|
"depth": 2,
|
||||||
|
"max_sources": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": "stop",
|
||||||
|
"result": last
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# TOOL EXECUTION
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def _execute(self, action: dict[str, Any], state: dict[str, Any]) -> Any:
|
||||||
|
tool_name = action.get("tool")
|
||||||
|
tool_input = action.get("input", {})
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"AGENT",
|
||||||
|
"agent_tool_call",
|
||||||
|
"INFO",
|
||||||
|
{
|
||||||
|
"tool": tool_name,
|
||||||
|
"input": tool_input
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not tool_name:
|
||||||
|
return {"error": "No tool specified"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = registry.run(
|
||||||
|
tool_name,
|
||||||
|
tool_input,
|
||||||
|
self.ctx
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"error": str(e),
|
||||||
|
"tool": tool_name
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# STOPPING CONDITION
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def _is_satisfied(self, state: dict[str, Any]) -> bool:
|
||||||
|
"""
|
||||||
|
Simple satisfaction heuristic.
|
||||||
|
|
||||||
|
Later upgrade: LLM-based evaluation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
memory = state["memory"]
|
||||||
|
|
||||||
|
if not memory:
|
||||||
|
return False
|
||||||
|
|
||||||
|
last = memory[-1]
|
||||||
|
|
||||||
|
if isinstance(last, dict):
|
||||||
|
# if research returned structured sources, assume OK
|
||||||
|
if "sources" in last and len(last["sources"]) > 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if "error" in last:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
141
tools/bochs.py
141
tools/bochs.py
@@ -0,0 +1,141 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.safety import safety
|
||||||
|
from core.events import bus
|
||||||
|
from core.subprocess import run_command
|
||||||
|
|
||||||
|
|
||||||
|
class BochsTool(BaseTool):
|
||||||
|
name = "bochs"
|
||||||
|
description = "Bochs emulator control (run, validate, debug, config execution)"
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# EXECUTE ROUTER
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"BOCHS",
|
||||||
|
"bochs_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "run":
|
||||||
|
return self.run_vm(payload)
|
||||||
|
|
||||||
|
case "validate":
|
||||||
|
return self.validate_config(payload)
|
||||||
|
|
||||||
|
case "debug":
|
||||||
|
return self.debug_vm(payload)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown bochs action: {action}")
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# HELPERS
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def _path(self, value: str) -> Path:
|
||||||
|
return safety.validate_path(value)
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# RUN VM
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def run_vm(self, payload: dict[str, Any]):
|
||||||
|
config = payload.get("config")
|
||||||
|
|
||||||
|
if not isinstance(config, str):
|
||||||
|
raise ValueError("config must be string")
|
||||||
|
|
||||||
|
config_path = self._path(config)
|
||||||
|
|
||||||
|
if not config_path.exists():
|
||||||
|
raise ValueError(f"Bochs config not found: {config}")
|
||||||
|
|
||||||
|
result = run_command(
|
||||||
|
cmd=["bochs", "-f", str(config_path), "-q"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "run",
|
||||||
|
"config": str(config_path),
|
||||||
|
"status": "success" if result.get("return_code") == 0 else "error",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# VALIDATE CONFIG
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def validate_config(self, payload: dict[str, Any]):
|
||||||
|
config = payload.get("config")
|
||||||
|
|
||||||
|
if not isinstance(config, str):
|
||||||
|
raise ValueError("config must be string")
|
||||||
|
|
||||||
|
config_path = self._path(config)
|
||||||
|
|
||||||
|
if not config_path.exists():
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": "config file not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bochs has no strict "validate" mode, so we simulate dry run parse
|
||||||
|
result = run_command(
|
||||||
|
cmd=["bochs", "-f", str(config_path), "-n"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "validate",
|
||||||
|
"config": str(config_path),
|
||||||
|
"status": "ok" if result.get("return_code") == 0 else "warning",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# DEBUG MODE
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def debug_vm(self, payload: dict[str, Any]):
|
||||||
|
config = payload.get("config")
|
||||||
|
|
||||||
|
if not isinstance(config, str):
|
||||||
|
raise ValueError("config must be string")
|
||||||
|
|
||||||
|
config_path = self._path(config)
|
||||||
|
|
||||||
|
if not config_path.exists():
|
||||||
|
raise ValueError("config not found")
|
||||||
|
|
||||||
|
result = run_command(
|
||||||
|
cmd=["bochs", "-f", str(config_path), "-q", "-debug"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "debug",
|
||||||
|
"config": str(config_path),
|
||||||
|
"status": "success" if result.get("return_code") == 0 else "error",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# REGISTER TOOL
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
registry.register(BochsTool())
|
||||||
188
tools/cmake.py
188
tools/cmake.py
@@ -0,0 +1,188 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.safety import safety
|
||||||
|
from core.events import bus
|
||||||
|
from core.subprocess import run_command
|
||||||
|
|
||||||
|
|
||||||
|
class CMakeTool(BaseTool):
|
||||||
|
name = "cmake"
|
||||||
|
description = "CMake build system operations (configure, build, generate, clean)"
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# EXECUTE ROUTER
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"CMAKE",
|
||||||
|
"cmake_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "configure":
|
||||||
|
return self.configure(payload)
|
||||||
|
|
||||||
|
case "build":
|
||||||
|
return self.build(payload)
|
||||||
|
|
||||||
|
case "clean":
|
||||||
|
return self.clean(payload)
|
||||||
|
|
||||||
|
case "generate":
|
||||||
|
return self.generate(payload)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown cmake action: {action}")
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# HELPERS
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def _path(self, value: str) -> Path:
|
||||||
|
return safety.validate_path(value)
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# CONFIGURE
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def configure(self, payload: dict[str, Any]):
|
||||||
|
source_dir = payload.get("source_dir", ".")
|
||||||
|
build_dir = payload.get("build_dir", "build")
|
||||||
|
generator = payload.get("generator") # optional
|
||||||
|
build_type = payload.get("build_type", "Release")
|
||||||
|
|
||||||
|
if not isinstance(source_dir, str):
|
||||||
|
raise ValueError("source_dir must be string")
|
||||||
|
if not isinstance(build_dir, str):
|
||||||
|
raise ValueError("build_dir must be string")
|
||||||
|
|
||||||
|
src = self._path(source_dir)
|
||||||
|
bld = self._path(build_dir)
|
||||||
|
|
||||||
|
bld.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"cmake",
|
||||||
|
"-S", str(src),
|
||||||
|
"-B", str(bld),
|
||||||
|
f"-DCMAKE_BUILD_TYPE={build_type}"
|
||||||
|
]
|
||||||
|
|
||||||
|
if generator:
|
||||||
|
if not isinstance(generator, str):
|
||||||
|
raise ValueError("generator must be string")
|
||||||
|
cmd.extend(["-G", generator])
|
||||||
|
|
||||||
|
result = run_command(cmd=cmd)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "configure",
|
||||||
|
"source_dir": str(src),
|
||||||
|
"build_dir": str(bld),
|
||||||
|
"status": "success" if result.get("return_code") == 0 else "error",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# BUILD
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def build(self, payload: dict[str, Any]):
|
||||||
|
build_dir = payload.get("build_dir", "build")
|
||||||
|
target = payload.get("target") # optional
|
||||||
|
jobs = payload.get("jobs", 0)
|
||||||
|
|
||||||
|
if not isinstance(build_dir, str):
|
||||||
|
raise ValueError("build_dir must be string")
|
||||||
|
|
||||||
|
bld = self._path(build_dir)
|
||||||
|
|
||||||
|
cmd = ["cmake", "--build", str(bld)]
|
||||||
|
|
||||||
|
if target:
|
||||||
|
if not isinstance(target, str):
|
||||||
|
raise ValueError("target must be string")
|
||||||
|
cmd.extend(["--target", target])
|
||||||
|
|
||||||
|
if isinstance(jobs, int) and jobs > 0:
|
||||||
|
cmd.extend(["--parallel", str(jobs)])
|
||||||
|
|
||||||
|
result = run_command(cmd=cmd)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "build",
|
||||||
|
"build_dir": str(bld),
|
||||||
|
"target": target,
|
||||||
|
"status": "success" if result.get("return_code") == 0 else "error",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# GENERATE (alias convenience)
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def generate(self, payload: dict[str, Any]):
|
||||||
|
# CMake modern workflow usually doesn't need this separately,
|
||||||
|
# but kept for explicit "generate-only" workflows.
|
||||||
|
return self.configure(payload)
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# CLEAN
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def clean(self, payload: dict[str, Any]):
|
||||||
|
build_dir = payload.get("build_dir", "build")
|
||||||
|
|
||||||
|
if not isinstance(build_dir, str):
|
||||||
|
raise ValueError("build_dir must be string")
|
||||||
|
|
||||||
|
bld = self._path(build_dir)
|
||||||
|
|
||||||
|
if not bld.exists():
|
||||||
|
return {
|
||||||
|
"action": "clean",
|
||||||
|
"status": "skipped",
|
||||||
|
"message": "build directory does not exist"
|
||||||
|
}
|
||||||
|
|
||||||
|
# safe clean: only remove cache artifacts, not full directory unless requested
|
||||||
|
cache_file = bld / "CMakeCache.txt"
|
||||||
|
|
||||||
|
removed = []
|
||||||
|
|
||||||
|
if cache_file.exists():
|
||||||
|
cache_file.unlink()
|
||||||
|
removed.append("CMakeCache.txt")
|
||||||
|
|
||||||
|
# optional: remove CMakeFiles
|
||||||
|
cmake_files = bld / "CMakeFiles"
|
||||||
|
if cmake_files.exists():
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(cmake_files)
|
||||||
|
removed.append("CMakeFiles/")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "clean",
|
||||||
|
"build_dir": str(bld),
|
||||||
|
"removed": removed,
|
||||||
|
"status": "ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# REGISTER
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
registry.register(CMakeTool())
|
||||||
194
tools/crawler.py
Normal file
194
tools/crawler.py
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import urljoin, urlparse
|
||||||
|
import urllib.request
|
||||||
|
import re
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.events import bus
|
||||||
|
|
||||||
|
|
||||||
|
class CrawlerTool(BaseTool):
|
||||||
|
"""
|
||||||
|
Lightweight safe web crawler.
|
||||||
|
|
||||||
|
Designed for:
|
||||||
|
- page fetching
|
||||||
|
- link extraction
|
||||||
|
- basic text scraping
|
||||||
|
- bounded crawling
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "crawler"
|
||||||
|
description = "Fetch and crawl web pages safely"
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# EXECUTE
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "fetch")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"CRAWLER",
|
||||||
|
"crawler_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "fetch":
|
||||||
|
return self.fetch(payload)
|
||||||
|
|
||||||
|
case "links":
|
||||||
|
return self.extract_links(payload)
|
||||||
|
|
||||||
|
case "crawl":
|
||||||
|
return self.crawl(payload)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown crawler action: {action}")
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# FETCH PAGE
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def fetch(self, payload: dict[str, Any]):
|
||||||
|
url = payload.get("url")
|
||||||
|
|
||||||
|
if not isinstance(url, str):
|
||||||
|
raise ValueError("url must be string")
|
||||||
|
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
headers={"User-Agent": "MCP-Crawler/1.0"}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=6) as resp:
|
||||||
|
html = resp.read().decode("utf-8", errors="ignore")
|
||||||
|
|
||||||
|
text = self._strip_html(html)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"url": url,
|
||||||
|
"text": text[:5000],
|
||||||
|
"length": len(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"url": url,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# EXTRACT LINKS
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def extract_links(self, payload: dict[str, Any]):
|
||||||
|
url = payload.get("url")
|
||||||
|
|
||||||
|
if not isinstance(url, str):
|
||||||
|
raise ValueError("url must be string")
|
||||||
|
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url)
|
||||||
|
with urllib.request.urlopen(req, timeout=6) as resp:
|
||||||
|
html = resp.read().decode("utf-8", errors="ignore")
|
||||||
|
|
||||||
|
links = re.findall(r'href=["\'](.*?)["\']', html)
|
||||||
|
|
||||||
|
normalized = []
|
||||||
|
for link in links:
|
||||||
|
normalized.append(urljoin(url, link))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"url": url,
|
||||||
|
"links": normalized[:200],
|
||||||
|
"count": len(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"url": url,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# SIMPLE CRAWL (DEPTH 1–2)
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def crawl(self, payload: dict[str, Any]):
|
||||||
|
start_url = payload.get("url")
|
||||||
|
depth = payload.get("depth", 1)
|
||||||
|
|
||||||
|
if not isinstance(start_url, str):
|
||||||
|
raise ValueError("url must be string")
|
||||||
|
if not isinstance(depth, int):
|
||||||
|
depth = 1
|
||||||
|
|
||||||
|
visited = set()
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def safe_fetch(u: str):
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(u)
|
||||||
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||||
|
return resp.read().decode("utf-8", errors="ignore")
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def crawl_url(url: str, d: int):
|
||||||
|
if d < 0 or url in visited:
|
||||||
|
return
|
||||||
|
|
||||||
|
visited.add(url)
|
||||||
|
|
||||||
|
html = safe_fetch(url)
|
||||||
|
text = self._strip_html(html)
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"url": url,
|
||||||
|
"text": text[:2000]
|
||||||
|
})
|
||||||
|
|
||||||
|
if d == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
links = re.findall(r'href=["\'](.*?)["\']', html)
|
||||||
|
for link in links[:20]:
|
||||||
|
full = urljoin(url, link)
|
||||||
|
|
||||||
|
# safety: stay same domain
|
||||||
|
if urlparse(full).netloc == urlparse(start_url).netloc:
|
||||||
|
crawl_url(full, d - 1)
|
||||||
|
|
||||||
|
crawl_url(start_url, depth)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"start": start_url,
|
||||||
|
"depth": depth,
|
||||||
|
"pages": results,
|
||||||
|
"count": len(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# HTML STRIPPER
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def _strip_html(self, html: str) -> str:
|
||||||
|
html = re.sub(r"<script.*?>.*?</script>", "", html, flags=re.S)
|
||||||
|
html = re.sub(r"<style.*?>.*?</style>", "", html, flags=re.S)
|
||||||
|
html = re.sub(r"<.*?>", " ", html)
|
||||||
|
html = re.sub(r"\s+", " ", html)
|
||||||
|
return html.strip()
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# REGISTER
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
registry.register(CrawlerTool())
|
||||||
@@ -2,9 +2,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
import pkgutil
|
import pkgutil
|
||||||
|
import logging
|
||||||
|
|
||||||
import tools as tools_pkg
|
import tools as tools_pkg
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def load_all_tools():
|
def load_all_tools():
|
||||||
"""
|
"""
|
||||||
@@ -12,11 +15,36 @@ def load_all_tools():
|
|||||||
|
|
||||||
Imports all modules inside /tools so they register
|
Imports all modules inside /tools so they register
|
||||||
themselves into the registry.
|
themselves into the registry.
|
||||||
|
|
||||||
|
Safe version:
|
||||||
|
- isolates import failures per module
|
||||||
|
- logs instead of crashing system boot
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for module in pkgutil.iter_modules(
|
loaded = 0
|
||||||
tools_pkg.__path__
|
failed = 0
|
||||||
):
|
|
||||||
importlib.import_module(
|
for module in pkgutil.iter_modules(tools_pkg.__path__):
|
||||||
f"tools.{module.name}"
|
module_name = f"tools.{module.name}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
importlib.import_module(module_name)
|
||||||
|
loaded += 1
|
||||||
|
|
||||||
|
logger.info(f"[TOOLS] Loaded: {module_name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
logger.exception(
|
||||||
|
f"[TOOLS] Failed to load {module_name}: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"[TOOLS] Discovery complete: loaded={loaded}, failed={failed}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"loaded": loaded,
|
||||||
|
"failed": failed
|
||||||
|
}
|
||||||
225
tools/docker.py
225
tools/docker.py
@@ -0,0 +1,225 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.events import bus
|
||||||
|
from core.subprocess import run_command
|
||||||
|
|
||||||
|
|
||||||
|
class DockerTool(BaseTool):
|
||||||
|
name = "docker"
|
||||||
|
description = "Docker container and image management"
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# EXECUTE ROUTER
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"DOCKER",
|
||||||
|
"docker_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "ps":
|
||||||
|
return self.ps(payload)
|
||||||
|
|
||||||
|
case "run":
|
||||||
|
return self.run_container(payload, ctx)
|
||||||
|
|
||||||
|
case "stop":
|
||||||
|
return self.stop_container(payload)
|
||||||
|
|
||||||
|
case "start":
|
||||||
|
return self.start_container(payload)
|
||||||
|
|
||||||
|
case "restart":
|
||||||
|
return self.restart_container(payload)
|
||||||
|
|
||||||
|
case "logs":
|
||||||
|
return self.logs(payload)
|
||||||
|
|
||||||
|
case "images":
|
||||||
|
return self.images(payload)
|
||||||
|
|
||||||
|
case "build":
|
||||||
|
return self.build_image(payload, ctx)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown docker action: {action}")
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# CONTAINERS LIST
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def ps(self, payload: dict[str, Any]):
|
||||||
|
all_containers = payload.get("all", False)
|
||||||
|
|
||||||
|
cmd = ["docker", "ps"]
|
||||||
|
if all_containers:
|
||||||
|
cmd.append("-a")
|
||||||
|
|
||||||
|
result = run_command(cmd=cmd)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "ps",
|
||||||
|
"status": "ok" if result.get("return_code") == 0 else "error",
|
||||||
|
"output": result.get("stdout", ""),
|
||||||
|
"error": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# RUN CONTAINER
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def run_container(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
image = payload.get("image")
|
||||||
|
name = payload.get("name")
|
||||||
|
args = payload.get("args", [])
|
||||||
|
|
||||||
|
if not isinstance(image, str):
|
||||||
|
raise ValueError("image must be string")
|
||||||
|
|
||||||
|
if not isinstance(args, list):
|
||||||
|
raise ValueError("args must be list")
|
||||||
|
|
||||||
|
cmd = ["docker", "run"]
|
||||||
|
|
||||||
|
if name:
|
||||||
|
if not isinstance(name, str):
|
||||||
|
raise ValueError("name must be string")
|
||||||
|
cmd += ["--name", name]
|
||||||
|
|
||||||
|
cmd += args
|
||||||
|
cmd.append(image)
|
||||||
|
|
||||||
|
if ctx.dry_run:
|
||||||
|
return {
|
||||||
|
"dry_run": True,
|
||||||
|
"command": cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
result = run_command(cmd=cmd)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "run",
|
||||||
|
"image": image,
|
||||||
|
"status": "ok" if result.get("return_code") == 0 else "error",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# STOP / START / RESTART
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def stop_container(self, payload: dict[str, Any]):
|
||||||
|
return self._simple_container_action("stop", payload)
|
||||||
|
|
||||||
|
def start_container(self, payload: dict[str, Any]):
|
||||||
|
return self._simple_container_action("start", payload)
|
||||||
|
|
||||||
|
def restart_container(self, payload: dict[str, Any]):
|
||||||
|
return self._simple_container_action("restart", payload)
|
||||||
|
|
||||||
|
def _simple_container_action(self, action: str, payload: dict[str, Any]):
|
||||||
|
container = payload.get("container")
|
||||||
|
|
||||||
|
if not isinstance(container, str):
|
||||||
|
raise ValueError("container must be string")
|
||||||
|
|
||||||
|
result = run_command(
|
||||||
|
cmd=["docker", action, container]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": action,
|
||||||
|
"container": container,
|
||||||
|
"status": "ok" if result.get("return_code") == 0 else "error",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# LOGS
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def logs(self, payload: dict[str, Any]):
|
||||||
|
container = payload.get("container")
|
||||||
|
tail = payload.get("tail", 100)
|
||||||
|
|
||||||
|
if not isinstance(container, str):
|
||||||
|
raise ValueError("container must be string")
|
||||||
|
|
||||||
|
cmd = ["docker", "logs", "--tail", str(tail), container]
|
||||||
|
|
||||||
|
result = run_command(cmd=cmd)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "logs",
|
||||||
|
"container": container,
|
||||||
|
"output": result.get("stdout", ""),
|
||||||
|
"error": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# IMAGES
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def images(self, payload: dict[str, Any]):
|
||||||
|
result = run_command(cmd=["docker", "images"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "images",
|
||||||
|
"output": result.get("stdout", ""),
|
||||||
|
"error": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# BUILD IMAGE
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def build_image(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
path = payload.get("path", ".")
|
||||||
|
tag = payload.get("tag")
|
||||||
|
|
||||||
|
if not isinstance(path, str):
|
||||||
|
raise ValueError("path must be string")
|
||||||
|
|
||||||
|
cmd = ["docker", "build", "-t"]
|
||||||
|
|
||||||
|
if isinstance(tag, str):
|
||||||
|
cmd.append(tag)
|
||||||
|
else:
|
||||||
|
cmd.append("untagged-image")
|
||||||
|
|
||||||
|
cmd.append(path)
|
||||||
|
|
||||||
|
if ctx.dry_run:
|
||||||
|
return {
|
||||||
|
"dry_run": True,
|
||||||
|
"command": cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
result = run_command(cmd=cmd)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "build",
|
||||||
|
"path": path,
|
||||||
|
"status": "ok" if result.get("return_code") == 0 else "error",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# REGISTER TOOL
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
registry.register(DockerTool())
|
||||||
254
tools/ffmpeg.py
254
tools/ffmpeg.py
@@ -0,0 +1,254 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.events import bus
|
||||||
|
from core.subprocess import run_command
|
||||||
|
|
||||||
|
|
||||||
|
class FFmpegTool(BaseTool):
|
||||||
|
name = "ffmpeg"
|
||||||
|
description = "Media processing using FFmpeg"
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# ROUTER
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"FFMPEG",
|
||||||
|
"ffmpeg_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "convert":
|
||||||
|
return self.convert(payload, ctx)
|
||||||
|
|
||||||
|
case "extract_audio":
|
||||||
|
return self.extract_audio(payload, ctx)
|
||||||
|
|
||||||
|
case "trim":
|
||||||
|
return self.trim(payload, ctx)
|
||||||
|
|
||||||
|
case "merge":
|
||||||
|
return self.merge(payload, ctx)
|
||||||
|
|
||||||
|
case "probe":
|
||||||
|
return self.probe(payload)
|
||||||
|
|
||||||
|
case "thumbnail":
|
||||||
|
return self.thumbnail(payload, ctx)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown ffmpeg action: {action}")
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# CONVERT
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def convert(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
input_file = payload.get("input")
|
||||||
|
output_file = payload.get("output")
|
||||||
|
codec = payload.get("codec") # optional
|
||||||
|
|
||||||
|
if not isinstance(input_file, str) or not isinstance(output_file, str):
|
||||||
|
raise ValueError("input/output must be strings")
|
||||||
|
|
||||||
|
cmd = ["ffmpeg", "-y", "-i", input_file]
|
||||||
|
|
||||||
|
if isinstance(codec, str):
|
||||||
|
cmd += ["-c:v", codec]
|
||||||
|
|
||||||
|
cmd.append(output_file)
|
||||||
|
|
||||||
|
if ctx.dry_run:
|
||||||
|
return {"dry_run": True, "command": cmd}
|
||||||
|
|
||||||
|
result = run_command(cmd=cmd)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "convert",
|
||||||
|
"status": "ok" if result.get("return_code") == 0 else "error",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# EXTRACT AUDIO
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def extract_audio(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
input_file = payload.get("input")
|
||||||
|
output_file = payload.get("output")
|
||||||
|
|
||||||
|
if not isinstance(input_file, str) or not isinstance(output_file, str):
|
||||||
|
raise ValueError("input/output must be strings")
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-y",
|
||||||
|
"-i", input_file,
|
||||||
|
"-vn",
|
||||||
|
"-acodec", "copy",
|
||||||
|
output_file
|
||||||
|
]
|
||||||
|
|
||||||
|
if ctx.dry_run:
|
||||||
|
return {"dry_run": True, "command": cmd}
|
||||||
|
|
||||||
|
result = run_command(cmd=cmd)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "extract_audio",
|
||||||
|
"status": "ok" if result.get("return_code") == 0 else "error",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# TRIM
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def trim(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
input_file = payload.get("input")
|
||||||
|
output_file = payload.get("output")
|
||||||
|
start = payload.get("start", "00:00:00")
|
||||||
|
duration = payload.get("duration")
|
||||||
|
|
||||||
|
if not isinstance(input_file, str) or not isinstance(output_file, str):
|
||||||
|
raise ValueError("input/output must be strings")
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-y",
|
||||||
|
"-i", input_file,
|
||||||
|
"-ss", str(start),
|
||||||
|
]
|
||||||
|
|
||||||
|
if duration:
|
||||||
|
cmd += ["-t", str(duration)]
|
||||||
|
|
||||||
|
cmd.append(output_file)
|
||||||
|
|
||||||
|
if ctx.dry_run:
|
||||||
|
return {"dry_run": True, "command": cmd}
|
||||||
|
|
||||||
|
result = run_command(cmd=cmd)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "trim",
|
||||||
|
"status": "ok" if result.get("return_code") == 0 else "error",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# MERGE FILES
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def merge(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
inputs = payload.get("inputs")
|
||||||
|
output = payload.get("output")
|
||||||
|
|
||||||
|
if not isinstance(inputs, list) or not isinstance(output, str):
|
||||||
|
raise ValueError("inputs must be list, output must be string")
|
||||||
|
|
||||||
|
# ffmpeg concat demuxer style
|
||||||
|
file_list = "ffmpeg_concat.txt"
|
||||||
|
with open(file_list, "w", encoding="utf-8") as f:
|
||||||
|
for item in inputs:
|
||||||
|
f.write(f"file '{item}'\n")
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-y",
|
||||||
|
"-f", "concat",
|
||||||
|
"-safe", "0",
|
||||||
|
"-i", file_list,
|
||||||
|
"-c", "copy",
|
||||||
|
output
|
||||||
|
]
|
||||||
|
|
||||||
|
if ctx.dry_run:
|
||||||
|
return {"dry_run": True, "command": cmd}
|
||||||
|
|
||||||
|
result = run_command(cmd=cmd)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "merge",
|
||||||
|
"status": "ok" if result.get("return_code") == 0 else "error",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# PROBE
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def probe(self, payload: dict[str, Any]):
|
||||||
|
input_file = payload.get("input")
|
||||||
|
|
||||||
|
if not isinstance(input_file, str):
|
||||||
|
raise ValueError("input must be string")
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"ffprobe",
|
||||||
|
"-v", "error",
|
||||||
|
"-show_format",
|
||||||
|
"-show_streams",
|
||||||
|
input_file
|
||||||
|
]
|
||||||
|
|
||||||
|
result = run_command(cmd=cmd)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "probe",
|
||||||
|
"data": result.get("stdout", ""),
|
||||||
|
"error": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# THUMBNAIL
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def thumbnail(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
input_file = payload.get("input")
|
||||||
|
output_file = payload.get("output")
|
||||||
|
time = payload.get("time", "00:00:01")
|
||||||
|
|
||||||
|
if not isinstance(input_file, str) or not isinstance(output_file, str):
|
||||||
|
raise ValueError("input/output must be strings")
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-y",
|
||||||
|
"-ss", str(time),
|
||||||
|
"-i", input_file,
|
||||||
|
"-vframes", "1",
|
||||||
|
output_file
|
||||||
|
]
|
||||||
|
|
||||||
|
if ctx.dry_run:
|
||||||
|
return {"dry_run": True, "command": cmd}
|
||||||
|
|
||||||
|
result = run_command(cmd=cmd)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "thumbnail",
|
||||||
|
"status": "ok" if result.get("return_code") == 0 else "error",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# REGISTER
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
registry.register(FFmpegTool())
|
||||||
@@ -13,146 +13,110 @@ class FilesystemTool(BaseTool):
|
|||||||
name = "filesystem"
|
name = "filesystem"
|
||||||
description = "Safe filesystem operations"
|
description = "Safe filesystem operations"
|
||||||
|
|
||||||
|
MAX_READ_BYTES = 5_000_000
|
||||||
|
MAX_LIST_ENTRIES = 5000
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# EXECUTE
|
# EXECUTE ROUTER
|
||||||
# =========================
|
# =========================
|
||||||
|
|
||||||
def execute(
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
self,
|
|
||||||
payload: dict[str, Any],
|
|
||||||
ctx: ToolContext
|
|
||||||
):
|
|
||||||
action = str(payload.get("action", "")).strip()
|
action = str(payload.get("action", "")).strip()
|
||||||
|
|
||||||
bus.log(
|
bus.log(
|
||||||
"FILESYSTEM",
|
"FILESYSTEM",
|
||||||
"filesystem_execute",
|
"filesystem_execute",
|
||||||
"INFO",
|
"INFO",
|
||||||
{
|
{"action": action}
|
||||||
"action": action
|
)
|
||||||
|
|
||||||
|
handlers = {
|
||||||
|
"read_file": self.read_file,
|
||||||
|
"write_file": self.write_file,
|
||||||
|
"list_dir": self.list_dir,
|
||||||
|
"exists": self.exists,
|
||||||
|
"mkdir": self.mkdir,
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
match action:
|
handler = handlers.get(action)
|
||||||
case "read_file":
|
if not handler:
|
||||||
return self.read_file(payload)
|
raise ValueError(f"Unknown filesystem action: {action}")
|
||||||
|
|
||||||
case "write_file":
|
return handler(payload, ctx)
|
||||||
return self.write_file(payload)
|
|
||||||
|
|
||||||
case "list_dir":
|
# =========================
|
||||||
return self.list_dir(payload)
|
# PATH HELPERS
|
||||||
|
# =========================
|
||||||
|
|
||||||
case "exists":
|
def _get_path(self, payload: dict[str, Any]) -> Path:
|
||||||
return self.exists(payload)
|
path_value = payload.get("path")
|
||||||
|
if not isinstance(path_value, str):
|
||||||
|
raise ValueError("path must be string")
|
||||||
|
|
||||||
case "mkdir":
|
return safety.validate_path(path_value)
|
||||||
return self.mkdir(payload)
|
|
||||||
|
|
||||||
case _:
|
|
||||||
raise ValueError(
|
|
||||||
f"Unknown filesystem action: {action}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# READ FILE
|
# READ FILE
|
||||||
# =========================
|
# =========================
|
||||||
|
|
||||||
def read_file(
|
def read_file(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
self,
|
path = self._get_path(payload)
|
||||||
payload: dict[str, Any]
|
|
||||||
):
|
|
||||||
path_value = payload.get("path")
|
|
||||||
|
|
||||||
if not isinstance(path_value, str):
|
data = path.read_text(encoding="utf-8")
|
||||||
raise ValueError("path must be string")
|
|
||||||
|
|
||||||
path = safety.validate_path(path_value)
|
if len(data.encode("utf-8")) > self.MAX_READ_BYTES:
|
||||||
|
raise ValueError("File exceeds read size limit")
|
||||||
|
|
||||||
return {
|
return {"path": str(path), "content": data}
|
||||||
"path": str(path),
|
|
||||||
"content": path.read_text(
|
|
||||||
encoding="utf-8"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# WRITE FILE
|
# WRITE FILE
|
||||||
# =========================
|
# =========================
|
||||||
|
|
||||||
def write_file(
|
def write_file(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
self,
|
path = self._get_path(payload)
|
||||||
payload: dict[str, Any]
|
|
||||||
):
|
|
||||||
path_value = payload.get("path")
|
|
||||||
content_value = payload.get("content")
|
|
||||||
|
|
||||||
if not isinstance(path_value, str):
|
content = payload.get("content")
|
||||||
raise ValueError("path must be string")
|
if not isinstance(content, str):
|
||||||
|
raise ValueError("content must be string")
|
||||||
|
|
||||||
if not isinstance(content_value, str):
|
safety.check_file_write(path, content)
|
||||||
raise ValueError(
|
|
||||||
"content must be string"
|
|
||||||
)
|
|
||||||
|
|
||||||
path = safety.validate_path(path_value)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
safety.check_file_write(
|
if path.exists():
|
||||||
path,
|
backup = path.with_suffix(path.suffix + ".bak")
|
||||||
content_value
|
try:
|
||||||
)
|
backup.write_text(path.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
path.parent.mkdir(
|
path.write_text(content, encoding="utf-8")
|
||||||
parents=True,
|
|
||||||
exist_ok=True
|
|
||||||
)
|
|
||||||
|
|
||||||
path.write_text(
|
|
||||||
content_value,
|
|
||||||
encoding="utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"path": str(path),
|
"path": str(path),
|
||||||
"bytes_written": len(
|
"bytes_written": len(content.encode("utf-8"))
|
||||||
content_value.encode("utf-8")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# LIST DIRECTORY
|
# LIST DIRECTORY
|
||||||
# =========================
|
# =========================
|
||||||
|
|
||||||
def list_dir(
|
def list_dir(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
self,
|
path = self._get_path(payload)
|
||||||
payload: dict[str, Any]
|
|
||||||
):
|
|
||||||
path_value = payload.get(
|
|
||||||
"path",
|
|
||||||
"."
|
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(path_value, str):
|
entries = []
|
||||||
raise ValueError(
|
for i, item in enumerate(path.iterdir()):
|
||||||
"path must be string"
|
if i >= self.MAX_LIST_ENTRIES:
|
||||||
)
|
break
|
||||||
|
|
||||||
path = safety.validate_path(
|
entries.append({
|
||||||
path_value
|
|
||||||
)
|
|
||||||
|
|
||||||
entries: list[dict[str, Any]] = []
|
|
||||||
|
|
||||||
for item in path.iterdir():
|
|
||||||
entries.append(
|
|
||||||
{
|
|
||||||
"name": item.name,
|
"name": item.name,
|
||||||
"path": str(item),
|
"path": str(item),
|
||||||
"is_dir": item.is_dir(),
|
"is_dir": item.is_dir(),
|
||||||
"is_file": item.is_file()
|
"is_file": item.is_file()
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"path": str(path),
|
"path": str(path),
|
||||||
@@ -164,20 +128,8 @@ class FilesystemTool(BaseTool):
|
|||||||
# EXISTS
|
# EXISTS
|
||||||
# =========================
|
# =========================
|
||||||
|
|
||||||
def exists(
|
def exists(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
self,
|
path = self._get_path(payload)
|
||||||
payload: dict[str, Any]
|
|
||||||
):
|
|
||||||
path_value = payload.get("path")
|
|
||||||
|
|
||||||
if not isinstance(path_value, str):
|
|
||||||
raise ValueError(
|
|
||||||
"path must be string"
|
|
||||||
)
|
|
||||||
|
|
||||||
path = safety.validate_path(
|
|
||||||
path_value
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"path": str(path),
|
"path": str(path),
|
||||||
@@ -190,25 +142,10 @@ class FilesystemTool(BaseTool):
|
|||||||
# MKDIR
|
# MKDIR
|
||||||
# =========================
|
# =========================
|
||||||
|
|
||||||
def mkdir(
|
def mkdir(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
self,
|
path = self._get_path(payload)
|
||||||
payload: dict[str, Any]
|
|
||||||
):
|
|
||||||
path_value = payload.get("path")
|
|
||||||
|
|
||||||
if not isinstance(path_value, str):
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
raise ValueError(
|
|
||||||
"path must be string"
|
|
||||||
)
|
|
||||||
|
|
||||||
path = safety.validate_path(
|
|
||||||
path_value
|
|
||||||
)
|
|
||||||
|
|
||||||
path.mkdir(
|
|
||||||
parents=True,
|
|
||||||
exist_ok=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@@ -216,10 +153,4 @@ class FilesystemTool(BaseTool):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
registry.register(FilesystemTool())
|
||||||
# SELF REGISTER
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
registry.register(
|
|
||||||
FilesystemTool()
|
|
||||||
)
|
|
||||||
@@ -79,7 +79,7 @@ class GiteaTool(BaseTool):
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, json=payload_data, headers=self.headers)
|
response = requests.post(url, json=payload_data, headers=self.headers, timeout=(3, 10))
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
@@ -97,7 +97,7 @@ class GiteaTool(BaseTool):
|
|||||||
url = f"{GITEA_URL}/api/v1/user/repos"
|
url = f"{GITEA_URL}/api/v1/user/repos"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, headers=self.headers)
|
response = requests.get(url, headers=self.headers, timeout=(3, 10))
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
repos = response.json()
|
repos = response.json()
|
||||||
return {
|
return {
|
||||||
@@ -124,7 +124,7 @@ class GiteaTool(BaseTool):
|
|||||||
url = f"{GITEA_URL}/api/v1/repos/{owner}/{repo}"
|
url = f"{GITEA_URL}/api/v1/repos/{owner}/{repo}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, headers=self.headers)
|
response = requests.get(url, headers=self.headers, timeout=(3, 10))
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
@@ -173,7 +173,7 @@ class GiteaTool(BaseTool):
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, json=payload_data, headers=self.headers)
|
response = requests.post(url, json=payload_data, headers=self.headers, timeout=(3, 10))
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
@@ -204,7 +204,7 @@ class GiteaTool(BaseTool):
|
|||||||
url = f"{GITEA_URL}/api/v1/repos/{owner}/{repo}/contents/{path}"
|
url = f"{GITEA_URL}/api/v1/repos/{owner}/{repo}/contents/{path}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, headers=self.headers)
|
response = requests.get(url, headers=self.headers, timeout=(3, 10))
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
|
|||||||
175
tools/gpu.py
Normal file
175
tools/gpu.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.events import bus
|
||||||
|
from core.subprocess import run_command
|
||||||
|
|
||||||
|
|
||||||
|
class GPUTool(BaseTool):
|
||||||
|
"""
|
||||||
|
GPU introspection tool.
|
||||||
|
|
||||||
|
Uses nvidia-smi when available.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "gpu"
|
||||||
|
description = "GPU usage, memory, and process inspection"
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# EXECUTE
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "info")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"GPU",
|
||||||
|
"gpu_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "info":
|
||||||
|
return self.gpu_info()
|
||||||
|
|
||||||
|
case "usage":
|
||||||
|
return self.gpu_usage()
|
||||||
|
|
||||||
|
case "processes":
|
||||||
|
return self.gpu_processes()
|
||||||
|
|
||||||
|
case "full":
|
||||||
|
return self.full_snapshot()
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown gpu action: {action}")
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# GPU INFO
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def gpu_info(self):
|
||||||
|
result = run_command(
|
||||||
|
cmd=[
|
||||||
|
"nvidia-smi",
|
||||||
|
"--query-gpu=name,driver_version,memory.total",
|
||||||
|
"--format=csv,noheader"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.get("return_code") != 0:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": result.get("stderr", "nvidia-smi not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = result.get("stdout", "").strip().splitlines()
|
||||||
|
|
||||||
|
gpus = []
|
||||||
|
for line in lines:
|
||||||
|
parts = [p.strip() for p in line.split(",")]
|
||||||
|
if len(parts) >= 3:
|
||||||
|
gpus.append({
|
||||||
|
"name": parts[0],
|
||||||
|
"driver": parts[1],
|
||||||
|
"memory_total": parts[2]
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"gpu_count": len(gpus),
|
||||||
|
"gpus": gpus
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# GPU USAGE
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def gpu_usage(self):
|
||||||
|
result = run_command(
|
||||||
|
cmd=[
|
||||||
|
"nvidia-smi",
|
||||||
|
"--query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu",
|
||||||
|
"--format=csv,noheader,nounits"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.get("return_code") != 0:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = result.get("stdout", "").strip().splitlines()
|
||||||
|
|
||||||
|
usage = []
|
||||||
|
for line in lines:
|
||||||
|
parts = [p.strip() for p in line.split(",")]
|
||||||
|
if len(parts) >= 4:
|
||||||
|
usage.append({
|
||||||
|
"gpu_util_percent": parts[0],
|
||||||
|
"memory_used_mb": parts[1],
|
||||||
|
"memory_total_mb": parts[2],
|
||||||
|
"temperature_c": parts[3]
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"gpus": usage
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# GPU PROCESSES
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def gpu_processes(self):
|
||||||
|
result = run_command(
|
||||||
|
cmd=[
|
||||||
|
"nvidia-smi",
|
||||||
|
"--query-compute-apps=pid,process_name,used_memory",
|
||||||
|
"--format=csv,noheader"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.get("return_code") != 0:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = result.get("stdout", "").strip().splitlines()
|
||||||
|
|
||||||
|
processes = []
|
||||||
|
for line in lines:
|
||||||
|
parts = [p.strip() for p in line.split(",")]
|
||||||
|
if len(parts) >= 3:
|
||||||
|
processes.append({
|
||||||
|
"pid": parts[0],
|
||||||
|
"name": parts[1],
|
||||||
|
"memory": parts[2]
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"count": len(processes),
|
||||||
|
"processes": processes
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# FULL SNAPSHOT
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def full_snapshot(self):
|
||||||
|
return {
|
||||||
|
"info": self.gpu_info(),
|
||||||
|
"usage": self.gpu_usage(),
|
||||||
|
"processes": self.gpu_processes()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# REGISTER
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
registry.register(GPUTool())
|
||||||
190
tools/info.py
Normal file
190
tools/info.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.events import bus
|
||||||
|
|
||||||
|
|
||||||
|
class InfoTool(BaseTool):
|
||||||
|
"""
|
||||||
|
Introspective tool for:
|
||||||
|
- tool discovery
|
||||||
|
- execution signature inspection
|
||||||
|
- structured payload hints
|
||||||
|
- example generation
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "info"
|
||||||
|
description = "Get tool schemas, args, and usage hints"
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# EXECUTE
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = payload.get("action", "list_tools")
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"INFO",
|
||||||
|
"info_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "list_tools":
|
||||||
|
return self.list_tools()
|
||||||
|
|
||||||
|
case "tool_schema":
|
||||||
|
return self.tool_schema(payload)
|
||||||
|
|
||||||
|
case "tool_catalog":
|
||||||
|
return self.tool_catalog()
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown info action: {action}")
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# LIST TOOLS
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def list_tools(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": t.name,
|
||||||
|
"description": getattr(t, "description", "")
|
||||||
|
}
|
||||||
|
for t in registry.all_tools()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# TOOL SCHEMA (CORE FEATURE)
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def tool_schema(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
name = payload.get("name")
|
||||||
|
|
||||||
|
if not isinstance(name, str):
|
||||||
|
raise ValueError("name must be string")
|
||||||
|
|
||||||
|
tool = next((t for t in registry.all_tools() if t.name == name), None)
|
||||||
|
|
||||||
|
if not tool:
|
||||||
|
return {"error": f"Tool not found: {name}"}
|
||||||
|
|
||||||
|
schema: dict[str, Any] = {
|
||||||
|
"name": tool.name,
|
||||||
|
"description": getattr(tool, "description", ""),
|
||||||
|
"class": tool.__class__.__name__,
|
||||||
|
"module": tool.__class__.__module__,
|
||||||
|
}
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# SAFE SIGNATURE INTROSPECTION
|
||||||
|
# =====================================================
|
||||||
|
try:
|
||||||
|
sig = inspect.signature(tool.execute)
|
||||||
|
|
||||||
|
params: dict[str, Any] = {}
|
||||||
|
|
||||||
|
for pname, param in sig.parameters.items():
|
||||||
|
if pname == "self":
|
||||||
|
continue
|
||||||
|
|
||||||
|
params[pname] = {
|
||||||
|
"required": param.default is inspect._empty,
|
||||||
|
"default": None if param.default is inspect._empty else param.default,
|
||||||
|
"kind": str(param.kind),
|
||||||
|
"type": (
|
||||||
|
param.annotation.__name__
|
||||||
|
if hasattr(param.annotation, "__name__")
|
||||||
|
else str(param.annotation)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
schema["execute_signature"] = params
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
schema["execute_signature_error"] = str(e)
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# STRUCTURED HINTS (NOT STRING PARSING)
|
||||||
|
# =====================================================
|
||||||
|
schema["common_payload_patterns"] = self._infer_patterns(tool)
|
||||||
|
schema["example_payload"] = self._generate_example_payload(tool)
|
||||||
|
|
||||||
|
return schema
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# TOOL CATALOG
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def tool_catalog(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"tool_count": len(registry.all_tools()),
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": t.name,
|
||||||
|
"description": getattr(t, "description", ""),
|
||||||
|
"patterns": self._infer_patterns(t),
|
||||||
|
}
|
||||||
|
for t in registry.all_tools()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# PATTERN INFERENCE (CLEANED UP)
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def _infer_patterns(self, tool: BaseTool) -> list[str]:
|
||||||
|
name = tool.name.lower()
|
||||||
|
|
||||||
|
mapping = {
|
||||||
|
"git": ["repo", "branch", "remote", "message"],
|
||||||
|
"filesystem": ["path", "content", "directory"],
|
||||||
|
"gitea": ["owner", "repo", "path", "message"],
|
||||||
|
"subprocess": ["cmd", "cwd", "timeout"],
|
||||||
|
"memory": ["entry", "query"],
|
||||||
|
"reflection": ["action"],
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping.get(name, [])
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# EXAMPLES
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def _generate_example_payload(self, tool: BaseTool) -> dict[str, Any]:
|
||||||
|
name = tool.name.lower()
|
||||||
|
|
||||||
|
examples = {
|
||||||
|
"git": {
|
||||||
|
"action": "status",
|
||||||
|
"repo": "."
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"action": "read_file",
|
||||||
|
"path": "./example.txt"
|
||||||
|
},
|
||||||
|
"gitea": {
|
||||||
|
"action": "list_repos"
|
||||||
|
},
|
||||||
|
"subprocess": {
|
||||||
|
"cmd": ["echo", "hello"],
|
||||||
|
"timeout": 60
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"action": "add",
|
||||||
|
"entry": {"note": "example"}
|
||||||
|
},
|
||||||
|
"reflection": {
|
||||||
|
"action": "reflect"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return examples.get(name, {"action": "unknown"})
|
||||||
183
tools/intelligent_search.py
Normal file
183
tools/intelligent_search.py
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.events import bus
|
||||||
|
|
||||||
|
|
||||||
|
class IntelligentSearchTool(BaseTool):
|
||||||
|
"""
|
||||||
|
Intelligent wrapper over basic search results.
|
||||||
|
|
||||||
|
Enhances:
|
||||||
|
- ranking
|
||||||
|
- deduplication
|
||||||
|
- best-result selection
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "intelligent_search"
|
||||||
|
description = "Rerank and filter search results for best relevance"
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# EXECUTE
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "rank")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"SEARCH",
|
||||||
|
"intelligent_search_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "rank":
|
||||||
|
return self.rank(payload)
|
||||||
|
|
||||||
|
case "best":
|
||||||
|
return self.best(payload)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown action: {action}")
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# RANK RESULTS
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def rank(self, payload: dict[str, Any]):
|
||||||
|
results = payload.get("results")
|
||||||
|
query = payload.get("query", "")
|
||||||
|
|
||||||
|
if not isinstance(results, list):
|
||||||
|
raise ValueError("results must be list")
|
||||||
|
|
||||||
|
scored = []
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
if not isinstance(r, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
title = r.get("title", "")
|
||||||
|
url = r.get("url", "")
|
||||||
|
|
||||||
|
score = self._score(query, title, url)
|
||||||
|
|
||||||
|
scored.append({
|
||||||
|
"title": title,
|
||||||
|
"url": url,
|
||||||
|
"score": score
|
||||||
|
})
|
||||||
|
|
||||||
|
scored.sort(key=lambda x: x["score"], reverse=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"query": query,
|
||||||
|
"ranked": scored
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# BEST RESULT ONLY
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def best(self, payload: dict[str, Any]):
|
||||||
|
results = payload.get("results")
|
||||||
|
query = payload.get("query", "")
|
||||||
|
|
||||||
|
if not isinstance(results, list):
|
||||||
|
raise ValueError("results must be list")
|
||||||
|
|
||||||
|
best_item = None
|
||||||
|
best_score = -1
|
||||||
|
|
||||||
|
seen_domains = set()
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
if not isinstance(r, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
title = r.get("title", "")
|
||||||
|
url = r.get("url", "")
|
||||||
|
|
||||||
|
domain = self._domain(url)
|
||||||
|
|
||||||
|
# simple dedupe
|
||||||
|
if domain in seen_domains:
|
||||||
|
continue
|
||||||
|
|
||||||
|
seen_domains.add(domain)
|
||||||
|
|
||||||
|
score = self._score(query, title, url)
|
||||||
|
|
||||||
|
if score > best_score:
|
||||||
|
best_score = score
|
||||||
|
best_item = {
|
||||||
|
"title": title,
|
||||||
|
"url": url,
|
||||||
|
"score": score
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"query": query,
|
||||||
|
"best": best_item
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# SCORING FUNCTION
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def _score(self, query: str, title: str, url: str) -> float:
|
||||||
|
"""
|
||||||
|
Lightweight heuristic ranking system.
|
||||||
|
|
||||||
|
Replace later with LLM scoring if desired.
|
||||||
|
"""
|
||||||
|
|
||||||
|
q = query.lower()
|
||||||
|
t = title.lower()
|
||||||
|
u = url.lower()
|
||||||
|
|
||||||
|
score = 0.0
|
||||||
|
|
||||||
|
# keyword overlap
|
||||||
|
for word in q.split():
|
||||||
|
if word in t:
|
||||||
|
score += 2.0
|
||||||
|
if word in u:
|
||||||
|
score += 1.0
|
||||||
|
|
||||||
|
# title boost
|
||||||
|
if q in t:
|
||||||
|
score += 5.0
|
||||||
|
|
||||||
|
# HTTPS boost
|
||||||
|
if url.startswith("https"):
|
||||||
|
score += 0.5
|
||||||
|
|
||||||
|
# domain quality heuristic
|
||||||
|
domain = self._domain(url)
|
||||||
|
if domain.endswith(".edu") or domain.endswith(".org"):
|
||||||
|
score += 1.5
|
||||||
|
|
||||||
|
return score
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# DOMAIN HELPERS
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def _domain(self, url: str) -> str:
|
||||||
|
try:
|
||||||
|
return urlparse(url).netloc.lower()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# REGISTER
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
registry.register(IntelligentSearchTool())
|
||||||
159
tools/memory.py
Normal file
159
tools/memory.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.events import bus
|
||||||
|
from core.config import WORKSPACE_ROOT
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryTool(BaseTool):
|
||||||
|
"""
|
||||||
|
Persistent memory store for agent experiences.
|
||||||
|
|
||||||
|
Stores:
|
||||||
|
- research results
|
||||||
|
- tool outputs
|
||||||
|
- agent decisions
|
||||||
|
- arbitrary notes
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "memory"
|
||||||
|
description = "Persistent memory storage and retrieval"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.memory_file = Path(WORKSPACE_ROOT) / "memory_store.json"
|
||||||
|
self._ensure_file()
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# EXECUTE
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "add")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"MEMORY",
|
||||||
|
"memory_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "add":
|
||||||
|
return self.add(payload)
|
||||||
|
|
||||||
|
case "search":
|
||||||
|
return self.search(payload)
|
||||||
|
|
||||||
|
case "list":
|
||||||
|
return self.list_all()
|
||||||
|
|
||||||
|
case "clear":
|
||||||
|
return self.clear()
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown memory action: {action}")
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# ADD MEMORY
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def add(self, payload: dict[str, Any]):
|
||||||
|
entry = payload.get("entry")
|
||||||
|
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
raise ValueError("entry must be dict")
|
||||||
|
|
||||||
|
memory = self._load()
|
||||||
|
|
||||||
|
record = {
|
||||||
|
"id": len(memory) + 1,
|
||||||
|
"timestamp": time.time(),
|
||||||
|
"entry": entry
|
||||||
|
}
|
||||||
|
|
||||||
|
memory.append(record)
|
||||||
|
self._save(memory)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"stored": record
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# SEARCH MEMORY
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def search(self, payload: dict[str, Any]):
|
||||||
|
query = payload.get("query", "")
|
||||||
|
|
||||||
|
if not isinstance(query, str):
|
||||||
|
raise ValueError("query must be string")
|
||||||
|
|
||||||
|
memory = self._load()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for item in memory:
|
||||||
|
entry = item.get("entry", {})
|
||||||
|
text_blob = json.dumps(entry).lower()
|
||||||
|
|
||||||
|
if query.lower() in text_blob:
|
||||||
|
results.append(item)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"query": query,
|
||||||
|
"results": results,
|
||||||
|
"count": len(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# LIST ALL
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def list_all(self):
|
||||||
|
return {
|
||||||
|
"memory": self._load()
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# CLEAR MEMORY
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self._save([])
|
||||||
|
return {"status": "cleared"}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# STORAGE LAYER
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def _ensure_file(self):
|
||||||
|
self.memory_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if not self.memory_file.exists():
|
||||||
|
self.memory_file.write_text("[]", encoding="utf-8")
|
||||||
|
|
||||||
|
def _load(self) -> list[dict[str, Any]]:
|
||||||
|
try:
|
||||||
|
return json.loads(self.memory_file.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _save(self, data: list[dict[str, Any]]):
|
||||||
|
self.memory_file.write_text(
|
||||||
|
json.dumps(data, indent=2),
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# REGISTER
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
registry.register(MemoryTool())
|
||||||
167
tools/net.py
Normal file
167
tools/net.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.events import bus
|
||||||
|
|
||||||
|
|
||||||
|
class NetTool(BaseTool):
|
||||||
|
"""
|
||||||
|
Network diagnostics and introspection tool.
|
||||||
|
|
||||||
|
Provides basic connectivity checks and resolution utilities.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "net"
|
||||||
|
description = "Network diagnostics: DNS, ports, HTTP checks"
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# EXECUTE
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "dns")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"NET",
|
||||||
|
"net_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "dns":
|
||||||
|
return self.dns_lookup(payload)
|
||||||
|
|
||||||
|
case "port":
|
||||||
|
return self.check_port(payload)
|
||||||
|
|
||||||
|
case "http":
|
||||||
|
return self.http_check(payload)
|
||||||
|
|
||||||
|
case "latency":
|
||||||
|
return self.latency_test(payload)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown net action: {action}")
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# DNS LOOKUP
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def dns_lookup(self, payload: dict[str, Any]):
|
||||||
|
host = payload.get("host")
|
||||||
|
|
||||||
|
if not isinstance(host, str):
|
||||||
|
raise ValueError("host must be string")
|
||||||
|
|
||||||
|
try:
|
||||||
|
ip = socket.gethostbyname(host)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"host": host,
|
||||||
|
"ip": ip
|
||||||
|
}
|
||||||
|
|
||||||
|
except socket.gaierror as e:
|
||||||
|
return {
|
||||||
|
"host": host,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# PORT CHECK
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def check_port(self, payload: dict[str, Any]):
|
||||||
|
host = payload.get("host", "127.0.0.1")
|
||||||
|
port = payload.get("port")
|
||||||
|
|
||||||
|
if not isinstance(host, str):
|
||||||
|
raise ValueError("host must be string")
|
||||||
|
if not isinstance(port, int):
|
||||||
|
raise ValueError("port must be int")
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(2)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = sock.connect_ex((host, port))
|
||||||
|
return {
|
||||||
|
"host": host,
|
||||||
|
"port": port,
|
||||||
|
"open": result == 0
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# HTTP CHECK
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def http_check(self, payload: dict[str, Any]):
|
||||||
|
url = payload.get("url")
|
||||||
|
|
||||||
|
if not isinstance(url, str):
|
||||||
|
raise ValueError("url must be string")
|
||||||
|
|
||||||
|
try:
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
method="GET"
|
||||||
|
)
|
||||||
|
|
||||||
|
with urllib.request.urlopen(req, timeout=5) as response:
|
||||||
|
status = response.getcode()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"url": url,
|
||||||
|
"status_code": status,
|
||||||
|
"latency_ms": round((time.time() - start) * 1000, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"url": url,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# LATENCY TEST
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def latency_test(self, payload: dict[str, Any]):
|
||||||
|
host = payload.get("host")
|
||||||
|
|
||||||
|
if not isinstance(host, str):
|
||||||
|
raise ValueError("host must be string")
|
||||||
|
|
||||||
|
try:
|
||||||
|
start = time.time()
|
||||||
|
socket.gethostbyname(host)
|
||||||
|
latency = (time.time() - start) * 1000
|
||||||
|
|
||||||
|
return {
|
||||||
|
"host": host,
|
||||||
|
"dns_latency_ms": round(latency, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"host": host,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# REGISTER
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
registry.register(NetTool())
|
||||||
158
tools/ollama.py
Normal file
158
tools/ollama.py
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.events import bus
|
||||||
|
from core.config import OLLAMA_URL
|
||||||
|
|
||||||
|
|
||||||
|
class OllamaTool(BaseTool):
|
||||||
|
"""
|
||||||
|
Local LLM interface via Ollama.
|
||||||
|
|
||||||
|
Enables the agent to call local models for reasoning,
|
||||||
|
summarization, and transformation tasks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "ollama"
|
||||||
|
description = "Local LLM inference via Ollama"
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# EXECUTE
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "generate")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"OLLAMA",
|
||||||
|
"ollama_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "generate":
|
||||||
|
return self.generate(payload, ctx)
|
||||||
|
|
||||||
|
case "chat":
|
||||||
|
return self.chat(payload, ctx)
|
||||||
|
|
||||||
|
case "models":
|
||||||
|
return self.list_models()
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown ollama action: {action}")
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# GENERATE (single prompt)
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def generate(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
model = payload.get("model", "llama3")
|
||||||
|
prompt = payload.get("prompt")
|
||||||
|
|
||||||
|
if not isinstance(prompt, str):
|
||||||
|
raise ValueError("prompt must be string")
|
||||||
|
|
||||||
|
url = f"{OLLAMA_URL}/api/generate"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"model": model,
|
||||||
|
"prompt": prompt,
|
||||||
|
"stream": False
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.dry_run:
|
||||||
|
return {
|
||||||
|
"dry_run": True,
|
||||||
|
"model": model,
|
||||||
|
"prompt_preview": prompt[:200]
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=data, timeout=(5, 120))
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"model": model,
|
||||||
|
"response": response.json().get("response", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# CHAT (multi-message style)
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def chat(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
model = payload.get("model", "llama3")
|
||||||
|
messages = payload.get("messages")
|
||||||
|
|
||||||
|
if not isinstance(messages, list):
|
||||||
|
raise ValueError("messages must be list[dict]")
|
||||||
|
|
||||||
|
url = f"{OLLAMA_URL}/api/chat"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"model": model,
|
||||||
|
"messages": messages,
|
||||||
|
"stream": False
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.dry_run:
|
||||||
|
return {
|
||||||
|
"dry_run": True,
|
||||||
|
"model": model,
|
||||||
|
"message_count": len(messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=data, timeout=(5, 180))
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"model": model,
|
||||||
|
"response": response.json().get("message", {}).get("content", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# LIST MODELS
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def list_models(self):
|
||||||
|
url = f"{OLLAMA_URL}/api/tags"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=(3, 10))
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"models": response.json().get("models", [])
|
||||||
|
}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# REGISTER TOOL
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
registry.register(OllamaTool())
|
||||||
166
tools/pid.py
Normal file
166
tools/pid.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.events import bus
|
||||||
|
from core.subprocess import run_command
|
||||||
|
|
||||||
|
|
||||||
|
class PidTool(BaseTool):
|
||||||
|
"""
|
||||||
|
Process inspection and management tool.
|
||||||
|
|
||||||
|
Provides visibility into running system processes and optional control.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "pid"
|
||||||
|
description = "Process listing, lookup, and management"
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# EXECUTE
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "list")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"PID",
|
||||||
|
"pid_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "list":
|
||||||
|
return self.list_processes(payload)
|
||||||
|
|
||||||
|
case "find":
|
||||||
|
return self.find_process(payload)
|
||||||
|
|
||||||
|
case "details":
|
||||||
|
return self.process_details(payload)
|
||||||
|
|
||||||
|
case "kill":
|
||||||
|
return self.kill_process(payload, ctx)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown pid action: {action}")
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# LIST PROCESSES
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def list_processes(self, payload: dict[str, Any]):
|
||||||
|
limit = payload.get("limit", 50)
|
||||||
|
|
||||||
|
result = run_command(
|
||||||
|
cmd=["tasklist"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.get("return_code") != 0:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = result.get("stdout", "").splitlines()
|
||||||
|
|
||||||
|
processes = []
|
||||||
|
for line in lines[3:]: # skip header rows
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) < 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
processes.append({
|
||||||
|
"name": parts[0],
|
||||||
|
"pid": parts[1]
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(processes) >= limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
return {
|
||||||
|
"count": len(processes),
|
||||||
|
"processes": processes
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# FIND PROCESS
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def find_process(self, payload: dict[str, Any]):
|
||||||
|
name = payload.get("name")
|
||||||
|
|
||||||
|
if not isinstance(name, str):
|
||||||
|
raise ValueError("name must be string")
|
||||||
|
|
||||||
|
result = run_command(
|
||||||
|
cmd=["tasklist", "/FI", f"IMAGENAME eq {name}"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"raw": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# PROCESS DETAILS
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def process_details(self, payload: dict[str, Any]):
|
||||||
|
pid = payload.get("pid")
|
||||||
|
|
||||||
|
if not isinstance(pid, int):
|
||||||
|
raise ValueError("pid must be int")
|
||||||
|
|
||||||
|
result = run_command(
|
||||||
|
cmd=["wmic", "process", "where", f"ProcessId={pid}", "get", "ProcessId,Name,CommandLine"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pid": pid,
|
||||||
|
"raw": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# KILL PROCESS
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def kill_process(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
pid = payload.get("pid")
|
||||||
|
force = payload.get("force", False)
|
||||||
|
|
||||||
|
if not isinstance(pid, int):
|
||||||
|
raise ValueError("pid must be int")
|
||||||
|
|
||||||
|
if ctx.dry_run:
|
||||||
|
return {
|
||||||
|
"dry_run": True,
|
||||||
|
"pid": pid,
|
||||||
|
"force": force,
|
||||||
|
"message": "Would terminate process"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = ["taskkill", "/PID", str(pid)]
|
||||||
|
if force:
|
||||||
|
cmd.append("/F")
|
||||||
|
|
||||||
|
result = run_command(cmd)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pid": pid,
|
||||||
|
"status": "success" if result.get("return_code") == 0 else "error",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# REGISTER TOOL
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
registry.register(PidTool())
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
import time
|
||||||
|
|
||||||
from core.tools.base import BaseTool, ToolContext
|
from core.tools.base import BaseTool, ToolContext
|
||||||
from core.tools.registry import registry
|
from core.tools.registry import registry
|
||||||
from core.events import bus
|
from core.events import bus
|
||||||
@@ -7,19 +10,13 @@ from core.events import bus
|
|||||||
|
|
||||||
class PingTool(BaseTool):
|
class PingTool(BaseTool):
|
||||||
name = "ping"
|
name = "ping"
|
||||||
|
description = "Health check / liveness probe"
|
||||||
# optional metadata (future MCP auto-binding)
|
|
||||||
description = "Health check"
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# EXECUTE
|
|
||||||
# -------------------------
|
|
||||||
|
|
||||||
def execute(
|
def execute(
|
||||||
self,
|
self,
|
||||||
payload: dict[str, str],
|
payload: dict[str, Any],
|
||||||
ctx: ToolContext
|
ctx: ToolContext
|
||||||
):
|
) -> dict[str, Any]:
|
||||||
message = payload.get("message", "pong")
|
message = payload.get("message", "pong")
|
||||||
|
|
||||||
bus.log(
|
bus.log(
|
||||||
@@ -34,12 +31,9 @@ class PingTool(BaseTool):
|
|||||||
return {
|
return {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"echo": message,
|
"echo": message,
|
||||||
"tool": self.name
|
"tool": self.name,
|
||||||
|
"timestamp": time.time()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# SELF REGISTER
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
registry.register(PingTool())
|
registry.register(PingTool())
|
||||||
136
tools/pwsh.py
Normal file
136
tools/pwsh.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.events import bus
|
||||||
|
from core.subprocess import run_command
|
||||||
|
|
||||||
|
|
||||||
|
class PwshTool(BaseTool):
|
||||||
|
"""
|
||||||
|
PowerShell execution tool.
|
||||||
|
|
||||||
|
Provides controlled execution of PowerShell commands and scripts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "pwsh"
|
||||||
|
description = "Execute PowerShell commands safely"
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# ROUTER
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "run")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"PWSH",
|
||||||
|
"pwsh_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "run":
|
||||||
|
return self.run(payload, ctx)
|
||||||
|
|
||||||
|
case "script":
|
||||||
|
return self.run_script(payload, ctx)
|
||||||
|
|
||||||
|
case "check":
|
||||||
|
return self.check_pwsh(payload)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown pwsh action: {action}")
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# RUN COMMAND
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def run(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
command = payload.get("command")
|
||||||
|
cwd = payload.get("cwd")
|
||||||
|
|
||||||
|
if not isinstance(command, str):
|
||||||
|
raise ValueError("command must be string")
|
||||||
|
|
||||||
|
if cwd is not None and not isinstance(cwd, str):
|
||||||
|
raise ValueError("cwd must be string")
|
||||||
|
|
||||||
|
cmd = ["pwsh", "-NoProfile", "-Command", command]
|
||||||
|
|
||||||
|
if ctx.dry_run:
|
||||||
|
return {
|
||||||
|
"dry_run": True,
|
||||||
|
"command": cmd,
|
||||||
|
"cwd": cwd
|
||||||
|
}
|
||||||
|
|
||||||
|
result = run_command(
|
||||||
|
cmd=cmd,
|
||||||
|
cwd=cwd
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "run",
|
||||||
|
"status": "ok" if result.get("return_code") == 0 else "error",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# RUN SCRIPT
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def run_script(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
script = payload.get("script")
|
||||||
|
cwd = payload.get("cwd")
|
||||||
|
|
||||||
|
if not isinstance(script, str):
|
||||||
|
raise ValueError("script must be string")
|
||||||
|
|
||||||
|
cmd = ["pwsh", "-NoProfile", "-Command", script]
|
||||||
|
|
||||||
|
if ctx.dry_run:
|
||||||
|
return {
|
||||||
|
"dry_run": True,
|
||||||
|
"script_preview": script[:500]
|
||||||
|
}
|
||||||
|
|
||||||
|
result = run_command(
|
||||||
|
cmd=cmd,
|
||||||
|
cwd=cwd
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "script",
|
||||||
|
"status": "ok" if result.get("return_code") == 0 else "error",
|
||||||
|
"stdout": result.get("stdout", ""),
|
||||||
|
"stderr": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# CHECK POWERSHELL
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def check_pwsh(self, payload: dict[str, Any]):
|
||||||
|
"""Check if PowerShell is available."""
|
||||||
|
result = run_command(
|
||||||
|
cmd=["pwsh", "-Command", "$PSVersionTable.PSVersion"]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action": "check",
|
||||||
|
"available": result.get("return_code") == 0,
|
||||||
|
"version_output": result.get("stdout", ""),
|
||||||
|
"error": result.get("stderr", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# REGISTER TOOL
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
registry.register(PwshTool())
|
||||||
155
tools/qemu.py
155
tools/qemu.py
@@ -0,0 +1,155 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.safety import safety
|
||||||
|
from core.events import bus
|
||||||
|
|
||||||
|
|
||||||
|
VM_STATE_FILE = Path("./vm_state.json")
|
||||||
|
|
||||||
|
|
||||||
|
class QemuTool(BaseTool):
|
||||||
|
name = "qemu"
|
||||||
|
description = "QEMU VM lifecycle management"
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# EXECUTE ROUTER
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"QEMU",
|
||||||
|
"qemu_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "start":
|
||||||
|
return self.start_vm(payload, ctx)
|
||||||
|
|
||||||
|
case "stop":
|
||||||
|
return self.stop_vm(payload, ctx)
|
||||||
|
|
||||||
|
case "status":
|
||||||
|
return self.status(payload)
|
||||||
|
|
||||||
|
case "list":
|
||||||
|
return self.list_vms()
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown qemu action: {action}")
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# STATE HELPERS
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
def _load_state(self) -> dict[str, Any]:
|
||||||
|
if VM_STATE_FILE.exists():
|
||||||
|
return json.loads(VM_STATE_FILE.read_text())
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _save_state(self, state: dict[str, Any]):
|
||||||
|
VM_STATE_FILE.write_text(json.dumps(state, indent=2))
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# START VM
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
def start_vm(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
name = payload.get("name")
|
||||||
|
image = payload.get("image")
|
||||||
|
|
||||||
|
if not isinstance(name, str):
|
||||||
|
raise ValueError("name must be string")
|
||||||
|
if not isinstance(image, str):
|
||||||
|
raise ValueError("image must be string")
|
||||||
|
|
||||||
|
image_path = safety.validate_path(image)
|
||||||
|
|
||||||
|
state = self._load_state()
|
||||||
|
|
||||||
|
if name in state and state[name].get("running"):
|
||||||
|
return {"status": "already_running", "name": name}
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"qemu-system-x86_64",
|
||||||
|
"-m", "2048",
|
||||||
|
"-drive", f"file={image_path},format=qcow2",
|
||||||
|
"-nographic"
|
||||||
|
]
|
||||||
|
|
||||||
|
process = subprocess.Popen(cmd)
|
||||||
|
|
||||||
|
state[name] = {
|
||||||
|
"pid": process.pid,
|
||||||
|
"image": str(image_path),
|
||||||
|
"running": True
|
||||||
|
}
|
||||||
|
|
||||||
|
self._save_state(state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "started",
|
||||||
|
"name": name,
|
||||||
|
"pid": process.pid
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# STOP VM
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
def stop_vm(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
name = payload.get("name")
|
||||||
|
|
||||||
|
if not isinstance(name, str):
|
||||||
|
raise ValueError("name must be string")
|
||||||
|
|
||||||
|
state = self._load_state()
|
||||||
|
|
||||||
|
vm = state.get(name)
|
||||||
|
if not vm or not vm.get("running"):
|
||||||
|
return {"status": "not_running", "name": name}
|
||||||
|
|
||||||
|
pid = vm["pid"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(["kill", str(pid)], check=False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
vm["running"] = False
|
||||||
|
self._save_state(state)
|
||||||
|
|
||||||
|
return {"status": "stopped", "name": name, "pid": pid}
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# STATUS
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
def status(self, payload: dict[str, Any]):
|
||||||
|
name = payload.get("name")
|
||||||
|
|
||||||
|
if not isinstance(name, str):
|
||||||
|
raise ValueError("name must be string")
|
||||||
|
|
||||||
|
state = self._load_state()
|
||||||
|
return state.get(name, {"status": "unknown"})
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# LIST
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
def list_vms(self):
|
||||||
|
return self._load_state()
|
||||||
|
|
||||||
|
|
||||||
|
registry.register(QemuTool())
|
||||||
177
tools/reflection.py
Normal file
177
tools/reflection.py
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.events import bus
|
||||||
|
|
||||||
|
from tools.memory import MemoryTool
|
||||||
|
|
||||||
|
|
||||||
|
class ReflectionTool(BaseTool):
|
||||||
|
"""
|
||||||
|
Analyzes stored memory and extracts insights.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "reflection"
|
||||||
|
description = "Analyze memory and extract insights or patterns"
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# EXECUTE
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "reflect")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"REFLECTION",
|
||||||
|
"reflection_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "reflect":
|
||||||
|
return self.reflect(payload, ctx)
|
||||||
|
|
||||||
|
case "summarize_failures":
|
||||||
|
return self.summarize_failures(payload, ctx)
|
||||||
|
|
||||||
|
case "summarize_successes":
|
||||||
|
return self.summarize_successes(payload, ctx)
|
||||||
|
|
||||||
|
case "detect_loops":
|
||||||
|
return self.detect_loops(payload, ctx)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown reflection action: {action}")
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# CORE REFLECTION
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def reflect(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
memory_tool = self._get_memory()
|
||||||
|
memory = memory_tool._load()
|
||||||
|
|
||||||
|
insights = []
|
||||||
|
|
||||||
|
for item in memory:
|
||||||
|
entry = item.get("entry", {})
|
||||||
|
text = json.dumps(entry).lower()
|
||||||
|
|
||||||
|
if "error" in text or "failed" in text:
|
||||||
|
insights.append({
|
||||||
|
"type": "failure_pattern",
|
||||||
|
"id": item.get("id"),
|
||||||
|
"note": "Failure-related memory detected"
|
||||||
|
})
|
||||||
|
|
||||||
|
if "success" in text or "ok" in text:
|
||||||
|
insights.append({
|
||||||
|
"type": "success_pattern",
|
||||||
|
"id": item.get("id"),
|
||||||
|
"note": "Success-related memory detected"
|
||||||
|
})
|
||||||
|
|
||||||
|
reflection = {
|
||||||
|
"timestamp": time.time(),
|
||||||
|
"insights": insights,
|
||||||
|
"total_memory": len(memory),
|
||||||
|
"insight_count": len(insights)
|
||||||
|
}
|
||||||
|
|
||||||
|
# store reflection back into memory
|
||||||
|
memory_tool.add({
|
||||||
|
"entry": {
|
||||||
|
"type": "reflection",
|
||||||
|
"data": reflection
|
||||||
|
}
|
||||||
|
}, ctx) # type: ignore
|
||||||
|
|
||||||
|
return reflection
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# FAILURE ANALYSIS
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def summarize_failures(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
memory_tool = self._get_memory()
|
||||||
|
memory = memory_tool._load()
|
||||||
|
|
||||||
|
failures = [
|
||||||
|
item for item in memory
|
||||||
|
if "error" in json.dumps(item.get("entry", {})).lower()
|
||||||
|
or "fail" in json.dumps(item.get("entry", {})).lower()
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"count": len(failures),
|
||||||
|
"failures": failures
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# SUCCESS ANALYSIS
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def summarize_successes(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
memory_tool = self._get_memory()
|
||||||
|
memory = memory_tool._load()
|
||||||
|
|
||||||
|
successes = [
|
||||||
|
item for item in memory
|
||||||
|
if "success" in json.dumps(item.get("entry", {})).lower()
|
||||||
|
or "ok" in json.dumps(item.get("entry", {})).lower()
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"count": len(successes),
|
||||||
|
"successes": successes
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# LOOP DETECTION
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def detect_loops(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
memory_tool = self._get_memory()
|
||||||
|
memory = memory_tool._load()
|
||||||
|
|
||||||
|
seen = {}
|
||||||
|
loops = []
|
||||||
|
|
||||||
|
for item in memory:
|
||||||
|
key = json.dumps(item.get("entry", {}), sort_keys=True)
|
||||||
|
|
||||||
|
if key in seen:
|
||||||
|
loops.append({
|
||||||
|
"original_id": seen[key],
|
||||||
|
"duplicate_id": item.get("id")
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
seen[key] = item.get("id")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"loop_count": len(loops),
|
||||||
|
"loops": loops
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# HELPERS
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def _get_memory(self) -> MemoryTool:
|
||||||
|
for tool in registry.all_tools():
|
||||||
|
if tool.name == "memory":
|
||||||
|
return tool # type: ignore
|
||||||
|
raise RuntimeError("Memory tool not found")
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# REGISTER
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
registry.register(ReflectionTool())
|
||||||
149
tools/research.py
Normal file
149
tools/research.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.events import bus
|
||||||
|
|
||||||
|
|
||||||
|
class ResearchTool(BaseTool):
|
||||||
|
"""
|
||||||
|
High-level research orchestrator.
|
||||||
|
|
||||||
|
Combines:
|
||||||
|
- search
|
||||||
|
- intelligent ranking
|
||||||
|
- crawling
|
||||||
|
into a structured report.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "research"
|
||||||
|
description = "Autonomous web research pipeline"
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# EXECUTE
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
query = payload.get("query")
|
||||||
|
depth = payload.get("depth", 1)
|
||||||
|
max_sources = payload.get("max_sources", 3)
|
||||||
|
|
||||||
|
if not isinstance(query, str):
|
||||||
|
raise ValueError("query must be string")
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"RESEARCH",
|
||||||
|
"research_execute",
|
||||||
|
"INFO",
|
||||||
|
{
|
||||||
|
"query": query,
|
||||||
|
"depth": depth,
|
||||||
|
"max_sources": max_sources
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 1: search
|
||||||
|
search_results = registry.run(
|
||||||
|
"search",
|
||||||
|
{"action": "search", "query": query, "limit": 10},
|
||||||
|
ctx
|
||||||
|
)
|
||||||
|
|
||||||
|
results = search_results.get("results", [])
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
return {
|
||||||
|
"query": query,
|
||||||
|
"error": "No search results found"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 2: intelligent ranking
|
||||||
|
ranked = registry.run(
|
||||||
|
"intelligent_search",
|
||||||
|
{
|
||||||
|
"action": "rank",
|
||||||
|
"query": query,
|
||||||
|
"results": results
|
||||||
|
},
|
||||||
|
ctx
|
||||||
|
)
|
||||||
|
|
||||||
|
ranked_list = ranked.get("ranked", [])[:max_sources]
|
||||||
|
|
||||||
|
# Step 3: crawl top sources
|
||||||
|
pages = []
|
||||||
|
|
||||||
|
for item in ranked_list:
|
||||||
|
url = item.get("url")
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
continue
|
||||||
|
|
||||||
|
page = registry.run(
|
||||||
|
"crawler",
|
||||||
|
{
|
||||||
|
"action": "fetch",
|
||||||
|
"url": url
|
||||||
|
},
|
||||||
|
ctx
|
||||||
|
)
|
||||||
|
|
||||||
|
pages.append({
|
||||||
|
"url": url,
|
||||||
|
"title": item.get("title"),
|
||||||
|
"text": page.get("text", ""),
|
||||||
|
"score": item.get("score", 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Step 4: synthesize structure
|
||||||
|
return {
|
||||||
|
"query": query,
|
||||||
|
"sources_used": len(pages),
|
||||||
|
"sources": pages,
|
||||||
|
"summary_hint": self._build_hint(pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# SIMPLE SYNTHESIS HELPER
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def _build_hint(self, pages: list[dict[str, Any]]) -> str:
|
||||||
|
"""
|
||||||
|
Lightweight heuristic summary hint.
|
||||||
|
|
||||||
|
This is NOT a full LLM summary — just structure guidance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not pages:
|
||||||
|
return "No data available."
|
||||||
|
|
||||||
|
topics = []
|
||||||
|
|
||||||
|
for p in pages:
|
||||||
|
text = p.get("text", "")
|
||||||
|
|
||||||
|
# crude keyword extraction (lightweight, no deps)
|
||||||
|
words = text.split()
|
||||||
|
keywords = [w for w in words if len(w) > 6][:10]
|
||||||
|
|
||||||
|
topics.append({
|
||||||
|
"url": p.get("url"),
|
||||||
|
"keywords": keywords
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
"Key extracted themes per source:\n"
|
||||||
|
+ "\n".join(
|
||||||
|
f"- {t['url']}: {', '.join(t['keywords'][:5])}"
|
||||||
|
for t in topics
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# REGISTER
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
registry.register(ResearchTool())
|
||||||
117
tools/search.py
Normal file
117
tools/search.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import re
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.events import bus
|
||||||
|
|
||||||
|
|
||||||
|
class SearchTool(BaseTool):
|
||||||
|
"""
|
||||||
|
Lightweight web search tool using DuckDuckGo HTML endpoint.
|
||||||
|
|
||||||
|
Designed for:
|
||||||
|
- query → results
|
||||||
|
- agent retrieval step before crawling
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "search"
|
||||||
|
description = "Web search (DuckDuckGo HTML scraping)"
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# EXECUTE
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "search")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"SEARCH",
|
||||||
|
"search_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "search":
|
||||||
|
return self.search(payload)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown search action: {action}")
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# SEARCH
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def search(self, payload: dict[str, Any]):
|
||||||
|
query = payload.get("query")
|
||||||
|
limit = payload.get("limit", 5)
|
||||||
|
|
||||||
|
if not isinstance(query, str):
|
||||||
|
raise ValueError("query must be string")
|
||||||
|
if not isinstance(limit, int):
|
||||||
|
limit = 5
|
||||||
|
|
||||||
|
encoded = urllib.parse.quote(query)
|
||||||
|
|
||||||
|
url = f"https://duckduckgo.com/html/?q={encoded}"
|
||||||
|
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
headers={
|
||||||
|
"User-Agent": "MCP-Search/1.0"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=6) as resp:
|
||||||
|
html = resp.read().decode("utf-8", errors="ignore")
|
||||||
|
|
||||||
|
results = self._parse_results(html)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"query": query,
|
||||||
|
"results": results[:limit],
|
||||||
|
"count": len(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"query": query,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# PARSER
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def _parse_results(self, html: str) -> list[dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
DuckDuckGo HTML parsing (lightweight heuristic).
|
||||||
|
"""
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Extract result blocks
|
||||||
|
links = re.findall(r'<a rel="nofollow" class="result__a" href="(.*?)".*?>(.*?)</a>', html)
|
||||||
|
|
||||||
|
for url, title in links:
|
||||||
|
clean_title = re.sub("<.*?>", "", title)
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"title": clean_title,
|
||||||
|
"url": url,
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# REGISTER
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
registry.register(SearchTool())
|
||||||
@@ -5,50 +5,59 @@ from typing import Any
|
|||||||
from core.subprocess import run_command
|
from core.subprocess import run_command
|
||||||
from core.tools.base import BaseTool, ToolContext
|
from core.tools.base import BaseTool, ToolContext
|
||||||
from core.tools.registry import registry
|
from core.tools.registry import registry
|
||||||
|
from core.safety import safety
|
||||||
|
|
||||||
|
|
||||||
class SubprocessTool(BaseTool):
|
class SubprocessTool(BaseTool):
|
||||||
name = "subprocess"
|
name = "subprocess"
|
||||||
description = "Run a subprocess command safely"
|
description = "Run a subprocess command safely"
|
||||||
|
|
||||||
# =========================
|
|
||||||
# EXECUTE
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
def execute(
|
def execute(
|
||||||
self,
|
self,
|
||||||
payload: dict[str, Any],
|
payload: dict[str, Any],
|
||||||
ctx: ToolContext
|
ctx: ToolContext
|
||||||
):
|
):
|
||||||
cmd = payload.get("cmd")
|
cmd = payload.get("cmd")
|
||||||
|
|
||||||
if not isinstance(cmd, list):
|
|
||||||
raise ValueError(
|
|
||||||
"cmd must be list[str]"
|
|
||||||
)
|
|
||||||
|
|
||||||
cwd = payload.get("cwd")
|
cwd = payload.get("cwd")
|
||||||
|
timeout = payload.get("timeout", 60)
|
||||||
|
|
||||||
if cwd is not None and not isinstance(
|
# -------------------------
|
||||||
cwd,
|
# Validate command
|
||||||
str
|
# -------------------------
|
||||||
):
|
if not isinstance(cmd, list) or not all(isinstance(c, str) for c in cmd):
|
||||||
raise ValueError(
|
raise ValueError("cmd must be list[str]")
|
||||||
"cwd must be string"
|
|
||||||
)
|
|
||||||
|
|
||||||
timeout = payload.get(
|
# Optional safety: block empty commands
|
||||||
"timeout",
|
if not cmd:
|
||||||
60
|
raise ValueError("cmd cannot be empty")
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(
|
# -------------------------
|
||||||
timeout,
|
# Validate cwd
|
||||||
int
|
# -------------------------
|
||||||
):
|
if cwd is not None:
|
||||||
raise ValueError(
|
if not isinstance(cwd, str):
|
||||||
"timeout must be int"
|
raise ValueError("cwd must be string")
|
||||||
)
|
|
||||||
|
cwd_path = safety.validate_path(cwd)
|
||||||
|
cwd = str(cwd_path)
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Validate timeout
|
||||||
|
# -------------------------
|
||||||
|
if not isinstance(timeout, int) or timeout <= 0:
|
||||||
|
raise ValueError("timeout must be positive int")
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Dry-run support (future-proofing)
|
||||||
|
# -------------------------
|
||||||
|
if getattr(ctx, "dry_run", False):
|
||||||
|
return {
|
||||||
|
"dry_run": True,
|
||||||
|
"cmd": cmd,
|
||||||
|
"cwd": cwd,
|
||||||
|
"timeout": timeout,
|
||||||
|
"message": "Would execute subprocess"
|
||||||
|
}
|
||||||
|
|
||||||
return run_command(
|
return run_command(
|
||||||
cmd=cmd,
|
cmd=cmd,
|
||||||
@@ -57,10 +66,4 @@ class SubprocessTool(BaseTool):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
registry.register(SubprocessTool())
|
||||||
# SELF REGISTER
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
registry.register(
|
|
||||||
SubprocessTool()
|
|
||||||
)
|
|
||||||
188
tools/venv.py
188
tools/venv.py
@@ -0,0 +1,188 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from core.tools.base import BaseTool, ToolContext
|
||||||
|
from core.tools.registry import registry
|
||||||
|
from core.safety import safety
|
||||||
|
from core.events import bus
|
||||||
|
|
||||||
|
|
||||||
|
class VenvTool(BaseTool):
|
||||||
|
name = "venv"
|
||||||
|
description = "Python virtual environment management (create, install, run, list packages)"
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# EXECUTE ROUTER (ONLY ENTRYPOINT WITH ctx)
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
||||||
|
action = str(payload.get("action", "")).strip()
|
||||||
|
|
||||||
|
bus.log(
|
||||||
|
"VENV",
|
||||||
|
"venv_execute",
|
||||||
|
"INFO",
|
||||||
|
{"action": action}
|
||||||
|
)
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case "create":
|
||||||
|
return self.create_venv(payload)
|
||||||
|
|
||||||
|
case "install":
|
||||||
|
return self.install_package(payload)
|
||||||
|
|
||||||
|
case "run":
|
||||||
|
return self.run_python(payload)
|
||||||
|
|
||||||
|
case "list":
|
||||||
|
return self.list_packages(payload)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown venv action: {action}")
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# PATH HELPERS
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def _venv_path(self, path: str) -> Path:
|
||||||
|
return safety.validate_path(path)
|
||||||
|
|
||||||
|
def _python_bin(self, venv: Path) -> Path:
|
||||||
|
"""Cross-platform python binary resolution."""
|
||||||
|
if (venv / "bin").exists():
|
||||||
|
return venv / "bin" / "python"
|
||||||
|
return venv / "Scripts" / "python.exe"
|
||||||
|
|
||||||
|
def _pip_bin(self, venv: Path) -> Path:
|
||||||
|
"""Cross-platform pip binary resolution."""
|
||||||
|
if (venv / "bin").exists():
|
||||||
|
return venv / "bin" / "pip"
|
||||||
|
return venv / "Scripts" / "pip.exe"
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# CREATE VENV
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def create_venv(self, payload: dict[str, Any]):
|
||||||
|
path = payload.get("path")
|
||||||
|
|
||||||
|
if not isinstance(path, str):
|
||||||
|
raise ValueError("path must be string")
|
||||||
|
|
||||||
|
venv_path = self._venv_path(path)
|
||||||
|
|
||||||
|
if venv_path.exists():
|
||||||
|
return {
|
||||||
|
"status": "exists",
|
||||||
|
"path": str(venv_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, "-m", "venv", str(venv_path)],
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "created",
|
||||||
|
"path": str(venv_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# INSTALL PACKAGE
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def install_package(self, payload: dict[str, Any]):
|
||||||
|
path = payload.get("path")
|
||||||
|
package = payload.get("package")
|
||||||
|
|
||||||
|
if not isinstance(path, str):
|
||||||
|
raise ValueError("path must be string")
|
||||||
|
|
||||||
|
if not isinstance(package, str):
|
||||||
|
raise ValueError("package must be string")
|
||||||
|
|
||||||
|
venv_path = self._venv_path(path)
|
||||||
|
pip = self._pip_bin(venv_path)
|
||||||
|
|
||||||
|
if not pip.exists():
|
||||||
|
raise ValueError("pip not found in virtual environment")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(pip), "install", package],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "ok" if result.returncode == 0 else "error",
|
||||||
|
"stdout": result.stdout,
|
||||||
|
"stderr": result.stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# RUN PYTHON CODE
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def run_python(self, payload: dict[str, Any]):
|
||||||
|
path = payload.get("path")
|
||||||
|
code = payload.get("code")
|
||||||
|
|
||||||
|
if not isinstance(path, str):
|
||||||
|
raise ValueError("path must be string")
|
||||||
|
|
||||||
|
if not isinstance(code, str):
|
||||||
|
raise ValueError("code must be string")
|
||||||
|
|
||||||
|
venv_path = self._venv_path(path)
|
||||||
|
python_bin = self._python_bin(venv_path)
|
||||||
|
|
||||||
|
if not python_bin.exists():
|
||||||
|
raise ValueError("python executable not found in venv")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(python_bin), "-c", code],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "ok" if result.returncode == 0 else "error",
|
||||||
|
"stdout": result.stdout,
|
||||||
|
"stderr": result.stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# LIST PACKAGES
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def list_packages(self, payload: dict[str, Any]):
|
||||||
|
path = payload.get("path")
|
||||||
|
|
||||||
|
if not isinstance(path, str):
|
||||||
|
raise ValueError("path must be string")
|
||||||
|
|
||||||
|
venv_path = self._venv_path(path)
|
||||||
|
pip = self._pip_bin(venv_path)
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(pip), "list"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"output": result.stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# REGISTER TOOL
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
registry.register(VenvTool())
|
||||||
Reference in New Issue
Block a user
