JARVIS / tools /__init__.py
Khanna, Videh Rakesh Rakesh
feat: security hardening, transcription mode, 15 new features, Gradio UI
0191dfa
"""JARVIS Tool System — extensible tool registry."""
import json
import logging
import platform
from typing import Callable
_log = logging.getLogger("jarvis.tools")
TOOL_REGISTRY: dict[str, dict] = {}
# Tools that require macOS (osascript/subprocess) and should be delegated
# to a connected device when the server runs on Linux (HF Space / Render).
_MACOS_ONLY_TOOLS: set[str] = set()
IS_MACOS = platform.system() == "Darwin"
def tool(name: str, description: str, parameters: dict = None, macos_only: bool = False):
"""Decorator to register a tool.
Args:
macos_only: If True, this tool requires macOS (osascript, AppleScript, etc.)
and will be delegated to a connected device when running on Linux.
"""
def decorator(func: Callable):
TOOL_REGISTRY[name] = {
"name": name,
"description": description,
"parameters": parameters or {},
"function": func,
}
if macos_only:
_MACOS_ONLY_TOOLS.add(name)
return func
return decorator
async def _delegate_to_device(name: str, args: dict) -> str:
"""Delegate a macOS-only tool call to a connected device via WebSocket push."""
try:
from user_device_registry import list_devices, send_command_to_device
except ImportError:
return f"Error: {name} requires macOS but this server runs on Linux. No device delegation available."
# Find an online device to delegate to
devices = await list_devices("default")
online_devices = [d for d in devices if d.get("status") == "online"]
if not online_devices:
return (
f"Error: '{name}' requires macOS but this server runs in the cloud. "
f"No connected Mac device found to execute this command. "
f"Make sure the JARVIS listener is running on your Mac."
)
target = online_devices[0]
device_id = target.get("device_id", "")
alias = target.get("alias", "device")
# Try WebSocket push first (instant)
try:
# Import at call time to avoid circular imports
import importlib
server_mod = importlib.import_module("server")
push_fn = getattr(server_mod, "push_command_to_device", None)
if push_fn:
pushed = await push_fn(device_id, {
"action": "execute_tool",
"tool": name,
"args": args,
})
if pushed:
return f"Command '{name}' sent to {alias} for execution."
except Exception:
pass
# Fallback: queue command via REST
result = await send_command_to_device(
target_alias=alias,
command=json.dumps({"tool": name, "args": args}),
user_id="default",
)
if "error" in result:
return f"Error delegating '{name}' to {alias}: {result['error']}"
return f"Command '{name}' queued for execution on {alias}."
async def execute_tool(name: str, args: dict) -> str:
if name not in TOOL_REGISTRY:
return f"Error: Unknown tool '{name}'"
# If running on Linux (HF Space) and this tool needs macOS, delegate it
if not IS_MACOS and name in _MACOS_ONLY_TOOLS:
return await _delegate_to_device(name, args)
# Validate required parameters and basic types before calling
schema = TOOL_REGISTRY[name].get("parameters", {})
properties = schema.get("properties", {})
required = schema.get("required", [])
for req_param in required:
if req_param not in args:
return f"Error: {name} requires parameter '{req_param}'"
# Basic type coercion for common mismatches from LLM output
for param_name, param_value in list(args.items()):
if param_name in properties:
expected_type = properties[param_name].get("type")
if expected_type == "integer" and isinstance(param_value, str):
try:
args[param_name] = int(param_value)
except ValueError:
return f"Error: {name} parameter '{param_name}' must be an integer"
elif expected_type == "boolean" and isinstance(param_value, str):
args[param_name] = param_value.lower() in ("true", "1", "yes")
try:
result = TOOL_REGISTRY[name]["function"](**args)
if hasattr(result, "__await__"):
result = await result
return str(result)
except TypeError as e:
return f"Error executing {name}: invalid arguments"
except Exception as e:
_log.error(f"Tool {name} failed: {type(e).__name__}: {e}")
return f"Error executing {name}: an unexpected error occurred"
def register_macos_tools():
"""Register all macOS-only tool names.
Called after all tool modules are imported so TOOL_REGISTRY is populated.
These tools use osascript, AppleScript, or macOS-specific CLI commands.
On Linux (HF Space / Render), they get automatically delegated to a
connected Mac device via WebSocket push.
"""
macos_tools = {
# system_control.py — all 22 tools
"set_volume", "get_volume", "set_brightness", "toggle_dark_mode",
"wifi_control", "bluetooth_control", "do_not_disturb", "screenshot",
"media_control", "clipboard", "send_notification", "list_running_apps",
"quit_app", "set_timer", "lock_screen", "sleep_display", "empty_trash",
"announce", "open_url", "search_files", "create_reminder", "get_events",
# app_automation.py — all 36 tools
"spotify_play", "spotify_play_uri", "spotify_search", "spotify_queue",
"spotify_status", "spotify_control",
"notes_create", "notes_append", "notes_list", "notes_search",
"notes_read", "notes_delete",
"reminders_add", "reminders_list", "reminders_complete",
"calendar_create_event", "calendar_today",
"send_imessage", "read_messages",
"mail_compose", "mail_unread",
"browser_open", "browser_tabs", "browser_read_page",
"spotlight_search", "finder_open", "finder_move", "finder_copy", "trash_file",
"contacts_search", "maps_directions",
"app_keystroke", "app_menu_click", "app_window_manage", "textedit_create",
"photos_recent", "photos_search", "photos_albums", "photos_open",
"focus_status", "focus_set",
# builtin.py — macOS-specific subset
"open_app", "open_terminal", "run_in_terminal",
# vscode_tools.py — all 7 tools
"vscode_open", "vscode_open_terminal", "vscode_run_command",
"copilot_chat", "copilot_inline", "vscode_list_extensions", "vscode_diff",
# device_control.py — macOS-specific subset
"bluetooth_scan", "bluetooth_pair", "bluetooth_connect",
"bluetooth_disconnect", "bluetooth_info", "network_scan",
"bonjour_discover", "homekit_control", "homekit_list_shortcuts",
"find_my_devices", "siri_command", "airdrop_file",
}
_MACOS_ONLY_TOOLS.update(macos_tools)
def get_tool_definitions() -> list[dict]:
"""Get tool definitions formatted for LLM function calling."""
tools = []
for name, info in TOOL_REGISTRY.items():
tools.append({
"name": info["name"],
"description": info["description"],
"parameters": info["parameters"],
})
return tools
def get_tools_prompt() -> str:
"""Generate a tools description for the system prompt."""
lines = ["You have access to the following tools:\n"]
for name, info in TOOL_REGISTRY.items():
params = info["parameters"]
param_str = ", ".join(
f"{p}: {params['properties'][p].get('description', '')}"
for p in params.get("properties", {})
)
lines.append(f"- **{name}**({param_str}): {info['description']}")
lines.append(
"\n## TOOL CALL FORMAT (MANDATORY)\n"
"To use a tool, you MUST respond with a JSON block inside ```tool fences.\n"
"The format is EXACTLY:\n"
'```tool\n{"tool": "tool_name", "args": {"param": "value"}}\n```\n\n'
"## TOOL CALL EXAMPLES\n"
"User: \"Open Safari\"\n"
"Response: Opening Safari for you, sir.\n"
'```tool\n{"tool": "open_app", "args": {"app_name": "Safari"}}\n```\n\n'
"User: \"Open VS Code\"\n"
"Response: Launching Visual Studio Code now.\n"
'```tool\n{"tool": "open_app", "args": {"app_name": "Visual Studio Code"}}\n```\n\n'
"User: \"What's the weather in London?\"\n"
"Response: Let me check London's weather.\n"
'```tool\n{"tool": "get_weather", "args": {"location": "London"}}\n```\n\n'
"User: \"Turn on dark mode\"\n"
"Response: Switching to dark mode.\n"
'```tool\n{"tool": "toggle_dark_mode", "args": {"enable": true}}\n```\n\n'
"User: \"Set volume to 50\"\n"
"Response: Setting volume to 50%.\n"
'```tool\n{"tool": "set_volume", "args": {"level": 50}}\n```\n\n'
"## CRITICAL RULES\n"
"- ALWAYS emit a ```tool block when the user wants you to DO something (open app, play music, set volume, search, etc.)\n"
"- NEVER just describe what you would do — actually call the tool.\n"
"- You can include speech text BEFORE the tool block.\n"
"- You can chain multiple ```tool blocks in one response.\n"
"- After tool results come back, synthesize them into a natural response.\n"
"- If a command maps to ANY registered tool, USE IT. Do not respond with just text.\n"
)
return "\n".join(lines)