mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-26 01:01:40 +00:00
Add inference.sh CLI (infsh) as a tool integration, giving agents access to 150+ AI apps through a single CLI — image gen (FLUX, Reve, Seedream), video (Veo, Wan, Seedance), LLMs, search (Tavily, Exa), 3D, avatar/lipsync, and more. One API key manages all services. Tools: - infsh: run any infsh CLI command (app list, app run, etc.) - infsh_install: install the CLI if not present Registered as an 'inference' toolset (opt-in, not in core tools). Includes comprehensive skill docs with examples for all app categories. Changes from original PR: - NOT added to _HERMES_CORE_TOOLS (available via --toolsets inference) - Added 12 tests covering tool registration, command execution, error handling, timeout, JSON parsing, and install flow Inspired by PR #1021 by @okaris. Co-authored-by: okaris <okaris@users.noreply.github.com>
302 lines
9.4 KiB
Python
302 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Inference.sh Tool Module
|
|
|
|
A simple tool for running AI apps via the inference.sh CLI (infsh).
|
|
Provides two functions:
|
|
- infsh_install: Install the infsh CLI
|
|
- infsh: Run any infsh command
|
|
|
|
This is a lightweight wrapper that gives agents direct access to 150+ AI apps
|
|
including image generation (FLUX, Reve), video (Veo, Wan), LLMs, search, and more.
|
|
|
|
Usage:
|
|
from tools.infsh_tool import infsh_tool, infsh_install
|
|
|
|
# Install the CLI
|
|
result = infsh_install()
|
|
|
|
# Search for apps first (always do this!)
|
|
result = infsh_tool("app list --search flux")
|
|
|
|
# Run an app
|
|
result = infsh_tool("app run falai/flux-dev-lora --input '{\"prompt\": \"a cat\"}' --json")
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from typing import Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configuration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
DEFAULT_TIMEOUT = 300 # 5 minutes for long-running AI tasks
|
|
INSTALL_TIMEOUT = 60
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Availability check
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def check_infsh_requirements() -> bool:
|
|
"""Check if infsh is available in PATH."""
|
|
return shutil.which("infsh") is not None
|
|
|
|
|
|
def _get_infsh_path() -> Optional[str]:
|
|
"""Get the path to infsh binary."""
|
|
return shutil.which("infsh")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Install function
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def infsh_install() -> str:
|
|
"""
|
|
Install the inference.sh CLI.
|
|
|
|
Downloads and installs the infsh binary using the official installer script.
|
|
The installer detects OS/arch, downloads the correct binary, verifies checksum,
|
|
and places it in PATH.
|
|
|
|
Returns:
|
|
JSON string with success/error status
|
|
"""
|
|
try:
|
|
# Check if already installed
|
|
if check_infsh_requirements():
|
|
infsh_path = _get_infsh_path()
|
|
# Get version
|
|
version_result = subprocess.run(
|
|
["infsh", "--version"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10
|
|
)
|
|
version = version_result.stdout.strip() if version_result.returncode == 0 else "unknown"
|
|
return json.dumps({
|
|
"success": True,
|
|
"message": f"infsh is already installed at {infsh_path}",
|
|
"version": version,
|
|
"already_installed": True
|
|
})
|
|
|
|
# Run the installer
|
|
result = subprocess.run(
|
|
["sh", "-c", "curl -fsSL https://cli.inference.sh | sh"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=INSTALL_TIMEOUT,
|
|
env={**os.environ, "NONINTERACTIVE": "1"}
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
return json.dumps({
|
|
"success": False,
|
|
"error": f"Installation failed: {result.stderr}",
|
|
"stdout": result.stdout
|
|
})
|
|
|
|
# Verify installation
|
|
if not check_infsh_requirements():
|
|
return json.dumps({
|
|
"success": False,
|
|
"error": "Installation completed but infsh not found in PATH. You may need to restart your shell or add ~/.local/bin to PATH.",
|
|
"stdout": result.stdout
|
|
})
|
|
|
|
return json.dumps({
|
|
"success": True,
|
|
"message": "infsh installed successfully",
|
|
"stdout": result.stdout,
|
|
"next_step": "Run 'infsh login' to authenticate, or set INFSH_API_KEY environment variable"
|
|
})
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return json.dumps({
|
|
"success": False,
|
|
"error": f"Installation timed out after {INSTALL_TIMEOUT}s"
|
|
})
|
|
except Exception as e:
|
|
logger.exception("infsh_install error: %s", e)
|
|
return json.dumps({
|
|
"success": False,
|
|
"error": f"Installation error: {type(e).__name__}: {e}"
|
|
})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main tool function
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def infsh_tool(
|
|
command: str,
|
|
timeout: Optional[int] = None,
|
|
) -> str:
|
|
"""
|
|
Execute an infsh CLI command.
|
|
|
|
Args:
|
|
command: The infsh command to run (without the 'infsh' prefix).
|
|
Examples: "app list", "app run falai/flux-schnell --input '{}'"
|
|
timeout: Command timeout in seconds (default: 300)
|
|
|
|
Returns:
|
|
JSON string with output, exit_code, and error fields
|
|
"""
|
|
try:
|
|
effective_timeout = timeout or DEFAULT_TIMEOUT
|
|
|
|
# Check if infsh is installed
|
|
if not check_infsh_requirements():
|
|
return json.dumps({
|
|
"success": False,
|
|
"error": "infsh CLI is not installed. Use infsh_install to install it first.",
|
|
"hint": "Call the infsh_install tool to set up the CLI"
|
|
})
|
|
|
|
# Build the full command
|
|
full_command = f"infsh {command}"
|
|
|
|
# Execute
|
|
result = subprocess.run(
|
|
full_command,
|
|
shell=True,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=effective_timeout,
|
|
env=os.environ
|
|
)
|
|
|
|
output = result.stdout
|
|
error = result.stderr
|
|
|
|
# Try to parse JSON output if present
|
|
parsed_output = None
|
|
if output.strip():
|
|
try:
|
|
parsed_output = json.loads(output)
|
|
except json.JSONDecodeError:
|
|
pass # Not JSON, keep as string
|
|
|
|
response = {
|
|
"success": result.returncode == 0,
|
|
"exit_code": result.returncode,
|
|
"output": parsed_output if parsed_output is not None else output,
|
|
}
|
|
|
|
if error:
|
|
response["stderr"] = error
|
|
|
|
return json.dumps(response, indent=2)
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return json.dumps({
|
|
"success": False,
|
|
"error": f"Command timed out after {effective_timeout}s",
|
|
"hint": "For long-running tasks, consider using --no-wait flag"
|
|
})
|
|
except Exception as e:
|
|
logger.exception("infsh_tool error: %s", e)
|
|
return json.dumps({
|
|
"success": False,
|
|
"error": f"Execution error: {type(e).__name__}: {e}"
|
|
})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Registry
|
|
# ---------------------------------------------------------------------------
|
|
|
|
from tools.registry import registry
|
|
|
|
INFSH_TOOL_DESCRIPTION = """Run AI apps via inference.sh CLI. Access 150+ apps for image generation, video, LLMs, search, 3D, and more.
|
|
|
|
One API key for everything - manage all AI services (FLUX, Veo, Claude, Tavily, etc.) with a single inference.sh account. You can also bring your own API keys.
|
|
|
|
IMPORTANT: Always use 'app list --search <query>' first to find the exact app ID before running. App names change frequently.
|
|
|
|
Commands:
|
|
- app list --search <query>: Find apps (ALWAYS DO THIS FIRST)
|
|
- app run <app-id> --input '<json>' --json: Run an app
|
|
- app get <app-id>: Get app schema before running
|
|
|
|
Verified app examples (use --search to confirm current names):
|
|
- Image: google/nano-banana, google/nano-banana-pro, google/nano-banana-2, falai/flux-dev-lora, bytedance/seedream-5-lite, falai/reve, xai/grok-imagine-image
|
|
- Video: google/veo-3-1-fast, bytedance/seedance-1-5-pro, falai/wan-2-5
|
|
- Upscale: falai/topaz-image-upscaler
|
|
- Search: tavily/search-assistant, exa/search
|
|
- LLM: openrouter/claude-sonnet-45
|
|
|
|
Workflow: ALWAYS search first, then run:
|
|
1. app list --search image
|
|
2. app run falai/flux-dev-lora --input '{"prompt": "a sunset"}' --json"""
|
|
|
|
INFSH_SCHEMA = {
|
|
"name": "infsh",
|
|
"description": INFSH_TOOL_DESCRIPTION,
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"command": {
|
|
"type": "string",
|
|
"description": "The infsh command (without 'infsh' prefix). ALWAYS use 'app list --search <query>' first to find correct app IDs, then 'app run <id> --input <json> --json'"
|
|
},
|
|
"timeout": {
|
|
"type": "integer",
|
|
"description": "Max seconds to wait (default: 300). AI tasks like video generation may take 1-2 minutes.",
|
|
"minimum": 1
|
|
}
|
|
},
|
|
"required": ["command"]
|
|
}
|
|
}
|
|
|
|
INFSH_INSTALL_SCHEMA = {
|
|
"name": "infsh_install",
|
|
"description": "Install the inference.sh CLI (infsh). Downloads and installs the binary. Run this first if infsh is not available.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {},
|
|
"required": []
|
|
}
|
|
}
|
|
|
|
|
|
def _handle_infsh(args, **kw):
|
|
return infsh_tool(
|
|
command=args.get("command", ""),
|
|
timeout=args.get("timeout"),
|
|
)
|
|
|
|
|
|
def _handle_infsh_install(args, **kw):
|
|
return infsh_install()
|
|
|
|
|
|
# Register both tools under the "inference" toolset
|
|
registry.register(
|
|
name="infsh",
|
|
toolset="inference",
|
|
schema=INFSH_SCHEMA,
|
|
handler=_handle_infsh,
|
|
check_fn=check_infsh_requirements,
|
|
requires_env=[],
|
|
)
|
|
|
|
registry.register(
|
|
name="infsh_install",
|
|
toolset="inference",
|
|
schema=INFSH_INSTALL_SCHEMA,
|
|
handler=_handle_infsh_install,
|
|
check_fn=lambda: True, # Always available - it's the installer
|
|
requires_env=[],
|
|
)
|