mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(dashboard): persist Discord toolsets to Discord platform
This commit is contained in:
parent
c80b244b52
commit
3ffd8b3da0
6 changed files with 105 additions and 19 deletions
|
|
@ -165,6 +165,20 @@ def _toolset_allowed_for_platform(ts_key: str, platform: str) -> bool:
|
|||
return allowed is None or platform in allowed
|
||||
|
||||
|
||||
def _toolset_configuration_platform(ts_key: str, default: str = "cli") -> str:
|
||||
"""Return the platform a platform-less configuration UI should target.
|
||||
|
||||
Most configurable toolsets retain the historical desktop/CLI target. A
|
||||
toolset restricted away from that platform must instead be configured on
|
||||
one of its supported platforms; otherwise the shared save helper correctly
|
||||
drops it and the UI reports a successful no-op.
|
||||
"""
|
||||
allowed = _TOOLSET_PLATFORM_RESTRICTIONS.get(ts_key)
|
||||
if not allowed or default in allowed:
|
||||
return default
|
||||
return sorted(allowed)[0]
|
||||
|
||||
|
||||
def _get_effective_configurable_toolsets():
|
||||
"""Return CONFIGURABLE_TOOLSETS + any plugin-provided toolsets.
|
||||
|
||||
|
|
|
|||
|
|
@ -13700,29 +13700,43 @@ async def get_toolsets(profile: Optional[str] = None):
|
|||
from hermes_cli.tools_config import (
|
||||
_get_effective_configurable_toolsets,
|
||||
_get_platform_tools,
|
||||
_toolset_configuration_platform,
|
||||
_toolset_has_keys,
|
||||
gui_toolset_label,
|
||||
)
|
||||
from hermes_cli.platforms import platform_label
|
||||
from toolsets import resolve_toolset
|
||||
|
||||
with _profile_scope(profile):
|
||||
config = load_config()
|
||||
enabled_toolsets = _get_platform_tools(
|
||||
config,
|
||||
"cli",
|
||||
include_default_mcp_servers=False,
|
||||
)
|
||||
toolset_rows = _get_effective_configurable_toolsets()
|
||||
target_platforms = {
|
||||
_toolset_configuration_platform(name) for name, _, _ in toolset_rows
|
||||
}
|
||||
enabled_by_platform = {
|
||||
platform: _get_platform_tools(
|
||||
config,
|
||||
platform,
|
||||
include_default_mcp_servers=False,
|
||||
)
|
||||
for platform in target_platforms
|
||||
}
|
||||
result = []
|
||||
for name, label, desc in _get_effective_configurable_toolsets():
|
||||
for name, label, desc in toolset_rows:
|
||||
try:
|
||||
tools = sorted(set(resolve_toolset(name)))
|
||||
except Exception:
|
||||
tools = []
|
||||
is_enabled = name in enabled_toolsets
|
||||
target_platform = _toolset_configuration_platform(name)
|
||||
is_enabled = name in enabled_by_platform[target_platform]
|
||||
result.append({
|
||||
"name": name,
|
||||
"label": gui_toolset_label(label),
|
||||
"description": desc,
|
||||
"platform": target_platform,
|
||||
"platform_label": gui_toolset_label(
|
||||
platform_label(target_platform, target_platform)
|
||||
),
|
||||
"enabled": is_enabled,
|
||||
"available": is_enabled,
|
||||
"configured": _toolset_has_keys(name, config),
|
||||
|
|
@ -13738,34 +13752,46 @@ class ToolsetToggle(BaseModel):
|
|||
|
||||
@app.put("/api/tools/toolsets/{name}")
|
||||
async def toggle_toolset(name: str, body: ToolsetToggle, profile: Optional[str] = None):
|
||||
"""Enable/disable a configurable toolset for the desktop (cli) platform.
|
||||
"""Enable/disable a configurable toolset for its configuration platform.
|
||||
|
||||
Persists to ``platform_toolsets.cli`` via the same ``_save_platform_tools``
|
||||
helper the CLI ``hermes tools`` picker uses, so the GUI and CLI stay in
|
||||
lockstep. Scoped to ``body.profile`` when provided. Returns 400 for
|
||||
unknown toolset keys.
|
||||
Most toolsets persist to ``platform_toolsets.cli``. Platform-restricted
|
||||
toolsets instead target their supported platform (for example, Discord's
|
||||
native toolsets persist to ``platform_toolsets.discord``). The shared
|
||||
``_save_platform_tools`` helper keeps the GUI and CLI in lockstep. Scoped
|
||||
to ``body.profile`` when provided. Returns 400 for unknown toolset keys.
|
||||
"""
|
||||
from hermes_cli.tools_config import (
|
||||
_get_effective_configurable_toolsets,
|
||||
_get_platform_tools,
|
||||
_save_platform_tools,
|
||||
_toolset_configuration_platform,
|
||||
)
|
||||
|
||||
valid = {ts_key for ts_key, _, _ in _get_effective_configurable_toolsets()}
|
||||
if name not in valid:
|
||||
raise HTTPException(status_code=400, detail=f"Unknown toolset: {name}")
|
||||
|
||||
target_platform = _toolset_configuration_platform(name)
|
||||
with _profile_scope(body.profile or profile):
|
||||
config = load_config()
|
||||
enabled = set(
|
||||
_get_platform_tools(config, "cli", include_default_mcp_servers=False)
|
||||
_get_platform_tools(
|
||||
config,
|
||||
target_platform,
|
||||
include_default_mcp_servers=False,
|
||||
)
|
||||
)
|
||||
if body.enabled:
|
||||
enabled.add(name)
|
||||
else:
|
||||
enabled.discard(name)
|
||||
_save_platform_tools(config, "cli", enabled)
|
||||
return {"ok": True, "name": name, "enabled": body.enabled}
|
||||
_save_platform_tools(config, target_platform, enabled)
|
||||
return {
|
||||
"ok": True,
|
||||
"name": name,
|
||||
"platform": target_platform,
|
||||
"enabled": body.enabled,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/tools/toolsets/{name}/config")
|
||||
|
|
|
|||
|
|
@ -4792,6 +4792,8 @@ class TestNewEndpoints:
|
|||
"name": "web",
|
||||
"label": "Web Search & Scraping",
|
||||
"description": "web_search, web_extract",
|
||||
"platform": "cli",
|
||||
"platform_label": "CLI",
|
||||
"enabled": True,
|
||||
"available": True,
|
||||
"configured": False,
|
||||
|
|
@ -4801,6 +4803,8 @@ class TestNewEndpoints:
|
|||
"name": "skills",
|
||||
"label": "Skills",
|
||||
"description": "list, view, manage",
|
||||
"platform": "cli",
|
||||
"platform_label": "CLI",
|
||||
"enabled": True,
|
||||
"available": True,
|
||||
"configured": True,
|
||||
|
|
@ -4810,6 +4814,8 @@ class TestNewEndpoints:
|
|||
"name": "memory",
|
||||
"label": "Memory",
|
||||
"description": "persistent memory across sessions",
|
||||
"platform": "cli",
|
||||
"platform_label": "CLI",
|
||||
"enabled": False,
|
||||
"available": False,
|
||||
"configured": True,
|
||||
|
|
@ -4838,6 +4844,41 @@ class TestNewEndpoints:
|
|||
listing = {t["name"]: t for t in self.client.get("/api/tools/toolsets").json()}
|
||||
assert listing["x_search"]["enabled"] is False
|
||||
|
||||
def test_discord_toolsets_read_and_write_discord_platform(self):
|
||||
"""Platform-restricted toolsets must not be saved as successful CLI no-ops."""
|
||||
from hermes_cli.config import load_config
|
||||
|
||||
listing = {t["name"]: t for t in self.client.get("/api/tools/toolsets").json()}
|
||||
assert listing["discord"]["platform"] == "discord"
|
||||
assert listing["discord"]["platform_label"] == "Discord"
|
||||
assert listing["discord"]["enabled"] is False
|
||||
|
||||
resp = self.client.put("/api/tools/toolsets/discord", json={"enabled": True})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {
|
||||
"ok": True,
|
||||
"name": "discord",
|
||||
"platform": "discord",
|
||||
"enabled": True,
|
||||
}
|
||||
|
||||
config = load_config()
|
||||
assert "discord" in config["platform_toolsets"]["discord"]
|
||||
assert "discord" not in config["platform_toolsets"].get("cli", [])
|
||||
|
||||
listing = {t["name"]: t for t in self.client.get("/api/tools/toolsets").json()}
|
||||
assert listing["discord"]["enabled"] is True
|
||||
assert listing["discord_admin"]["enabled"] is False
|
||||
|
||||
resp = self.client.put(
|
||||
"/api/tools/toolsets/discord_admin", json={"enabled": True}
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
config = load_config()
|
||||
assert {"discord", "discord_admin"} <= set(
|
||||
config["platform_toolsets"]["discord"]
|
||||
)
|
||||
|
||||
def test_toggle_toolset_unknown_returns_400(self):
|
||||
resp = self.client.put(
|
||||
"/api/tools/toolsets/not_a_real_toolset", json={"enabled": True}
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ export function ToolsetConfigDrawer({ toolset, profile, onClose, onChanged }: Pr
|
|||
};
|
||||
|
||||
const labelText = toolset.label?.trim() || toolset.name;
|
||||
const platformText = toolset.platform_label?.trim() || toolset.platform;
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
|
|
@ -253,10 +254,12 @@ export function ToolsetConfigDrawer({ toolset, profile, onClose, onChanged }: Pr
|
|||
checked={enabled}
|
||||
onCheckedChange={(v) => void handleToggle(v)}
|
||||
disabled={toggling}
|
||||
aria-label="Enable toolset"
|
||||
aria-label={`Enable toolset for ${platformText}`}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{enabled ? "Enabled for the agent" : "Disabled"}
|
||||
{enabled
|
||||
? `Enabled for ${platformText}`
|
||||
: `Disabled for ${platformText}`}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -732,7 +732,7 @@ export const api = {
|
|||
getToolsets: (profile?: string) =>
|
||||
fetchJSON<ToolsetInfo[]>(`/api/tools/toolsets${profileQuery(profile)}`),
|
||||
toggleToolset: (name: string, enabled: boolean, profile?: string) =>
|
||||
fetchJSON<{ ok: boolean; name: string; enabled: boolean }>(
|
||||
fetchJSON<{ ok: boolean; name: string; platform: string; enabled: boolean }>(
|
||||
`/api/tools/toolsets/${encodeURIComponent(name)}`,
|
||||
{
|
||||
method: "PUT",
|
||||
|
|
@ -2205,6 +2205,8 @@ export interface ToolsetInfo {
|
|||
name: string;
|
||||
label: string;
|
||||
description: string;
|
||||
platform: string;
|
||||
platform_label: string;
|
||||
enabled: boolean;
|
||||
configured: boolean;
|
||||
tools: string[];
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ export default function SkillsPage() {
|
|||
/* ---- Refresh toolsets after a config change ---- */
|
||||
const refreshToolsets = async () => {
|
||||
try {
|
||||
const tsets = await api.getToolsets();
|
||||
const tsets = await api.getToolsets(selectedProfile || undefined);
|
||||
setToolsets(tsets);
|
||||
} catch {
|
||||
/* non-fatal: the drawer already toasted on the failing write */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue