mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-04 09:07:20 +08:00
Make the desktop memory settings dynamic instead of hardcoded per
provider. The dropdown is now populated from discover_memory_providers()
(bundled + user-installed + pip) rather than a static enum, and each
provider's config panel is derived from its own get_config_schema() —
the same declaration `hermes memory setup` uses — so adding or porting a
provider is pure declaration with no bespoke UI, conditional, or
endpoint.
- memory_providers.py: reworked from a hand-written Hindsight registry
into a pure adapter (describe_provider + coerce_value) that normalizes
a provider's raw schema into typed fields — secret(+env), select,
boolean, typed text — carrying `when` conditionals, url, and required.
- MemoryProvider ABC: add optional read_current_config() (default {}),
the read-back mirror of save_config(). Ported mem0, hindsight, honcho,
holographic; the rest fall back to schema defaults safely.
- web_server GET/PUT /api/memory/providers/{name}/config now load the
live provider, derive its schema, write non-secrets via the provider's
own save_config() (each keeps its native storage), persist secrets to
the env store, and `when`-gate validation so hidden fields aren't
required or written. Secrets stay write-only (is_set only).
- ProviderConfigPanel: `when`-conditional visibility (handles Hindsight's
mode-gated duplicate keys), boolean toggle, and credential url links.
Dropdown driven by getMemoryStatus(); hardcoded enum removed.
Tests assert the mapping contract and endpoint behavior (schema
derivation, save-via-save_config, secret-never-returned, when-gating,
select rejection) against real bundled providers rather than a snapshot
of a hardcoded list.
130 lines
3.8 KiB
Python
130 lines
3.8 KiB
Python
"""Tests for the schema-driven memory-provider config adapter.
|
|
|
|
These assert the *mapping contract* (how a provider's declared
|
|
``get_config_schema()`` becomes the normalized desktop field shape) and the
|
|
write-time coercion — not a snapshot of any particular provider's fields, which
|
|
are free to change.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from hermes_cli.memory_providers import (
|
|
KIND_BOOLEAN,
|
|
KIND_SECRET,
|
|
KIND_SELECT,
|
|
KIND_TEXT,
|
|
VALUE_BOOL,
|
|
VALUE_INT,
|
|
coerce_value,
|
|
describe_provider,
|
|
)
|
|
|
|
|
|
def _by_key(provider):
|
|
return {f.key: f for f in provider.fields}
|
|
|
|
|
|
def test_secret_field_maps_to_secret_kind_bound_to_env():
|
|
provider = describe_provider(
|
|
"x",
|
|
[{"key": "api_key", "secret": True, "env_var": "X_API_KEY", "url": "https://x"}],
|
|
)
|
|
field = _by_key(provider)["api_key"]
|
|
|
|
assert field.kind == KIND_SECRET
|
|
assert field.is_secret is True
|
|
assert field.env_key == "X_API_KEY"
|
|
assert field.url == "https://x"
|
|
|
|
|
|
def test_choices_map_to_select_options():
|
|
provider = describe_provider(
|
|
"x", [{"key": "mode", "default": "cloud", "choices": ["cloud", "local"]}]
|
|
)
|
|
field = _by_key(provider)["mode"]
|
|
|
|
assert field.kind == KIND_SELECT
|
|
assert field.allowed_values() == {"cloud", "local"}
|
|
|
|
|
|
def test_bool_default_maps_to_boolean_kind():
|
|
provider = describe_provider("x", [{"key": "auto", "default": True}])
|
|
field = _by_key(provider)["auto"]
|
|
|
|
assert field.kind == KIND_BOOLEAN
|
|
assert field.value_type == VALUE_BOOL
|
|
assert field.default == "true"
|
|
|
|
|
|
def test_int_default_maps_to_text_with_int_value_type():
|
|
provider = describe_provider("x", [{"key": "tokens", "default": 4096}])
|
|
field = _by_key(provider)["tokens"]
|
|
|
|
assert field.kind == KIND_TEXT
|
|
assert field.value_type == VALUE_INT
|
|
assert field.default == "4096"
|
|
|
|
|
|
def test_when_clause_is_carried_through():
|
|
provider = describe_provider(
|
|
"x",
|
|
[
|
|
{"key": "mode", "default": "cloud", "choices": ["cloud", "local"]},
|
|
{"key": "api_url", "default": "u", "when": {"mode": "cloud"}},
|
|
],
|
|
)
|
|
api_url = _by_key(provider)["api_url"]
|
|
|
|
assert api_url.when == (("mode", "cloud"),)
|
|
assert api_url.when_matches({"mode": "cloud"}) is True
|
|
assert api_url.when_matches({"mode": "local"}) is False
|
|
|
|
|
|
def test_every_field_has_a_known_kind():
|
|
# Invariant: the adapter never emits a field the renderer can't handle.
|
|
provider = describe_provider(
|
|
"x",
|
|
[
|
|
{"key": "a", "secret": True, "env_var": "A"},
|
|
{"key": "b", "choices": ["1", "2"]},
|
|
{"key": "c", "default": True},
|
|
{"key": "d", "default": "text"},
|
|
],
|
|
)
|
|
known = {KIND_TEXT, KIND_SELECT, KIND_SECRET, KIND_BOOLEAN}
|
|
|
|
assert provider.fields # non-empty
|
|
assert all(f.kind in known for f in provider.fields)
|
|
|
|
|
|
def test_malformed_entries_are_skipped_not_fatal():
|
|
provider = describe_provider("x", ["nope", {}, {"key": ""}, {"key": "ok"}])
|
|
|
|
assert _by_key(provider).keys() == {"ok"}
|
|
|
|
|
|
def test_coerce_rejects_value_outside_select_options():
|
|
provider = describe_provider("x", [{"key": "mode", "choices": ["cloud", "local"]}])
|
|
field = _by_key(provider)["mode"]
|
|
|
|
with pytest.raises(ValueError):
|
|
coerce_value(field, "bogus")
|
|
|
|
|
|
def test_coerce_casts_bool_and_int_to_native_types():
|
|
provider = describe_provider(
|
|
"x", [{"key": "auto", "default": True}, {"key": "tokens", "default": 4096}]
|
|
)
|
|
fields = _by_key(provider)
|
|
|
|
assert coerce_value(fields["auto"], "true") is True
|
|
assert coerce_value(fields["auto"], "off") is False
|
|
assert coerce_value(fields["tokens"], "8000") == 8000
|
|
|
|
|
|
def test_coerce_empty_falls_back_to_default():
|
|
provider = describe_provider("x", [{"key": "name", "default": "hermes"}])
|
|
field = _by_key(provider)["name"]
|
|
|
|
assert coerce_value(field, "") == "hermes"
|