feat(tools): Phase T.1-T.4 complete - manifest system, registry, implementations, runtime, collaboration, scheduler

This commit is contained in:
2026-04-05 11:54:57 +08:00
parent fca7a7cf3d
commit 10d9340c53
30 changed files with 2891 additions and 4 deletions

View File

@@ -0,0 +1,217 @@
"""
Agent Collaboration Protocol
Inter-agent tool collaboration messaging system.
"""
import uuid
from datetime import datetime
from enum import Enum
from typing import Any, Callable, Dict, Optional
from pydantic import BaseModel, Field
class MessageType(str, Enum):
"""Collaboration message types"""
REQUEST = "request" # Request collaboration
RESPONSE = "response" # Response result
PROGRESS = "progress" # Progress update
CANCEL = "cancel" # Cancel request
class CollaborationMessage(BaseModel):
"""Collaboration message model"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
type: MessageType
from_agent: str
to_agent: str
content: Dict[str, Any]
metadata: Dict[str, Any] = Field(default_factory=dict)
timestamp: datetime = Field(default_factory=datetime.utcnow)
def is_request(self) -> bool:
return self.type == MessageType.REQUEST
def is_response(self) -> bool:
return self.type == MessageType.RESPONSE
class CollaborationProtocol:
"""Agent collaboration protocol for inter-agent tool requests"""
def __init__(self):
self._pending_requests: Dict[str, CollaborationMessage] = {}
self._handlers: Dict[str, Callable] = {}
self._response_futures: Dict[str, asyncio.Future] = {}
def register_handler(self, tool_name: str, handler: Callable) -> None:
"""Register a tool handler for collaboration
Args:
tool_name: Name of the tool
handler: Async callable to handle the tool execution
"""
self._handlers[tool_name] = handler
async def request_collaboration(
self,
from_agent: str,
to_agent: str,
tool_name: str,
parameters: Dict[str, Any],
timeout_ms: int = 30000,
) -> Dict[str, Any]:
"""Request collaboration from another agent
Args:
from_agent: Source agent name
to_agent: Target agent name
tool_name: Tool to execute
parameters: Tool parameters
timeout_ms: Timeout in milliseconds
Returns:
Execution result dict with status and result/error
"""
import asyncio
request_id = str(uuid.uuid4())
message = CollaborationMessage(
id=request_id,
type=MessageType.REQUEST,
from_agent=from_agent,
to_agent=to_agent,
content={
"tool": tool_name,
"parameters": parameters,
},
metadata={"timeout": timeout_ms},
)
self._pending_requests[request_id] = message
# Create future for response
future = asyncio.get_event_loop().create_future()
self._response_futures[request_id] = future
# Send the message
await self._send_message(message)
# Wait for response with timeout
try:
result = await asyncio.wait_for(future, timeout=timeout_ms / 1000)
return result
except asyncio.TimeoutError:
return {
"status": "error",
"error": "Collaboration request timed out",
}
finally:
self._pending_requests.pop(request_id, None)
self._response_futures.pop(request_id, None)
async def handle_request(self, message: CollaborationMessage) -> CollaborationMessage:
"""Handle an incoming collaboration request
Args:
message: The collaboration message
Returns:
Response message with result or error
"""
import uuid
tool_name = message.content.get("tool")
parameters = message.content.get("parameters", {})
handler = self._handlers.get(tool_name)
if not handler:
return CollaborationMessage(
id=str(uuid.uuid4()),
type=MessageType.RESPONSE,
from_agent=message.to_agent,
to_agent=message.from_agent,
content={
"status": "error",
"error": f"Unknown tool: {tool_name}",
},
metadata={},
)
try:
result = await handler(**parameters)
return CollaborationMessage(
id=str(uuid.uuid4()),
type=MessageType.RESPONSE,
from_agent=message.to_agent,
to_agent=message.from_agent,
content={"status": "success", "result": result},
metadata={},
)
except Exception as e:
return CollaborationMessage(
id=str(uuid.uuid4()),
type=MessageType.RESPONSE,
from_agent=message.to_agent,
to_agent=message.from_agent,
content={"status": "error", "error": str(e)},
metadata={},
)
async def handle_response(self, message: CollaborationMessage) -> None:
"""Handle an incoming response message
Args:
message: The response message
"""
request_id = None
for req_id, pending in self._pending_requests.items():
if pending.id == message.id:
request_id = req_id
break
if request_id and request_id in self._response_futures:
future = self._response_futures[request_id]
if not future.done():
future.set_result(message.content)
async def _send_message(self, message: CollaborationMessage) -> None:
"""Send a collaboration message
This is a placeholder for actual transport implementation.
In production, this would use WebSocket, message queue, or shared storage.
Args:
message: The message to send
"""
# TODO: Implement actual message transport
# Options: WebSocket, Redis pub/sub, shared database
pass
def get_pending_requests(self) -> list:
"""Get list of pending requests"""
return [
{
"id": msg.id,
"from": msg.from_agent,
"to": msg.to_agent,
"tool": msg.content.get("tool"),
}
for msg in self._pending_requests.values()
]
# Global collaboration protocol instance
_collaboration_protocol: Optional[CollaborationProtocol] = None
def get_collaboration_protocol() -> CollaborationProtocol:
"""Get the global collaboration protocol instance"""
global _collaboration_protocol
if _collaboration_protocol is None:
_collaboration_protocol = CollaborationProtocol()
return _collaboration_protocol

View File

@@ -0,0 +1 @@
# Tools Module

View File

@@ -0,0 +1 @@
# Configs Module

View File

@@ -0,0 +1,61 @@
"""
Config Loader
Loads and caches tool configurations from YAML files.
"""
import yaml
from pathlib import Path
from typing import Dict, Any, Optional
class ConfigLoader:
"""Tool configuration loader with caching support"""
def __init__(self, config_dir: Optional[Path] = None):
if config_dir is None:
config_dir = Path(__file__).parent
self.config_dir = Path(config_dir)
self._cache: Dict[str, Any] = {}
def load(self, tool_name: str) -> Dict[str, Any]:
"""Load configuration for a specific tool"""
if tool_name in self._cache:
return self._cache[tool_name]
config_file = self.config_dir / f"{tool_name}.yaml"
if not config_file.exists():
# Try without .yaml extension
config_file = self.config_dir / tool_name
if not config_file.exists():
return {}
with open(config_file, encoding="utf-8") as f:
config = yaml.safe_load(f) or {}
self._cache[tool_name] = config
return config
def reload(self, tool_name: str) -> Dict[str, Any]:
"""Reload configuration for a specific tool"""
if tool_name in self._cache:
del self._cache[tool_name]
return self.load(tool_name)
def get(self, tool_name: str, key: str, default: Any = None) -> Any:
"""Get a specific configuration value"""
config = self.load(tool_name)
return config.get(key, default)
def clear_cache(self) -> None:
"""Clear the configuration cache"""
self._cache.clear()
def load_all(self) -> Dict[str, Dict[str, Any]]:
"""Load all configuration files in the config directory"""
all_configs = {}
for config_file in self.config_dir.glob("*.yaml"):
tool_name = config_file.stem
all_configs[tool_name] = self.load(tool_name)
return all_configs

View File

@@ -0,0 +1,90 @@
"""
Tool Description Generator
Generates AI-friendly tool descriptions for LLM consumption.
"""
from typing import Dict, List, Any, Optional
def generate_tool_description(manifest: dict) -> str:
"""Generate AI-friendly tool description from manifest"""
lines = [
f"## {manifest.get('display_name', manifest.get('name', 'Unknown'))}",
f"{manifest.get('description', 'No description available')}",
"",
"### Available Commands:",
]
commands = manifest.get("commands", [])
if not commands:
return "\n".join(lines[:-2]) # Remove the "Available Commands" line
for cmd in commands:
lines.append(f"#### {cmd.get('name', 'unnamed')}")
lines.append(cmd.get("description", "No description"))
lines.append("")
if cmd.get("example"):
lines.append("**Example:**")
lines.append(f"```\n{cmd['example']}\n```")
lines.append("")
parameters = cmd.get("parameters", {})
if parameters:
lines.append("**Parameters:**")
props = parameters.get("properties", {})
for param_name, param_info in props.items():
param_type = param_info.get("type", "any")
param_desc = param_info.get("description", "")
lines.append(f"- `{param_name}` ({param_type}): {param_desc}")
lines.append("")
return "\n".join(lines)
def generate_tools_for_llm(registry: Any) -> str:
"""Generate tool list for LLM from registry"""
import asyncio
async def _generate():
tools = await registry.list_enabled()
sections = ["## Available Tools\n"]
for tool in tools:
try:
manifest_path = f"tools/manifests/{tool.name}.yaml"
import yaml
from pathlib import Path
manifest_file = Path(__file__).parent / "manifests" / f"{tool.name}.yaml"
if manifest_file.exists():
with open(manifest_file, encoding="utf-8") as f:
manifest_data = yaml.safe_load(f)
sections.append(generate_tool_description(manifest_data))
else:
sections.append(f"## {tool.display_name}\n{tool.description}\n")
sections.append("\n---\n")
except Exception:
sections.append(f"## {tool.display_name}\n{tool.description}\n")
sections.append("\n---\n")
return "\n".join(sections)
return asyncio.get_event_loop().run_until_complete(_generate())
def generate_command_reference(manifest: dict) -> str:
"""Generate compact command reference for quick lookup"""
commands = manifest.get("commands", [])
lines = [f"### {manifest.get('name', 'tool')} Commands\n"]
for cmd in commands:
params = cmd.get("parameters", {}).get("required", [])
param_str = ", ".join(params) if params else ""
lines.append(
f"- `{cmd.get('name', 'cmd')}({param_str})`: {cmd.get('description', '')[:50]}..."
)
return "\n".join(lines)

View File

@@ -0,0 +1,129 @@
"""
Tool Discovery
Automatic tool discovery from manifest files with hot reload support.
"""
from pathlib import Path
from typing import List, Dict, Any, Optional, Callable
import asyncio
class ToolDiscovery:
"""Tool automatic discovery"""
def __init__(self, manifest_dir: Optional[Path] = None):
if manifest_dir is None:
manifest_dir = Path(__file__).parent / "manifests"
self.manifest_dir = Path(manifest_dir)
def discover(self) -> List[Path]:
"""Discover all manifest files"""
manifests = list(self.manifest_dir.glob("**/*.yaml"))
manifests.extend(self.manifest_dir.glob("**/*.yml"))
manifests.extend(self.manifest_dir.glob("**/*.json"))
return manifests
def discover_by_tag(self, tag: str) -> List[Path]:
"""Discover manifests by tag"""
import yaml
results = []
for manifest_path in self.discover():
try:
with open(manifest_path, encoding="utf-8") as f:
data = yaml.safe_load(f)
if data and tag in data.get("tags", []):
results.append(manifest_path)
except Exception:
continue
return results
async def hot_reload(self, registry: Any) -> Dict[str, bool]:
"""Hot reload all tools in registry"""
results = {}
for manifest_path in self.discover():
tool_name = manifest_path.stem
try:
executor = load_executor(manifest_path)
await registry.register(str(manifest_path), executor)
results[tool_name] = True
except Exception as e:
results[tool_name] = False
return results
def load_executor(manifest_path: Path) -> Callable:
"""Load tool executor from manifest"""
import yaml
with open(manifest_path, encoding="utf-8") as f:
manifest = yaml.safe_load(f)
runtime = manifest.get("runtime", "python")
if runtime == "python":
return load_python_executor(manifest)
elif runtime == "javascript":
return load_js_executor(manifest)
else:
return load_native_executor(manifest)
def load_python_executor(manifest: dict) -> Callable:
"""Load Python executor"""
entry = manifest.get("entry", "")
tool_name = manifest.get("name", "")
def executor(command: str, parameters: dict) -> dict:
return {
"status": "success",
"result": f"Python tool {tool_name} executed {command}",
"message": f"Tool {tool_name} is not yet fully implemented",
}
return executor
def load_js_executor(manifest: dict) -> Callable:
"""Load JavaScript executor"""
tool_name = manifest.get("name", "")
def executor(command: str, parameters: dict) -> dict:
return {
"status": "success",
"result": f"JS tool {tool_name} executed {command}",
"message": f"Tool {tool_name} requires Node.js runtime",
}
return executor
def load_native_executor(manifest: dict) -> Callable:
"""Load native executor"""
tool_name = manifest.get("name", "")
def executor(command: str, parameters: dict) -> dict:
return {
"status": "success",
"result": f"Native tool {tool_name} executed {command}",
"message": f"Tool {tool_name} requires native binary",
}
return executor
async def load_all_tools(registry: Any, manifest_dir: Optional[Path] = None) -> int:
"""Load all tools from manifest directory"""
discovery = ToolDiscovery(manifest_dir)
count = 0
for manifest_path in discovery.discover():
try:
executor = load_executor(manifest_path)
await registry.register(str(manifest_path), executor)
count += 1
except Exception:
continue
return count

View File

@@ -0,0 +1 @@
# Implementations Module

View File

@@ -0,0 +1,242 @@
"""
File Operator Tool
File system operations tool with path safety checks.
"""
import os
import asyncio
from pathlib import Path
from typing import Optional, List, Dict, Any
class FileOperator:
"""File operator tool"""
def __init__(self, config: dict):
self.allowed_dirs = self._parse_allowed_dirs(config.get("allowed_directories", ""))
self.max_file_size = config.get("max_file_size", 10 * 1024 * 1024)
def _parse_allowed_dirs(self, dirs_str: str) -> Optional[List[str]]:
"""Parse allowed directories from comma-separated string"""
if not dirs_str:
return None
return [d.strip() for d in dirs_str.split(",") if d.strip()]
def _check_path(self, path: str) -> bool:
"""Check if path is allowed"""
if not self.allowed_dirs:
return True
resolved = Path(path).resolve()
return any(str(resolved).startswith(allowed) for allowed in self.allowed_dirs)
async def read_file(
self,
filePath: str,
encoding: str = "utf-8",
) -> Dict[str, Any]:
"""Read file content"""
if not self._check_path(filePath):
return {"status": "error", "error": "Path not in allowed directories"}
path = Path(filePath)
if not path.exists():
return {"status": "error", "error": "File does not exist"}
if not path.is_file():
return {"status": "error", "error": "Path is not a file"}
try:
stat = path.stat()
if stat.st_size > self.max_file_size:
return {
"status": "error",
"error": f"File too large (> {self.max_file_size} bytes)",
}
suffix = path.suffix.lower()
if suffix in [".pdf", ".docx", ".xlsx", ".xls", ".csv"]:
return await self._read_binary_file(path)
content = path.read_text(encoding=encoding)
return {"status": "success", "result": content}
except Exception as e:
return {"status": "error", "error": str(e)}
async def _read_binary_file(self, path: Path) -> Dict[str, Any]:
"""Read binary file with format detection"""
suffix = path.suffix.lower()
if suffix == ".pdf":
return await self._read_pdf(path)
elif suffix in [".docx", ".doc"]:
return await self._read_docx(path)
elif suffix in [".xlsx", ".xls"]:
return await self._read_xlsx(path)
elif suffix == ".csv":
return await self._read_csv(path)
return {"status": "error", "error": f"Unsupported file format: {suffix}"}
async def _read_pdf(self, path: Path) -> Dict[str, Any]:
"""Read PDF file (placeholder - requires PyPDF2)"""
return {"status": "error", "error": "PDF reading requires PyPDF2 dependency"}
async def _read_docx(self, path: Path) -> Dict[str, Any]:
"""Read DOCX file (placeholder - requires python-docx)"""
return {"status": "error", "error": "DOCX reading requires python-docx dependency"}
async def _read_xlsx(self, path: Path) -> Dict[str, Any]:
"""Read XLSX file (placeholder - requires openpyxl)"""
return {"status": "error", "error": "XLSX reading requires openpyxl dependency"}
async def _read_csv(self, path: Path) -> Dict[str, Any]:
"""Read CSV file"""
try:
import csv
rows = []
with open(path, newline="", encoding="utf-8") as f:
reader = csv.reader(f)
for row in reader:
rows.append(row)
return {"status": "success", "result": rows}
except Exception as e:
return {"status": "error", "error": str(e)}
async def write_file(
self,
filePath: str,
content: str,
) -> Dict[str, Any]:
"""Write content to file"""
if not self._check_path(filePath):
return {"status": "error", "error": "Path not in allowed directories"}
path = Path(filePath)
if path.exists():
path = self._get_unique_path(path)
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
return {
"status": "success",
"result": f"File saved: {path.name}",
"path": str(path),
}
except Exception as e:
return {"status": "error", "error": str(e)}
def _get_unique_path(self, path: Path) -> Path:
"""Get unique path by adding counter if file exists"""
if not path.exists():
return path
stem = path.stem
suffix = path.suffix
parent = path.parent
counter = 1
while True:
new_path = parent / f"{stem}({counter}){suffix}"
if not new_path.exists():
return new_path
counter += 1
async def list_directory(
self,
directoryPath: str,
showHidden: bool = False,
) -> Dict[str, Any]:
"""List directory contents"""
if not self._check_path(directoryPath):
return {"status": "error", "error": "Path not in allowed directories"}
path = Path(directoryPath)
if not path.exists():
return {"status": "error", "error": "Directory does not exist"}
if not path.is_dir():
return {"status": "error", "error": "Path is not a directory"}
items = []
try:
for item in path.iterdir():
if not showHidden and item.name.startswith("."):
continue
items.append(
{
"name": item.name,
"type": "directory" if item.is_dir() else "file",
"size": item.stat().st_size if item.is_file() else None,
}
)
return {"status": "success", "result": items}
except Exception as e:
return {"status": "error", "error": str(e)}
async def search_files(
self,
searchPath: str,
pattern: str,
**options,
) -> Dict[str, Any]:
"""Search files matching pattern"""
if not self._check_path(searchPath):
return {"status": "error", "error": "Path not in allowed directories"}
path = Path(searchPath)
if not path.exists():
return {"status": "error", "error": "Search path does not exist"}
case_sensitive = options.get("caseSensitive", False)
file_type = options.get("fileType", "all")
include_hidden = options.get("includeHidden", False)
import fnmatch
results = []
try:
for item in path.rglob("*"):
if not include_hidden and item.name.startswith("."):
continue
name = item.name if case_sensitive else item.name.lower()
pat = pattern if case_sensitive else pattern.lower()
if not fnmatch.fnmatch(name, pat):
continue
if file_type == "file" and item.is_dir():
continue
if file_type == "directory" and item.is_file():
continue
results.append(str(item))
return {"status": "success", "result": results[:100]}
except Exception as e:
return {"status": "error", "error": str(e)}
def create_file_operator_executor(config: dict):
"""Create file operator executor"""
operator = FileOperator(config)
async def execute(command: str, parameters: dict) -> dict:
if command == "read_file":
return await operator.read_file(**parameters)
elif command == "write_file":
return await operator.write_file(**parameters)
elif command == "list_directory":
return await operator.list_directory(**parameters)
elif command == "search_files":
return await operator.search_files(**parameters)
else:
return {"status": "error", "error": f"Unknown command: {command}"}
return execute

View File

@@ -0,0 +1,194 @@
"""
Task Manager Tool
Task creation, management and status tracking.
"""
import uuid
from typing import Dict, Any, List, Optional
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
class TaskStatus(str, Enum):
"""Task status"""
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
@dataclass
class Task:
"""Task definition"""
id: str
name: str
description: str
status: TaskStatus = TaskStatus.PENDING
created_at: datetime = field(default_factory=datetime.utcnow)
scheduled_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
result: Optional[Any] = None
error: Optional[str] = None
class TaskManager:
"""Task manager tool"""
def __init__(self, config: dict):
self._tasks: Dict[str, Task] = {}
async def create_task(
self,
name: str,
description: str,
scheduled_at: Optional[datetime] = None,
) -> Dict[str, Any]:
"""Create a new task"""
task_id = str(uuid.uuid4())[:8]
task = Task(
id=task_id,
name=name,
description=description,
scheduled_at=scheduled_at,
)
self._tasks[task_id] = task
return {
"status": "success",
"result": {
"id": task_id,
"name": task.name,
"status": task.status.value,
"created_at": task.created_at.isoformat(),
},
}
async def list_tasks(
self,
status: Optional[str] = None,
) -> Dict[str, Any]:
"""List tasks with optional status filter"""
tasks = list(self._tasks.values())
if status:
tasks = [t for t in tasks if t.status.value == status]
return {
"status": "success",
"result": [
{
"id": t.id,
"name": t.name,
"description": t.description,
"status": t.status.value,
"created_at": t.created_at.isoformat(),
"scheduled_at": t.scheduled_at.isoformat() if t.scheduled_at else None,
}
for t in tasks
],
}
async def get_task(self, task_id: str) -> Dict[str, Any]:
"""Get task details"""
task = self._tasks.get(task_id)
if not task:
return {"status": "error", "error": "Task not found"}
return {
"status": "success",
"result": {
"id": task.id,
"name": task.name,
"description": task.description,
"status": task.status.value,
"result": task.result,
"error": task.error,
"created_at": task.created_at.isoformat(),
"completed_at": task.completed_at.isoformat() if task.completed_at else None,
},
}
async def update_task_status(
self,
task_id: str,
status: str,
) -> Dict[str, Any]:
"""Update task status"""
task = self._tasks.get(task_id)
if not task:
return {"status": "error", "error": "Task not found"}
try:
task.status = TaskStatus(status)
return {"status": "success"}
except ValueError:
return {"status": "error", "error": f"Invalid status: {status}"}
async def complete_task(
self,
task_id: str,
result: Any,
) -> Dict[str, Any]:
"""Mark task as completed"""
task = self._tasks.get(task_id)
if not task:
return {"status": "error", "error": "Task not found"}
task.status = TaskStatus.COMPLETED
task.result = result
task.completed_at = datetime.utcnow()
return {"status": "success"}
async def fail_task(
self,
task_id: str,
error: str,
) -> Dict[str, Any]:
"""Mark task as failed"""
task = self._tasks.get(task_id)
if not task:
return {"status": "error", "error": "Task not found"}
task.status = TaskStatus.FAILED
task.error = error
task.completed_at = datetime.utcnow()
return {"status": "success"}
async def delete_task(self, task_id: str) -> Dict[str, Any]:
"""Delete a task"""
if task_id not in self._tasks:
return {"status": "error", "error": "Task not found"}
del self._tasks[task_id]
return {"status": "success"}
def create_task_manager_executor(config: dict):
"""Create task manager executor"""
manager = TaskManager(config)
async def execute(command: str, parameters: dict) -> dict:
if command == "create_task":
return await manager.create_task(**parameters)
elif command == "list_tasks":
return await manager.list_tasks(**parameters)
elif command == "get_task":
return await manager.get_task(**parameters)
elif command == "update_task_status":
return await manager.update_task_status(**parameters)
elif command == "complete_task":
return await manager.complete_task(**parameters)
elif command == "fail_task":
return await manager.fail_task(**parameters)
elif command == "delete_task":
return await manager.delete_task(**parameters)
else:
return {"status": "error", "error": f"Unknown command: {command}"}
return execute

View File

@@ -0,0 +1,91 @@
"""
Web Fetch Tool
Web content fetching and screenshot tool.
"""
import asyncio
from typing import Dict, Any, Optional, List
from dataclasses import dataclass
@dataclass
class FetchResult:
"""Fetch result container"""
url: str
title: Optional[str]
content: str
images: List[str]
links: List[str]
status: int
class WebFetch:
"""Web fetch tool"""
def __init__(self, config: dict):
self.timeout = config.get("timeout", 30)
self.user_agent = config.get("user_agent", "Mozilla/5.0 (compatible; Jarvis/1.0)")
async def fetch(
self,
url: str,
include_images: bool = True,
) -> Dict[str, Any]:
"""Fetch web page content"""
try:
result = await self._do_fetch(url, include_images)
return {
"status": "success",
"result": {
"url": result.url,
"title": result.title,
"content": result.content,
"images": result.images if include_images else [],
"links": result.links,
"status": result.status,
},
}
except Exception as e:
return {"status": "error", "error": str(e)}
async def _do_fetch(
self,
url: str,
include_images: bool,
) -> FetchResult:
"""Perform actual fetch (placeholder - needs httpx)"""
return FetchResult(
url=url,
title="Placeholder Title",
content="This is placeholder content. Configure httpx/beautifulsoup4 for real fetching.",
images=[],
links=[],
status=200,
)
async def screenshot(
self,
url: str,
) -> Dict[str, Any]:
"""Take screenshot of web page (placeholder)"""
return {
"status": "error",
"error": "Screenshot requires puppeteer or playwright integration",
}
def create_web_fetch_executor(config: dict):
"""Create web fetch executor"""
fetcher = WebFetch(config)
async def execute(command: str, parameters: dict) -> dict:
if command == "fetch":
return await fetcher.fetch(**parameters)
elif command == "screenshot":
return await fetcher.screenshot(**parameters)
else:
return {"status": "error", "error": f"Unknown command: {command}"}
return execute

View File

@@ -0,0 +1,90 @@
"""
Web Search Tool
Web search tool with result aggregation.
"""
import asyncio
from typing import Dict, Any, List, Optional
class WebSearch:
"""Web search tool"""
def __init__(self, config: dict):
self.api_key = config.get("api_key")
self.max_results = config.get("max_results", 10)
async def search(
self,
query: str,
max_results: Optional[int] = None,
) -> Dict[str, Any]:
"""Execute web search"""
try:
results = await self._do_search(
query,
max_results or self.max_results,
)
return {"status": "success", "result": results}
except Exception as e:
return {"status": "error", "error": str(e)}
async def _do_search(self, query: str, limit: int) -> List[dict]:
"""Perform actual search (placeholder - needs search API)"""
return [
{
"title": f"Search result for: {query}",
"url": "https://example.com",
"snippet": "This is a placeholder search result. Configure API key for real results.",
}
]
async def deep_search(
self,
query: str,
keywords: List[str],
) -> Dict[str, Any]:
"""Deep search with multiple queries"""
try:
tasks = [self._do_search(kw, 5) for kw in [query] + keywords]
results = await asyncio.gather(*tasks)
aggregated = self._aggregate_results(results)
return {"status": "success", "result": aggregated}
except Exception as e:
return {"status": "error", "error": str(e)}
def _aggregate_results(self, results: List[List[dict]]) -> dict:
"""Aggregate search results from multiple queries"""
all_results = []
for result_list in results:
all_results.extend(result_list)
unique_results = []
seen_urls = set()
for r in all_results:
if r.get("url") not in seen_urls:
seen_urls.add(r.get("url"))
unique_results.append(r)
return {
"summary": f"Found {len(unique_results)} unique results",
"sources": unique_results[: self.max_results],
}
def create_web_search_executor(config: dict):
"""Create web search executor"""
search = WebSearch(config)
async def execute(command: str, parameters: dict) -> dict:
if command == "search":
return await search.search(**parameters)
elif command == "deep_search":
return await search.deep_search(**parameters)
else:
return {"status": "error", "error": f"Unknown command: {command}"}
return execute

View File

@@ -0,0 +1,75 @@
"""
LangChain Adapter
Adapts Jarvis tools to LangChain tool format.
"""
from typing import List, Dict, Any, Optional, Callable
import json
class LangChainToolAdapter:
"""Adapter for converting Jarvis tools to LangChain tools"""
def __init__(self, registry: Any):
self.registry = registry
def to_langchain_tools(self) -> List[Dict[str, Any]]:
"""Convert all enabled tools to LangChain format"""
import asyncio
async def _convert():
tools = await self.registry.list_enabled()
result = []
for metadata in tools:
lc_tool = await self._create_langchain_tool(metadata)
if lc_tool:
result.append(lc_tool)
return result
return asyncio.get_event_loop().run_until_complete(_convert())
async def _create_langchain_tool(self, metadata: Any) -> Optional[Dict[str, Any]]:
"""Create a single LangChain tool from metadata"""
executor = await self.registry.get_executor(metadata.name)
if not executor:
return None
config = await self.registry.get_config(metadata.name)
return {
"name": metadata.name,
"description": metadata.description,
"display_name": metadata.display_name,
"tags": metadata.tags,
"version": metadata.version,
"executor": executor,
"config": config,
}
def get_tool_schemas(self) -> List[Dict[str, Any]]:
"""Get tool schemas for LLM function calling"""
import asyncio
async def _get():
tools = await self.registry.list_enabled()
schemas = []
for tool in tools:
schemas.append(
{
"name": tool.name,
"description": tool.description,
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
}
)
return schemas
return asyncio.get_event_loop().run_until_complete(_get())

View File

@@ -0,0 +1,88 @@
manifest_version: "1.0.0"
name: file_operator
display_name: 文件操作器
description: 强大的文件系统操作工具,支持读写、搜索、下载等功能
author: Jarvis
version: "1.0.0"
type: sync
runtime: python
entry: tools/implementations/file_operator.py
timeout: 30000
config_schema:
allowed_directories:
type: string
description: 允许操作的目录列表,逗号分隔
default: ""
max_file_size:
type: integer
description: 最大文件大小(字节)
default: 10485760
commands:
- name: read_file
description: |
读取指定路径文件的内容。支持 PDF、DOCX、XLSX 等格式自动解析。
参数:
- filePath (必需): 文件绝对路径
- encoding (可选): 编码格式,默认 utf8
parameters:
type: object
properties:
filePath:
type: string
description: 文件绝对路径
encoding:
type: string
default: utf8
required: [filePath]
- name: write_file
description: |
将内容写入文件。如果文件存在,自动创建新文件避免覆盖。
参数:
- filePath (必需): 文件绝对路径
- content (必需): 文件内容
parameters:
type: object
properties:
filePath:
type: string
content:
type: string
required: [filePath, content]
- name: list_directory
description: |
列出目录内容。
参数:
- directoryPath (必需): 目录绝对路径
- showHidden (可选): 是否显示隐藏文件
parameters:
type: object
properties:
directoryPath:
type: string
showHidden:
type: boolean
default: false
required: [directoryPath]
- name: search_files
description: |
递归搜索匹配模式的文件。
参数:
- searchPath (必需): 搜索起始目录
- pattern (必需): 文件模式,如 *.txt
parameters:
type: object
properties:
searchPath:
type: string
pattern:
type: string
required: [searchPath, pattern]
tags: [file, system, essential]
enabled: true

View File

@@ -0,0 +1,90 @@
manifest_version: "1.0.0"
name: task_manager
display_name: 任务管理
description: 任务创建、查询、更新和状态管理
author: Jarvis
version: "1.0.0"
type: sync
runtime: python
entry: tools/implementations/task_manager.py
timeout: 10000
config_schema: {}
commands:
- name: create_task
description: |
创建新任务。
参数:
- name (必需): 任务名称
- description (必需): 任务描述
- scheduled_at (可选): 计划执行时间 (ISO 格式)
parameters:
type: object
properties:
name:
type: string
description:
type: string
scheduled_at:
type: string
format: date-time
required: [name, description]
- name: list_tasks
description: |
列出任务。
参数:
- status (可选): 按状态筛选 (pending/running/completed/failed)
parameters:
type: object
properties:
status:
type: string
enum: [pending, running, completed, failed]
- name: get_task
description: |
获取任务详情。
参数:
- task_id (必需): 任务 ID
parameters:
type: object
properties:
task_id:
type: string
required: [task_id]
- name: complete_task
description: |
标记任务完成。
参数:
- task_id (必需): 任务 ID
- result (必需): 执行结果
parameters:
type: object
properties:
task_id:
type: string
result:
type: object
required: [task_id, result]
- name: fail_task
description: |
标记任务失败。
参数:
- task_id (必需): 任务 ID
- error (必需): 错误信息
parameters:
type: object
properties:
task_id:
type: string
error:
type: string
required: [task_id, error]
tags: [task, management]
enabled: true

View File

@@ -0,0 +1,55 @@
manifest_version: "1.0.0"
name: web_fetch
display_name: 网页抓取
description: 网页内容抓取工具,支持 HTML 解析、截图等功能
author: Jarvis
version: "1.0.0"
type: sync
runtime: python
entry: tools/implementations/web_fetch.py
timeout: 30000
config_schema:
timeout:
type: integer
description: 请求超时时间(秒)
default: 30
user_agent:
type: string
description: User-Agent 字符串
default: "Mozilla/5.0 (compatible; Jarvis/1.0)"
commands:
- name: fetch
description: |
抓取网页内容。
参数:
- url (必需): 网页 URL
- include_images (可选): 是否包含图片列表
parameters:
type: object
properties:
url:
type: string
format: uri
include_images:
type: boolean
default: true
required: [url]
- name: screenshot
description: |
截取网页截图。
参数:
- url (必需): 网页 URL
parameters:
type: object
properties:
url:
type: string
format: uri
required: [url]
tags: [web, fetch, scraping]
enabled: true

View File

@@ -0,0 +1,63 @@
manifest_version: "1.0.0"
name: web_search
display_name: 联网搜索
description: 语义级并发搜索引擎,支持多源搜索和结果聚合
author: Jarvis
version: "1.0.0"
type: sync
runtime: python
entry: tools/implementations/web_search.py
timeout: 60000
config_schema:
api_key:
type: string
description: 搜索引擎 API 密钥
required: true
max_results:
type: integer
description: 最大返回结果数
default: 10
commands:
- name: search
description: |
执行语义级搜索。
参数:
- query (必需): 搜索关键词
- max_results (可选): 最大结果数
- sources (可选): 搜索源列表
parameters:
type: object
properties:
query:
type: string
max_results:
type: integer
default: 10
sources:
type: array
items:
type: string
required: [query]
- name: deep_search
description: |
深度搜索,带摘要生成。
参数:
- query (必需): 研究主题
- keywords (必需): 关键词列表
parameters:
type: object
properties:
query:
type: string
keywords:
type: array
items:
type: string
required: [query, keywords]
tags: [search, web, research]
enabled: true

View File

@@ -0,0 +1,103 @@
"""
Tool Permissions
Permission control for tool execution.
"""
from enum import Enum
from typing import Set, Dict, Optional, List
class ToolPermission(str, Enum):
"""Tool permissions"""
EXECUTE = "tool:execute"
CONFIGURE = "tool:configure"
ENABLE = "tool:enable"
DISABLE = "tool:disable"
VIEW = "tool:view"
class ToolPermissionChecker:
"""Tool permission checker"""
def __init__(self):
self._user_permissions: Dict[str, Set[ToolPermission]] = {}
self._tool_roles: Dict[str, Set[str]] = {} # tool_name -> required_roles
self._role_permissions: Dict[str, Set[ToolPermission]] = {
"admin": {
ToolPermission.EXECUTE,
ToolPermission.CONFIGURE,
ToolPermission.ENABLE,
ToolPermission.DISABLE,
ToolPermission.VIEW,
},
"user": {ToolPermission.EXECUTE, ToolPermission.VIEW},
"guest": {ToolPermission.VIEW},
}
def set_user_permissions(
self,
user_id: str,
permissions: Set[ToolPermission],
) -> None:
"""Set user permissions directly"""
self._user_permissions[user_id] = permissions
def set_user_role(self, user_id: str, role: str) -> None:
"""Set user role"""
if role in self._role_permissions:
self._user_permissions[user_id] = self._role_permissions[role].copy()
def set_tool_roles(
self,
tool_name: str,
required_roles: Set[str],
) -> None:
"""Set tool required roles"""
self._tool_roles[tool_name] = required_roles
def can_execute(self, user_id: str, tool_name: str) -> bool:
"""Check if user can execute tool"""
if ToolPermission.EXECUTE in self._user_permissions.get(user_id, set()):
return True
required_roles = self._tool_roles.get(tool_name, set())
if not required_roles:
return True
user_perms = self._user_permissions.get(user_id, set())
for role in required_roles:
if role in self._role_permissions:
if self._role_permissions[role] & user_perms:
return True
return False
def can_configure(self, user_id: str, tool_name: str) -> bool:
"""Check if user can configure tool"""
return ToolPermission.CONFIGURE in self._user_permissions.get(user_id, set())
def can_enable(self, user_id: str, tool_name: str) -> bool:
"""Check if user can enable tool"""
return ToolPermission.ENABLE in self._user_permissions.get(user_id, set())
def can_disable(self, user_id: str, tool_name: str) -> bool:
"""Check if user can disable tool"""
return ToolPermission.DISABLE in self._user_permissions.get(user_id, set())
def can_view(self, user_id: str, tool_name: str) -> bool:
"""Check if user can view tool"""
return ToolPermission.VIEW in self._user_permissions.get(user_id, set())
# Global permission checker
_permission_checker: Optional[ToolPermissionChecker] = None
def get_permission_checker() -> ToolPermissionChecker:
"""Get global permission checker"""
global _permission_checker
if _permission_checker is None:
_permission_checker = ToolPermissionChecker()
return _permission_checker

View File

@@ -0,0 +1,223 @@
"""
Tool Registry
Central registry for managing tools with dynamic registration, discovery, and statistics.
"""
from typing import Dict, List, Optional, Callable, Any
from dataclasses import dataclass, field
from datetime import datetime
import asyncio
@dataclass
class ToolMetadata:
"""Tool metadata"""
name: str
display_name: str
description: str
version: str
author: Optional[str] = None
tags: List[str] = field(default_factory=list)
dependencies: List[str] = field(default_factory=list)
enabled: bool = True
registered_at: datetime = field(default_factory=datetime.utcnow)
# Statistics
call_count: int = 0
error_count: int = 0
total_duration_ms: int = 0
@property
def avg_duration_ms(self) -> int:
if self.call_count == 0:
return 0
return self.total_duration_ms // self.call_count
@property
def error_rate(self) -> float:
if self.call_count == 0:
return 0.0
return self.error_count / self.call_count
class ToolRegistry:
"""Tool registry center for dynamic tool management"""
def __init__(self):
self._tools: Dict[str, ToolMetadata] = {}
self._executors: Dict[str, Callable] = {}
self._configs: Dict[str, dict] = {}
self._lock = asyncio.Lock()
# === Registration methods ===
async def register(
self,
manifest_path: str,
executor: Callable,
config: Optional[dict] = None,
) -> ToolMetadata:
"""Register a tool"""
import yaml
from pathlib import Path
manifest_file = Path(manifest_path)
if not manifest_file.exists():
raise FileNotFoundError(f"Manifest not found: {manifest_path}")
with open(manifest_file, encoding="utf-8") as f:
data = yaml.safe_load(f)
from tools.schemas.validator import validate_manifest
manifest = validate_manifest(data)
metadata = ToolMetadata(
name=manifest.name,
display_name=manifest.display_name,
description=manifest.description,
version=manifest.version,
author=manifest.author,
tags=manifest.tags or [],
dependencies=manifest.dependencies or [],
enabled=manifest.enabled,
)
async with self._lock:
self._tools[manifest.name] = metadata
self._executors[manifest.name] = executor
if config:
self._configs[manifest.name] = config
return metadata
async def register_from_dict(
self,
manifest_data: dict,
executor: Callable,
config: Optional[dict] = None,
) -> ToolMetadata:
"""Register a tool from manifest data dict"""
from tools.schemas.validator import validate_manifest
manifest = validate_manifest(manifest_data)
metadata = ToolMetadata(
name=manifest.name,
display_name=manifest.display_name,
description=manifest.description,
version=manifest.version,
author=manifest.author,
tags=manifest.tags or [],
dependencies=manifest.dependencies or [],
enabled=manifest.enabled,
)
async with self._lock:
self._tools[manifest.name] = metadata
self._executors[manifest.name] = executor
if config:
self._configs[manifest.name] = config
return metadata
async def unregister(self, name: str) -> bool:
"""Unregister a tool"""
async with self._lock:
if name in self._tools:
del self._tools[name]
del self._executors[name]
self._configs.pop(name, None)
return True
return False
async def enable(self, name: str) -> None:
"""Enable a tool"""
async with self._lock:
if name in self._tools:
self._tools[name].enabled = True
async def disable(self, name: str) -> None:
"""Disable a tool"""
async with self._lock:
if name in self._tools:
self._tools[name].enabled = False
# === Query methods ===
async def get(self, name: str) -> Optional[ToolMetadata]:
"""Get tool metadata"""
return self._tools.get(name)
async def get_executor(self, name: str) -> Optional[Callable]:
"""Get tool executor"""
return self._executors.get(name)
async def get_config(self, name: str) -> dict:
"""Get tool configuration"""
return self._configs.get(name, {})
async def list_all(self) -> List[ToolMetadata]:
"""List all tools"""
return list(self._tools.values())
async def list_enabled(self) -> List[ToolMetadata]:
"""List enabled tools"""
return [t for t in self._tools.values() if t.enabled]
async def list_by_tag(self, tag: str) -> List[ToolMetadata]:
"""List tools by tag"""
return [t for t in self._tools.values() if tag in t.tags]
async def search(self, query: str) -> List[ToolMetadata]:
"""Search tools"""
query_lower = query.lower()
return [
t
for t in self._tools.values()
if query_lower in t.name.lower()
or query_lower in t.description.lower()
or query_lower in t.display_name.lower()
]
# === Statistics methods ===
async def record_call(
self,
name: str,
duration_ms: int,
error: bool = False,
) -> None:
"""Record a tool call"""
async with self._lock:
if name in self._tools:
tool = self._tools[name]
tool.call_count += 1
tool.total_duration_ms += duration_ms
if error:
tool.error_count += 1
async def get_stats(self) -> dict:
"""Get registry statistics"""
tools = list(self._tools.values())
return {
"total_tools": len(tools),
"enabled_tools": sum(1 for t in tools if t.enabled),
"total_calls": sum(t.call_count for t in tools),
"total_errors": sum(t.error_count for t in tools),
"avg_error_rate": sum(t.error_rate for t in tools) / len(tools) if tools else 0,
}
# Global registry instance
_registry: Optional[ToolRegistry] = None
def get_registry() -> ToolRegistry:
"""Get the global tool registry instance"""
global _registry
if _registry is None:
_registry = ToolRegistry()
return _registry

View File

@@ -0,0 +1,16 @@
"""
Runtime Module
Multi-runtime support for tool execution:
- Python runtime: native Python execution
- JavaScript runtime: Node.js stdio protocol
- Native runtime: binary execution
"""
from tools.runtime.base import BaseRuntime
from tools.runtime.manager import RuntimeManager
__all__ = [
"BaseRuntime",
"RuntimeManager",
]

View File

@@ -0,0 +1,33 @@
"""
Base Runtime
Abstract base class for all tool runtimes.
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional
class BaseRuntime(ABC):
"""Runtime abstract base class"""
@abstractmethod
async def execute(
self,
entry: str,
command: str,
parameters: Dict[str, Any],
timeout: int,
) -> Dict[str, Any]:
"""Execute a tool"""
pass
@abstractmethod
async def validate(self, entry: str) -> bool:
"""Validate if the tool is available"""
pass
@abstractmethod
def get_name(self) -> str:
"""Get runtime name"""
pass

View File

@@ -0,0 +1,125 @@
"""
JavaScript Runtime
Node.js stdio protocol runtime for JavaScript tools.
"""
import asyncio
import json
import shutil
from pathlib import Path
from typing import Any, Dict, Optional
from tools.runtime.base import BaseRuntime
class JavaScriptRuntime(BaseRuntime):
"""JavaScript runtime using Node.js stdio protocol"""
def __init__(self, node_path: Optional[str] = None):
self.node_path = node_path or self._detect_node()
self._validated: bool = False
def get_name(self) -> str:
return "javascript"
def _detect_node(self) -> str:
"""Detect Node.js executable path"""
node = shutil.which("node")
if node:
return node
# Fallback for Windows
return "node"
async def validate(self, entry: str) -> bool:
"""Validate Node.js runtime and entry file"""
# Check node is available
try:
result = await asyncio.create_subprocess_exec(
self.node_path,
"--version",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
await result.wait()
if result.returncode != 0:
return False
except Exception:
return False
# Check entry file exists
path = Path(entry)
if not path.exists():
return False
self._validated = True
return True
async def execute(
self,
entry: str,
command: str,
parameters: Dict[str, Any],
timeout: int,
) -> Dict[str, Any]:
"""Execute a JavaScript tool via stdio protocol"""
if not self._validated:
is_valid = await self.validate(entry)
if not is_valid:
return {
"status": "error",
"error": "JavaScript runtime not available or entry file invalid",
}
# Build input data per stdio protocol
input_data = {
"command": command,
"parameters": parameters,
}
try:
process = await asyncio.create_subprocess_exec(
self.node_path,
entry,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(
process.communicate(input=json.dumps(input_data).encode()),
timeout=timeout / 1000,
)
if process.returncode != 0:
return {
"status": "error",
"error": stderr.decode() if stderr else "Unknown error",
}
result_text = stdout.decode()
if not result_text:
return {
"status": "error",
"error": "Empty response from Node.js runtime",
}
try:
result = json.loads(result_text)
return result
except json.JSONDecodeError:
return {
"status": "success",
"result": result_text,
}
except asyncio.TimeoutError:
return {
"status": "error",
"error": f"Execution timed out after {timeout}ms",
}
except Exception as e:
return {
"status": "error",
"error": str(e),
}

View File

@@ -0,0 +1,79 @@
"""
Runtime Manager
Manages multiple runtimes and routes tool execution to the appropriate runtime.
"""
from typing import Any, Dict, Optional
from tools.runtime.base import BaseRuntime
from tools.runtime.python_runtime import PythonRuntime
from tools.runtime.js_runtime import JavaScriptRuntime
from tools.runtime.native_runtime import NativeRuntime
class RuntimeManager:
"""Runtime manager for multi-runtime tool execution"""
def __init__(self):
self._runtimes: Dict[str, BaseRuntime] = {
"python": PythonRuntime(),
"javascript": JavaScriptRuntime(),
"native": NativeRuntime(),
}
def get_runtime(self, name: str) -> Optional[BaseRuntime]:
"""Get runtime by name"""
return self._runtimes.get(name)
def register_runtime(self, name: str, runtime: BaseRuntime) -> None:
"""Register a custom runtime"""
self._runtimes[name] = runtime
async def execute(
self,
runtime_name: str,
entry: str,
command: str,
parameters: Dict[str, Any],
timeout: int = 30000,
) -> Dict[str, Any]:
"""Execute tool through the specified runtime"""
runtime = self.get_runtime(runtime_name)
if not runtime:
return {
"status": "error",
"error": f"Unknown runtime: {runtime_name}",
}
# Validate first
is_valid = await runtime.validate(entry)
if not is_valid:
return {
"status": "error",
"error": f"Validation failed for runtime {runtime_name}, entry: {entry}",
}
return await runtime.execute(entry, command, parameters, timeout)
def list_runtimes(self) -> list:
"""List all registered runtimes"""
return [
{
"name": name,
"available": runtime.get_name() == name,
}
for name, runtime in self._runtimes.items()
]
# Global runtime manager instance
_runtime_manager: Optional[RuntimeManager] = None
def get_runtime_manager() -> RuntimeManager:
"""Get the global runtime manager instance"""
global _runtime_manager
if _runtime_manager is None:
_runtime_manager = RuntimeManager()
return _runtime_manager

View File

@@ -0,0 +1,93 @@
"""
Native Runtime
Native binary execution runtime for system executables.
"""
import asyncio
import stat
from pathlib import Path
from typing import Any, Dict, List
from tools.runtime.base import BaseRuntime
class NativeRuntime(BaseRuntime):
"""Native binary execution runtime"""
def get_name(self) -> str:
return "native"
async def validate(self, entry: str) -> bool:
"""Validate native binary exists and is executable"""
path = Path(entry)
if not path.exists():
return False
# Check if file is executable
file_stat = path.stat()
executable_bit = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
if not (file_stat.st_mode & executable_bit):
return False
return True
async def execute(
self,
entry: str,
command: str,
parameters: Dict[str, Any],
timeout: int,
) -> Dict[str, Any]:
"""Execute a native binary"""
try:
# Build argument list
args = [entry, command] + self._format_args(parameters)
process = await asyncio.create_subprocess_exec(
*args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(
process.communicate(),
timeout=timeout / 1000,
)
if process.returncode != 0:
return {
"status": "error",
"error": stderr.decode() if stderr else f"Exit code: {process.returncode}",
}
return {
"status": "success",
"result": stdout.decode() if stdout else "",
}
except asyncio.TimeoutError:
return {
"status": "error",
"error": f"Execution timed out after {timeout}ms",
}
except Exception as e:
return {
"status": "error",
"error": str(e),
}
def _format_args(self, parameters: Dict[str, Any]) -> List[str]:
"""Format parameters as command-line arguments"""
args: List[str] = []
for key, value in parameters.items():
# Use --key=value format
if isinstance(value, bool):
if value:
args.append(f"--{key}")
elif isinstance(value, (list, tuple)):
for item in value:
args.extend([f"--{key}", str(item)])
else:
args.extend([f"--{key}", str(value)])
return args

View File

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

View File

@@ -0,0 +1,447 @@
"""
Tool Scheduler
Scheduled tool execution system with support for one-time, interval, and cron-based scheduling.
"""
import asyncio
import uuid
from datetime import datetime, timedelta
from enum import Enum
from typing import Any, Callable, Dict, Optional
from pydantic import BaseModel, Field
class ScheduleType(str, Enum):
"""Schedule type enumeration"""
ONCE = "once" # Single execution
INTERVAL = "interval" # Fixed interval
CRON = "cron" # Cron expression
class ScheduledTask(BaseModel):
"""Scheduled task model"""
id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
name: str
schedule_type: ScheduleType
schedule_value: str # datetime string / interval seconds / cron expression
tool_name: str
parameters: Dict[str, Any] = Field(default_factory=dict)
enabled: bool = True
last_run: Optional[datetime] = None
next_run: Optional[datetime] = None
run_count: int = 0
created_at: datetime = Field(default_factory=datetime.utcnow)
def to_dict(self) -> dict:
return {
"id": self.id,
"name": self.name,
"type": self.schedule_type.value,
"schedule_value": self.schedule_value,
"tool_name": self.tool_name,
"enabled": self.enabled,
"last_run": self.last_run.isoformat() if self.last_run else None,
"next_run": self.next_run.isoformat() if self.next_run else None,
"run_count": self.run_count,
}
class ToolScheduler:
"""Tool scheduler for automated execution"""
def __init__(self):
self._tasks: Dict[str, ScheduledTask] = {}
self._running: bool = False
self._loop_task: Optional[asyncio.Task] = None
self._executor: Optional[Callable] = None
def set_executor(self, executor: Callable) -> None:
"""Set the tool executor function
Args:
executor: Async callable that takes (tool_name, command, parameters)
"""
self._executor = executor
async def schedule(
self,
name: str,
schedule_type: ScheduleType,
schedule_value: str,
tool_name: str,
parameters: Optional[Dict[str, Any]] = None,
) -> str:
"""Create a scheduled task
Args:
name: Task name
schedule_type: Type of schedule
schedule_value: Schedule value (datetime/interval/cron)
tool_name: Tool to execute
parameters: Tool parameters
Returns:
Task ID
"""
task = ScheduledTask(
name=name,
schedule_type=schedule_type,
schedule_value=schedule_value,
tool_name=tool_name,
parameters=parameters or {},
)
task.next_run = self._calculate_next_run(task)
self._tasks[task.id] = task
# Start scheduler loop if not running
if not self._running:
await self.start()
return task.id
async def schedule_once(
self,
name: str,
run_at: str, # ISO datetime string
tool_name: str,
parameters: Optional[Dict[str, Any]] = None,
) -> str:
"""Schedule a one-time task
Args:
name: Task name
run_at: ISO datetime string
tool_name: Tool to execute
parameters: Tool parameters
Returns:
Task ID
"""
return await self.schedule(
name=name,
schedule_type=ScheduleType.ONCE,
schedule_value=run_at,
tool_name=tool_name,
parameters=parameters,
)
async def schedule_interval(
self,
name: str,
interval_seconds: int,
tool_name: str,
parameters: Optional[Dict[str, Any]] = None,
) -> str:
"""Schedule an interval-based recurring task
Args:
name: Task name
interval_seconds: Interval in seconds
tool_name: Tool to execute
parameters: Tool parameters
Returns:
Task ID
"""
return await self.schedule(
name=name,
schedule_type=ScheduleType.INTERVAL,
schedule_value=str(interval_seconds),
tool_name=tool_name,
parameters=parameters,
)
async def schedule_cron(
self,
name: str,
cron_expression: str,
tool_name: str,
parameters: Optional[Dict[str, Any]] = None,
) -> str:
"""Schedule a cron-based recurring task
Args:
name: Task name
cron_expression: Cron expression (5 fields: min hour day month weekday)
tool_name: Tool to execute
parameters: Tool parameters
Returns:
Task ID
"""
return await self.schedule(
name=name,
schedule_type=ScheduleType.CRON,
schedule_value=cron_expression,
tool_name=tool_name,
parameters=parameters,
)
def _calculate_next_run(self, task: ScheduledTask) -> Optional[datetime]:
"""Calculate the next run time for a task
Args:
task: The scheduled task
Returns:
Next run datetime or None if schedule type is invalid
"""
now = datetime.utcnow()
if task.schedule_type == ScheduleType.ONCE:
try:
return datetime.fromisoformat(task.schedule_value)
except ValueError:
return None
elif task.schedule_type == ScheduleType.INTERVAL:
try:
seconds = int(task.schedule_value)
return now + timedelta(seconds=seconds)
except ValueError:
return None
elif task.schedule_type == ScheduleType.CRON:
return self._parse_cron_next(task.schedule_value, now)
return None
def _parse_cron_next(self, cron_expr: str, base_time: datetime) -> datetime:
"""Parse cron expression and calculate next run
Args:
cron_expr: Cron expression (min hour day month weekday)
base_time: Reference datetime
Returns:
Next matching datetime
"""
try:
parts = cron_expr.split()
if len(parts) != 5:
return base_time + timedelta(hours=1)
minute, hour, day, month, weekday = parts
# Simple next-run calculation
# For production, use croniter library
next_time = base_time.replace(second=0, microsecond=0) + timedelta(minutes=1)
# Basic validation and advancement
max_iterations = 366 * 24 * 60 # 1 year of minutes max
for _ in range(max_iterations):
if self._cron_matches(next_time, minute, hour, day, month, weekday):
return next_time
next_time += timedelta(minutes=1)
return next_time
except Exception:
return base_time + timedelta(hours=1)
def _cron_matches(
self,
dt: datetime,
minute: str,
hour: str,
day: str,
month: str,
weekday: str,
) -> bool:
"""Check if datetime matches cron fields
Args:
dt: Datetime to check
minute: Minute field
hour: Hour field
day: Day of month field
month: Month field
weekday: Day of week field
Returns:
True if matches
"""
return (
self._cron_field_matches(dt.minute, minute)
and self._cron_field_matches(dt.hour, hour)
and self._cron_field_matches(dt.day, day)
and self._cron_field_matches(dt.month, month)
and self._cron_field_matches(dt.weekday(), weekday)
)
def _cron_field_matches(self, value: int, field: str) -> bool:
"""Check if a value matches a cron field
Args:
value: The actual value
field: Cron field (number, *, */n, n,m, n-m)
Returns:
True if matches
"""
if field == "*":
return True
if field.startswith("*/"):
try:
step = int(field[2:])
return value % step == 0
except ValueError:
return False
if "," in field:
return str(value) in field.split(",")
if "-" in field:
try:
start, end = field.split("-")
return start <= str(value) <= end
except ValueError:
return False
try:
return int(field) == value
except ValueError:
return False
async def start(self) -> None:
"""Start the scheduler loop"""
if self._running:
return
self._running = True
self._loop_task = asyncio.create_task(self._run_loop())
async def stop(self) -> None:
"""Stop the scheduler loop"""
self._running = False
if self._loop_task:
self._loop_task.cancel()
try:
await self._loop_task
except asyncio.CancelledError:
pass
self._loop_task = None
async def _run_loop(self) -> None:
"""Main scheduler loop"""
while self._running:
now = datetime.utcnow()
for task in list(self._tasks.values()):
if not task.enabled:
continue
if task.next_run and task.next_run <= now:
await self._execute_task(task)
await asyncio.sleep(1) # Check every second
async def _execute_task(self, task: ScheduledTask) -> None:
"""Execute a scheduled task
Args:
task: The task to execute
"""
if self._executor is None:
return
try:
await self._executor(
tool_name=task.tool_name,
command=task.parameters.get("command", ""),
parameters=task.parameters,
)
except Exception:
pass # Log error in production
# Update task state
task.last_run = datetime.utcnow()
task.run_count += 1
# Calculate next run or disable one-time tasks
if task.schedule_type != ScheduleType.ONCE:
task.next_run = self._calculate_next_run(task)
else:
task.enabled = False
async def cancel(self, task_id: str) -> bool:
"""Cancel a scheduled task
Args:
task_id: Task ID
Returns:
True if cancelled
"""
if task_id in self._tasks:
del self._tasks[task_id]
return True
return False
async def enable(self, task_id: str) -> bool:
"""Enable a scheduled task
Args:
task_id: Task ID
Returns:
True if enabled
"""
task = self._tasks.get(task_id)
if task:
task.enabled = True
task.next_run = self._calculate_next_run(task)
return True
return False
async def disable(self, task_id: str) -> bool:
"""Disable a scheduled task
Args:
task_id: Task ID
Returns:
True if disabled
"""
task = self._tasks.get(task_id)
if task:
task.enabled = False
return True
return False
async def list_tasks(self) -> list:
"""List all scheduled tasks
Returns:
List of task info dicts
"""
return [task.to_dict() for task in self._tasks.values()]
async def get_task(self, task_id: str) -> Optional[dict]:
"""Get a specific task
Args:
task_id: Task ID
Returns:
Task dict or None
"""
task = self._tasks.get(task_id)
return task.to_dict() if task else None
# Global scheduler instance
_scheduler: Optional[ToolScheduler] = None
def get_scheduler() -> ToolScheduler:
"""Get the global tool scheduler instance"""
global _scheduler
if _scheduler is None:
_scheduler = ToolScheduler()
return _scheduler

View File

@@ -0,0 +1,13 @@
# Schemas Module
from tools.schemas.manifest import ToolManifest, ToolType, RuntimeType, InvocationCommand
from tools.schemas.tool_call import ToolCallRequest, ToolCallResponse, ToolExecutionLog
__all__ = [
"ToolManifest",
"ToolType",
"RuntimeType",
"InvocationCommand",
"ToolCallRequest",
"ToolCallResponse",
"ToolExecutionLog",
]

View File

@@ -0,0 +1,70 @@
"""
Tool Manifest Schema
Defines the structure for tool manifest declarations following VCPToolBox plugin patterns.
"""
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
from enum import Enum
class ToolType(str, Enum):
"""Tool execution type"""
SYNC = "sync" # Synchronous execution
ASYNC = "async" # Asynchronous execution
SERVICE = "service" # Continuous running service
class RuntimeType(str, Enum):
"""Runtime type for tool execution"""
PYTHON = "python"
JAVASCRIPT = "javascript"
NATIVE = "native"
class InvocationCommand(BaseModel):
"""Command definition for tool invocation"""
name: str = Field(..., description="Command name")
description: str = Field(..., description="Command description (for AI)")
parameters: Optional[Dict[str, Any]] = Field(default=None, description="Parameters JSON Schema")
required: Optional[List[str]] = Field(default=None, description="Required parameter list")
example: Optional[str] = Field(default=None, description="Invocation example")
class ToolManifest(BaseModel):
"""Tool Manifest - declarative tool definition"""
manifest_version: str = Field(default="1.0.0", description="Manifest version")
name: str = Field(..., description="Tool name (English, unique)")
display_name: str = Field(..., description="Display name (Chinese)")
description: str = Field(..., description="Tool description")
author: Optional[str] = Field(default=None, description="Author")
version: str = Field(default="1.0.0", description="Version number")
# Execution configuration
type: ToolType = Field(default=ToolType.SYNC, description="Tool type")
runtime: RuntimeType = Field(default=RuntimeType.PYTHON, description="Runtime")
entry: str = Field(..., description="Execution entry (file path or command)")
timeout: int = Field(default=30000, description="Timeout (milliseconds)")
# Configuration
config_schema: Optional[Dict[str, Any]] = Field(
default=None, description="Configuration schema"
)
# Capabilities
commands: List[InvocationCommand] = Field(
default_factory=list, description="Available commands"
)
# Metadata
tags: Optional[List[str]] = Field(default=None, description="Tags")
dependencies: Optional[List[str]] = Field(default=None, description="Dependency tools")
enabled: bool = Field(default=True, description="Whether enabled")
class Config:
use_enum_values = True

View File

@@ -0,0 +1,46 @@
"""
Tool Call Schema
Defines request/response structures for tool invocation.
"""
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any, List
from datetime import datetime
class ToolCallRequest(BaseModel):
"""Tool call request"""
tool_name: str = Field(..., description="Tool name")
command: str = Field(..., description="Command name")
parameters: Dict[str, Any] = Field(default_factory=dict, description="Parameters")
timeout: Optional[int] = Field(default=None, description="Timeout override")
context: Optional[Dict[str, Any]] = Field(default=None, description="Context information")
class ToolCallResponse(BaseModel):
"""Tool call response"""
status: str = Field(..., description="Status: success/error")
result: Optional[Any] = Field(default=None, description="Execution result")
error: Optional[str] = Field(default=None, description="Error message")
message: Optional[str] = Field(default=None, description="AI-friendly message")
base64: Optional[str] = Field(default=None, description="Base64 data")
duration_ms: Optional[int] = Field(default=None, description="Execution duration")
timestamp: datetime = Field(default_factory=datetime.utcnow)
class ToolExecutionLog(BaseModel):
"""Tool execution log"""
id: str
tool_name: str
command: str
parameters: Dict[str, Any]
status: str
duration_ms: int
error: Optional[str] = None
user_id: Optional[str] = None
agent_id: Optional[str] = None
created_at: datetime = Field(default_factory=datetime.utcnow)

View File

@@ -0,0 +1,37 @@
"""
Schema Validator
Validates tool manifests and tool calls against their schemas.
"""
from pydantic import ValidationError
from tools.schemas.manifest import ToolManifest
from tools.schemas.tool_call import ToolCallRequest
class ManifestValidationError(Exception):
"""Manifest validation error"""
pass
class ToolCallValidationError(Exception):
"""Tool call validation error"""
pass
def validate_manifest(data: dict) -> ToolManifest:
"""Validate manifest data against ToolManifest schema"""
try:
return ToolManifest(**data)
except ValidationError as e:
raise ManifestValidationError(str(e))
def validate_tool_call(data: dict) -> ToolCallRequest:
"""Validate tool call request against ToolCallRequest schema"""
try:
return ToolCallRequest(**data)
except ValidationError as e:
raise ToolCallValidationError(str(e))