diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index 425613cb5..e5e81fe6d 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -366,14 +366,20 @@ class APIServerAdapter(BasePlatformAdapter): Create an AIAgent instance using the gateway's runtime config. Uses _resolve_runtime_agent_kwargs() to pick up model, api_key, - base_url, etc. from config.yaml / env vars. + base_url, etc. from config.yaml / env vars. Toolsets are resolved + from config.yaml platform_toolsets.api_server (same as all other + gateway platforms), falling back to the hermes-api-server default. """ from run_agent import AIAgent - from gateway.run import _resolve_runtime_agent_kwargs, _resolve_gateway_model + from gateway.run import _resolve_runtime_agent_kwargs, _resolve_gateway_model, _load_gateway_config + from hermes_cli.tools_config import _get_platform_tools runtime_kwargs = _resolve_runtime_agent_kwargs() model = _resolve_gateway_model() + user_config = _load_gateway_config() + enabled_toolsets = sorted(_get_platform_tools(user_config, "api_server")) + max_iterations = int(os.getenv("HERMES_MAX_ITERATIONS", "90")) agent = AIAgent( @@ -383,6 +389,7 @@ class APIServerAdapter(BasePlatformAdapter): quiet_mode=True, verbose_logging=False, ephemeral_system_prompt=ephemeral_system_prompt or None, + enabled_toolsets=enabled_toolsets, session_id=session_id, platform="api_server", stream_delta_callback=stream_delta_callback, diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index 6f76c98ae..0f5390c87 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -134,6 +134,7 @@ PLATFORMS = { "homeassistant": {"label": "🏠 Home Assistant", "default_toolset": "hermes-homeassistant"}, "email": {"label": "📧 Email", "default_toolset": "hermes-email"}, "dingtalk": {"label": "💬 DingTalk", "default_toolset": "hermes-dingtalk"}, + "api_server": {"label": "🌐 API Server", "default_toolset": "hermes-api-server"}, } diff --git a/tests/gateway/test_api_server_toolset.py b/tests/gateway/test_api_server_toolset.py new file mode 100644 index 000000000..3b4ff254d --- /dev/null +++ b/tests/gateway/test_api_server_toolset.py @@ -0,0 +1,129 @@ +"""Tests for hermes-api-server toolset and API server tool availability.""" +import os +import json +from unittest.mock import patch, MagicMock + +import pytest + +from toolsets import resolve_toolset, get_toolset, validate_toolset + + +class TestHermesApiServerToolset: + """Tests for the hermes-api-server toolset definition.""" + + def test_toolset_exists(self): + ts = get_toolset("hermes-api-server") + assert ts is not None + + def test_toolset_validates(self): + assert validate_toolset("hermes-api-server") + + def test_toolset_includes_web_tools(self): + tools = resolve_toolset("hermes-api-server") + assert "web_search" in tools + assert "web_extract" in tools + + def test_toolset_includes_core_tools(self): + tools = resolve_toolset("hermes-api-server") + expected = [ + "terminal", "process", + "read_file", "write_file", "patch", "search_files", + "vision_analyze", "image_generate", + "execute_code", "delegate_task", + "todo", "memory", "session_search", "cronjob", + ] + for tool in expected: + assert tool in tools, f"Missing expected tool: {tool}" + + def test_toolset_includes_browser_tools(self): + tools = resolve_toolset("hermes-api-server") + for tool in ["browser_navigate", "browser_snapshot", "browser_click", + "browser_type", "browser_scroll", "browser_back", + "browser_press", "browser_close"]: + assert tool in tools, f"Missing browser tool: {tool}" + + def test_toolset_includes_homeassistant_tools(self): + tools = resolve_toolset("hermes-api-server") + for tool in ["ha_list_entities", "ha_get_state", "ha_list_services", "ha_call_service"]: + assert tool in tools, f"Missing HA tool: {tool}" + + def test_toolset_excludes_clarify(self): + tools = resolve_toolset("hermes-api-server") + assert "clarify" not in tools + + def test_toolset_excludes_send_message(self): + tools = resolve_toolset("hermes-api-server") + assert "send_message" not in tools + + def test_toolset_excludes_text_to_speech(self): + tools = resolve_toolset("hermes-api-server") + assert "text_to_speech" not in tools + + +class TestApiServerPlatformConfig: + def test_platforms_dict_includes_api_server(self): + from hermes_cli.tools_config import PLATFORMS + assert "api_server" in PLATFORMS + assert PLATFORMS["api_server"]["default_toolset"] == "hermes-api-server" + + +class TestApiServerAdapterToolset: + @patch("gateway.platforms.api_server.AIOHTTP_AVAILABLE", True) + def test_create_agent_reads_config_toolsets(self): + """API server resolves toolsets from config like all other platforms.""" + from gateway.platforms.api_server import APIServerAdapter + from gateway.config import PlatformConfig + + adapter = APIServerAdapter(PlatformConfig()) + + with patch("gateway.run._resolve_runtime_agent_kwargs") as mock_kwargs, \ + patch("gateway.run._resolve_gateway_model") as mock_model, \ + patch("gateway.run._load_gateway_config") as mock_config, \ + patch("run_agent.AIAgent") as mock_agent_cls: + + mock_kwargs.return_value = {"api_key": "test-key", "base_url": None, + "provider": None, "api_mode": None, + "command": None, "args": []} + mock_model.return_value = "test/model" + # No platform_toolsets override — should fall back to hermes-api-server default + mock_config.return_value = {} + mock_agent_cls.return_value = MagicMock() + + adapter._create_agent() + + mock_agent_cls.assert_called_once() + call_kwargs = mock_agent_cls.call_args + toolsets = call_kwargs.kwargs.get("enabled_toolsets") + assert isinstance(toolsets, list) + assert len(toolsets) > 0 + assert call_kwargs.kwargs.get("platform") == "api_server" + + @patch("gateway.platforms.api_server.AIOHTTP_AVAILABLE", True) + def test_create_agent_respects_config_override(self): + """User can override API server toolsets via platform_toolsets in config.yaml.""" + from gateway.platforms.api_server import APIServerAdapter + from gateway.config import PlatformConfig + + adapter = APIServerAdapter(PlatformConfig()) + + with patch("gateway.run._resolve_runtime_agent_kwargs") as mock_kwargs, \ + patch("gateway.run._resolve_gateway_model") as mock_model, \ + patch("gateway.run._load_gateway_config") as mock_config, \ + patch("run_agent.AIAgent") as mock_agent_cls: + + mock_kwargs.return_value = {"api_key": "test-key", "base_url": None, + "provider": None, "api_mode": None, + "command": None, "args": []} + mock_model.return_value = "test/model" + # User overrides with just web and terminal + mock_config.return_value = { + "platform_toolsets": {"api_server": ["web", "terminal"]} + } + mock_agent_cls.return_value = MagicMock() + + adapter._create_agent() + + mock_agent_cls.assert_called_once() + call_kwargs = mock_agent_cls.call_args + toolsets = call_kwargs.kwargs.get("enabled_toolsets") + assert sorted(toolsets) == ["terminal", "web"] diff --git a/toolsets.py b/toolsets.py index 1f6a0674d..c9f39e75f 100644 --- a/toolsets.py +++ b/toolsets.py @@ -248,6 +248,42 @@ TOOLSETS = { ], "includes": [] }, + + "hermes-api-server": { + "description": "OpenAI-compatible API server — full agent tools accessible via HTTP (no interactive UI tools like clarify or send_message)", + "tools": [ + # Web + "web_search", "web_extract", + # Terminal + process management + "terminal", "process", + # File manipulation + "read_file", "write_file", "patch", "search_files", + # Vision + image generation + "vision_analyze", "image_generate", + # MoA + "mixture_of_agents", + # Skills + "skills_list", "skill_view", "skill_manage", + # Browser automation + "browser_navigate", "browser_snapshot", "browser_click", + "browser_type", "browser_scroll", "browser_back", + "browser_press", "browser_close", "browser_get_images", + "browser_vision", "browser_console", + # Planning & memory + "todo", "memory", + # Session history search + "session_search", + # Code execution + delegation + "execute_code", "delegate_task", + # Cronjob management + "cronjob", + # Home Assistant smart home control (gated on HASS_TOKEN via check_fn) + "ha_list_entities", "ha_get_state", "ha_list_services", "ha_call_service", + # Honcho memory tools (gated on honcho being active via check_fn) + "honcho_context", "honcho_profile", "honcho_search", "honcho_conclude", + ], + "includes": [] + }, "hermes-cli": { "description": "Full interactive CLI toolset - all default tools plus cronjob management",