mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
feat(api-server): add GET /v1/skills and /v1/toolsets (#33016)
Lets external clients enumerate the agent's skills and resolved toolsets deterministically over the OpenAI-compatible API server, without standing up the dashboard web server or sending a chat message and asking the model to list them. - GET /v1/skills — list installed skills (name, description, category) - GET /v1/toolsets — list toolsets resolved for the api_server platform, with enabled/configured state and the concrete tool names each expands to - Both gated by API_SERVER_KEY (same Bearer scheme as every other /v1/* endpoint) - /v1/capabilities advertises both new endpoints Closes the gap a community user just hit asking how to list skills over REST when only the OpenAI-compatible server is running. Test plan - python -m pytest tests/gateway/test_api_server.py -k "Skills or Toolsets or Capabilities" -o 'addopts=' -q → 9/9 pass - python -m pytest tests/gateway/test_api_server.py -o 'addopts=' -q → 156/156 pass, no regressions - E2E: started a real adapter on an isolated HERMES_HOME with a fake skill installed; curl-equivalent calls to /v1/capabilities, /v1/skills, /v1/toolsets returned the expected JSON; unauthenticated calls returned 401 with the configured API_SERVER_KEY.
This commit is contained in:
parent
febc4cfec0
commit
25f43d38de
2 changed files with 243 additions and 0 deletions
|
|
@ -1101,9 +1101,98 @@ class APIServerAdapter(BasePlatformAdapter):
|
|||
"run_events": {"method": "GET", "path": "/v1/runs/{run_id}/events"},
|
||||
"run_approval": {"method": "POST", "path": "/v1/runs/{run_id}/approval"},
|
||||
"run_stop": {"method": "POST", "path": "/v1/runs/{run_id}/stop"},
|
||||
"skills": {"method": "GET", "path": "/v1/skills"},
|
||||
"toolsets": {"method": "GET", "path": "/v1/toolsets"},
|
||||
},
|
||||
})
|
||||
|
||||
async def _handle_skills(self, request: "web.Request") -> "web.Response":
|
||||
"""GET /v1/skills — list installed skills visible to the API-server agent.
|
||||
|
||||
Read-only listing intended for external clients that need to know
|
||||
which skills are available without sending a chat message and asking
|
||||
the model. Mirrors what the gateway/CLI surfaces through
|
||||
``/skills list``, but as a deterministic JSON payload.
|
||||
|
||||
Returns the same skill metadata (name, description, category) the
|
||||
skills hub uses internally. Disabled skills are excluded so the
|
||||
listing matches what the agent actually loads.
|
||||
"""
|
||||
auth_err = self._check_auth(request)
|
||||
if auth_err:
|
||||
return auth_err
|
||||
|
||||
try:
|
||||
from tools.skills_tool import _find_all_skills, _sort_skills
|
||||
skills = _sort_skills(_find_all_skills(skip_disabled=False))
|
||||
except Exception:
|
||||
logger.exception("GET /v1/skills failed")
|
||||
return web.json_response(
|
||||
_openai_error("Failed to enumerate skills", err_type="server_error"),
|
||||
status=500,
|
||||
)
|
||||
|
||||
return web.json_response({
|
||||
"object": "list",
|
||||
"data": skills,
|
||||
})
|
||||
|
||||
async def _handle_toolsets(self, request: "web.Request") -> "web.Response":
|
||||
"""GET /v1/toolsets — list toolsets and their resolved tools.
|
||||
|
||||
Returns the toolset surface the api_server platform actually exposes
|
||||
to its agent: each toolset's enabled/configured state plus the
|
||||
concrete tool names it expands to. This is the deterministic
|
||||
equivalent of what a client would otherwise have to recover by
|
||||
asking the model what tools it can call.
|
||||
"""
|
||||
auth_err = self._check_auth(request)
|
||||
if auth_err:
|
||||
return auth_err
|
||||
|
||||
try:
|
||||
from hermes_cli.config import load_config
|
||||
from hermes_cli.tools_config import (
|
||||
_get_effective_configurable_toolsets,
|
||||
_get_platform_tools,
|
||||
_toolset_has_keys,
|
||||
)
|
||||
from toolsets import resolve_toolset
|
||||
|
||||
config = load_config()
|
||||
enabled_toolsets = _get_platform_tools(
|
||||
config,
|
||||
"api_server",
|
||||
include_default_mcp_servers=False,
|
||||
)
|
||||
data: List[Dict[str, Any]] = []
|
||||
for name, label, desc in _get_effective_configurable_toolsets():
|
||||
try:
|
||||
tools = sorted(set(resolve_toolset(name)))
|
||||
except Exception:
|
||||
tools = []
|
||||
is_enabled = name in enabled_toolsets
|
||||
data.append({
|
||||
"name": name,
|
||||
"label": label,
|
||||
"description": desc,
|
||||
"enabled": is_enabled,
|
||||
"configured": _toolset_has_keys(name, config),
|
||||
"tools": tools,
|
||||
})
|
||||
except Exception:
|
||||
logger.exception("GET /v1/toolsets failed")
|
||||
return web.json_response(
|
||||
_openai_error("Failed to enumerate toolsets", err_type="server_error"),
|
||||
status=500,
|
||||
)
|
||||
|
||||
return web.json_response({
|
||||
"object": "list",
|
||||
"platform": "api_server",
|
||||
"data": data,
|
||||
})
|
||||
|
||||
async def _handle_chat_completions(self, request: "web.Request") -> "web.Response":
|
||||
"""POST /v1/chat/completions — OpenAI Chat Completions format."""
|
||||
auth_err = self._check_auth(request)
|
||||
|
|
@ -3492,6 +3581,8 @@ class APIServerAdapter(BasePlatformAdapter):
|
|||
self._app.router.add_get("/v1/health", self._handle_health)
|
||||
self._app.router.add_get("/v1/models", self._handle_models)
|
||||
self._app.router.add_get("/v1/capabilities", self._handle_capabilities)
|
||||
self._app.router.add_get("/v1/skills", self._handle_skills)
|
||||
self._app.router.add_get("/v1/toolsets", self._handle_toolsets)
|
||||
self._app.router.add_post("/v1/chat/completions", self._handle_chat_completions)
|
||||
self._app.router.add_post("/v1/responses", self._handle_responses)
|
||||
self._app.router.add_get("/v1/responses/{response_id}", self._handle_get_response)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue