mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(memory): provider actions extension point
Providers with behavior beyond fields (validate a server, start a local
instance, link a CLI profile) previously had no mount besides forking the
panel. Let the schema declare actions and one generic endpoint run them:
ProviderAction on ProviderConfigSchema, POST
/api/memory/providers/{name}/actions/{action} dispatching to
ACTION_HANDLERS in the plugin's config_actions.py (path-loaded and
import-light, like the schema), profile-scoped and off the event loop.
Handlers get the submitted values dict, return a JSON-able result, and
raise ValueError for user-facing 400s. The panel renders declared actions
as generic buttons beside Save. No bundled provider declares actions yet.
This commit is contained in:
parent
76c063b3d9
commit
35099685be
9 changed files with 259 additions and 5 deletions
|
|
@ -89,6 +89,22 @@ class ProviderField:
|
|||
return {opt.value for opt in self.options}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderAction:
|
||||
"""A provider-specific operation exposed as a button on the config panel.
|
||||
|
||||
Declared here, implemented in the plugin's ``config_actions.py`` as an
|
||||
entry in ``ACTION_HANDLERS`` — ``{key: handler}`` where the handler takes
|
||||
the submitted field values dict and returns a JSON-able result dict.
|
||||
Like ``config_schema.py``, that file is loaded by path and must stay
|
||||
import-light; heavy imports belong inside the handler bodies.
|
||||
"""
|
||||
|
||||
key: str
|
||||
label: str
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderConfigSchema:
|
||||
"""A provider plugin's declared config surface."""
|
||||
|
|
@ -99,10 +115,14 @@ class ProviderConfigSchema:
|
|||
# Optional link to the provider's config docs, shown in the full-config modal.
|
||||
docs_url: str = ""
|
||||
fields: tuple[ProviderField, ...] = dataclass_field(default_factory=tuple)
|
||||
actions: tuple[ProviderAction, ...] = ()
|
||||
|
||||
def inline_fields(self) -> tuple[ProviderField, ...]:
|
||||
return tuple(f for f in self.fields if f.inline)
|
||||
|
||||
def action(self, key: str) -> ProviderAction | None:
|
||||
return next((a for a in self.actions if a.key == key), None)
|
||||
|
||||
|
||||
_SCHEMA_CACHE: dict[str, ProviderConfigSchema] = {}
|
||||
|
||||
|
|
@ -140,3 +160,30 @@ def get_provider_config_schema(name: str) -> ProviderConfigSchema | None:
|
|||
if schema is not None:
|
||||
_SCHEMA_CACHE[key] = schema
|
||||
return schema
|
||||
|
||||
|
||||
def get_provider_action_handler(name: str, action_key: str):
|
||||
"""Return the handler for a declared action, or ``None`` when missing.
|
||||
|
||||
Handlers live in ``ACTION_HANDLERS`` in the plugin's ``config_actions.py``,
|
||||
loaded by path like the schema. Not cached: actions are user-initiated and
|
||||
rare, and a fixed handler file must not need a restart to be picked up.
|
||||
"""
|
||||
|
||||
from plugins.memory import find_provider_dir
|
||||
|
||||
provider_dir = find_provider_dir(name)
|
||||
path = provider_dir / "config_actions.py" if provider_dir else None
|
||||
if path is None or not path.is_file():
|
||||
return None
|
||||
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(f"_hermes_memory_config_actions.{name}", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
except Exception:
|
||||
_log.exception("failed to load config actions for memory provider %r", name)
|
||||
return None
|
||||
|
||||
handlers = getattr(module, "ACTION_HANDLERS", None)
|
||||
return handlers.get(action_key) if isinstance(handlers, dict) else None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue