mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
refactor(memory): move provider config schemas into their plugins
Each provider now declares its config surface in config_schema.py inside its own plugin dir (plugins/memory/<name>/), loaded by file path like the plugins themselves so plugin __init__ imports never reach the web server. hermes_cli/memory_providers.py is gone; the shared field primitives and loader live in plugins/memory/config_schema.py, and the schema tests move to tests/plugins/memory/ alongside the other per-plugin suites.
This commit is contained in:
parent
15c8c34c91
commit
2a632807e0
7 changed files with 319 additions and 253 deletions
|
|
@ -69,11 +69,11 @@ from hermes_cli.config import (
|
|||
write_platform_config_field,
|
||||
_deep_merge,
|
||||
)
|
||||
from hermes_cli.memory_providers import (
|
||||
MemoryProvider,
|
||||
from plugins.memory.config_schema import (
|
||||
ProviderConfigSchema,
|
||||
ProviderField,
|
||||
STORAGE_HONCHO_HOST_BLOCK,
|
||||
get_memory_provider,
|
||||
get_provider_config_schema,
|
||||
)
|
||||
from gateway.status import (
|
||||
derive_gateway_busy,
|
||||
|
|
@ -3978,11 +3978,11 @@ def _serialize_field_value(field: ProviderField, value: Any) -> str:
|
|||
# — flat-json backend (default; reusable for simple providers) —
|
||||
|
||||
|
||||
def _flat_json_path(provider: MemoryProvider) -> Path:
|
||||
def _flat_json_path(provider: ProviderConfigSchema) -> Path:
|
||||
return get_hermes_home() / provider.name / "config.json"
|
||||
|
||||
|
||||
def _read_flat_json(provider: MemoryProvider) -> Dict[str, Any]:
|
||||
def _read_flat_json(provider: ProviderConfigSchema) -> Dict[str, Any]:
|
||||
path = _flat_json_path(provider)
|
||||
if not path.exists():
|
||||
return {}
|
||||
|
|
@ -4076,7 +4076,7 @@ def _honcho_field_is_set(field: ProviderField, raw: Dict[str, Any], host_block:
|
|||
return any(source.get(k) for source in sources for k in (field.key, *field.aliases))
|
||||
|
||||
|
||||
def _memory_provider_payload(provider: MemoryProvider) -> Dict[str, Any]:
|
||||
def _memory_provider_payload(provider: ProviderConfigSchema) -> Dict[str, Any]:
|
||||
fields: List[Dict[str, Any]] = []
|
||||
|
||||
if provider.storage == STORAGE_HONCHO_HOST_BLOCK:
|
||||
|
|
@ -4118,7 +4118,7 @@ def _memory_provider_payload(provider: MemoryProvider) -> Dict[str, Any]:
|
|||
return {"name": provider.name, "label": provider.label, "docs_url": provider.docs_url, "fields": fields}
|
||||
|
||||
|
||||
def _write_provider_flat(provider: MemoryProvider, values: Dict[str, str]) -> None:
|
||||
def _write_provider_flat(provider: ProviderConfigSchema, values: Dict[str, str]) -> None:
|
||||
from utils import atomic_json_write
|
||||
|
||||
existing = _read_flat_json(provider)
|
||||
|
|
@ -4144,7 +4144,7 @@ def _write_provider_flat(provider: MemoryProvider, values: Dict[str, str]) -> No
|
|||
atomic_json_write(path, existing, mode=0o600)
|
||||
|
||||
|
||||
def _write_provider_honcho(provider: MemoryProvider, values: Dict[str, str]) -> None:
|
||||
def _write_provider_honcho(provider: ProviderConfigSchema, values: Dict[str, str]) -> None:
|
||||
"""Persist submitted fields to Honcho's real config for the active host.
|
||||
|
||||
Only keys present in ``values`` are touched, so a partial save (e.g. the
|
||||
|
|
@ -4191,7 +4191,7 @@ def _write_provider_honcho(provider: MemoryProvider, values: Dict[str, str]) ->
|
|||
|
||||
@app.get("/api/memory/providers/{name}/config")
|
||||
async def get_memory_provider_config(name: str):
|
||||
provider = get_memory_provider(name)
|
||||
provider = get_provider_config_schema(name)
|
||||
if provider is None:
|
||||
# Undeclared providers (e.g. builtin) have no config surface. Return an
|
||||
# empty schema so the generic panel simply renders nothing.
|
||||
|
|
@ -4201,7 +4201,7 @@ async def get_memory_provider_config(name: str):
|
|||
|
||||
@app.put("/api/memory/providers/{name}/config")
|
||||
async def update_memory_provider_config(name: str, body: MemoryProviderConfigUpdate):
|
||||
provider = get_memory_provider(name)
|
||||
provider = get_provider_config_schema(name)
|
||||
if provider is None:
|
||||
raise HTTPException(status_code=404, detail=f"Unknown memory provider: {name}")
|
||||
|
||||
|
|
|
|||
138
plugins/memory/config_schema.py
Normal file
138
plugins/memory/config_schema.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"""Declarative configuration schema for memory provider plugins.
|
||||
|
||||
Each memory provider plugin *declares* its configurable surface in a
|
||||
``config_schema.py`` next to its ``__init__.py`` — the fields, their types,
|
||||
which values are secrets, and (for selects) the allowed options. A single
|
||||
generic renderer in the desktop UI and a single generic ``GET/PUT
|
||||
/api/memory/providers/{name}/config`` endpoint pair drive the whole
|
||||
experience, so adding a provider config surface is pure declaration with no
|
||||
bespoke UI components.
|
||||
|
||||
Schema files are loaded by path (like the provider plugins themselves), never
|
||||
via package import: plugin ``__init__.py`` files pull in the agent runtime,
|
||||
which must not load into the web server. A ``config_schema.py`` may only
|
||||
import from this module.
|
||||
|
||||
This module is intentionally pure data: it imports nothing from the
|
||||
config/env layer. ``web_server`` owns the generic read/write logic that
|
||||
interprets these declarations, dispatching on ``ProviderConfigSchema.storage``
|
||||
to the matching backend.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import logging
|
||||
from dataclasses import dataclass, field as dataclass_field
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
# Field kinds understood by the generic renderer.
|
||||
KIND_TEXT = "text"
|
||||
KIND_SELECT = "select"
|
||||
KIND_SECRET = "secret"
|
||||
KIND_BOOL = "bool"
|
||||
KIND_NUMBER = "number"
|
||||
KIND_JSON = "json"
|
||||
|
||||
# Storage backends understood by web_server. ``flat_json`` persists non-secret
|
||||
# fields to ``<hermes_home>/<provider>/config.json``; ``honcho_host_block``
|
||||
# targets Honcho's real config (honcho.json, scoped to the active profile host).
|
||||
STORAGE_FLAT_JSON = "flat_json"
|
||||
STORAGE_HONCHO_HOST_BLOCK = "honcho_host_block"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderFieldOption:
|
||||
"""A single choice for a ``select`` field."""
|
||||
|
||||
value: str
|
||||
label: str
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderField:
|
||||
"""One configurable field on a memory provider.
|
||||
|
||||
A field is stored in exactly one place, decided by ``kind``:
|
||||
|
||||
* non-secret kinds — persisted to the provider's config via its storage
|
||||
backend under ``key``.
|
||||
* ``secret`` — persisted to the env store under ``env_key`` and never read
|
||||
back out over the API (only an ``is_set`` flag is surfaced).
|
||||
|
||||
``aliases`` and ``env_fallbacks`` let a field read legacy values written by
|
||||
earlier CLI/env setup without re-introducing per-provider code. ``inline``
|
||||
marks the curated subset shown in the compact panel; the rest surface only
|
||||
in the full-config modal. ``group`` buckets fields within that modal.
|
||||
"""
|
||||
|
||||
key: str
|
||||
label: str
|
||||
kind: str = KIND_TEXT
|
||||
default: str = ""
|
||||
description: str = ""
|
||||
placeholder: str = ""
|
||||
options: tuple[ProviderFieldOption, ...] = ()
|
||||
env_key: str | None = None
|
||||
aliases: tuple[str, ...] = ()
|
||||
env_fallbacks: tuple[str, ...] = ()
|
||||
inline: bool = False
|
||||
group: str = ""
|
||||
# Where a host-block backend stores the field: "host" (per-profile host
|
||||
# block) or "root" (config root). Ignored by the flat-json backend.
|
||||
scope: str = "host"
|
||||
|
||||
@property
|
||||
def is_secret(self) -> bool:
|
||||
return self.kind == KIND_SECRET
|
||||
|
||||
def allowed_values(self) -> set[str]:
|
||||
return {opt.value for opt in self.options}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderConfigSchema:
|
||||
"""A provider plugin's declared config surface."""
|
||||
|
||||
name: str
|
||||
label: str
|
||||
storage: str = STORAGE_FLAT_JSON
|
||||
# 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)
|
||||
|
||||
def inline_fields(self) -> tuple[ProviderField, ...]:
|
||||
return tuple(f for f in self.fields if f.inline)
|
||||
|
||||
|
||||
_SCHEMA_CACHE: dict[str, ProviderConfigSchema | None] = {}
|
||||
|
||||
|
||||
def get_provider_config_schema(name: str) -> ProviderConfigSchema | None:
|
||||
"""Return the ``CONFIG_SCHEMA`` declared by the provider plugin ``name``.
|
||||
|
||||
Providers without a ``config_schema.py`` (e.g. ``builtin``) return ``None``
|
||||
and simply render no config panel.
|
||||
"""
|
||||
|
||||
if name in _SCHEMA_CACHE:
|
||||
return _SCHEMA_CACHE[name]
|
||||
|
||||
from plugins.memory import find_provider_dir
|
||||
|
||||
schema: ProviderConfigSchema | None = None
|
||||
provider_dir = find_provider_dir(name)
|
||||
path = provider_dir / "config_schema.py" if provider_dir else None
|
||||
if path is not None and path.is_file():
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(f"_hermes_memory_config_schema.{name}", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
schema = getattr(module, "CONFIG_SCHEMA", None)
|
||||
except Exception:
|
||||
_log.exception("failed to load config schema for memory provider %r", name)
|
||||
|
||||
_SCHEMA_CACHE[name] = schema
|
||||
return schema
|
||||
76
plugins/memory/hindsight/config_schema.py
Normal file
76
plugins/memory/hindsight/config_schema.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
"""Hindsight's declared config surface — rendered by the generic desktop panel."""
|
||||
|
||||
from plugins.memory.config_schema import (
|
||||
KIND_SECRET,
|
||||
KIND_SELECT,
|
||||
KIND_TEXT,
|
||||
ProviderConfigSchema,
|
||||
ProviderField,
|
||||
ProviderFieldOption,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = ProviderConfigSchema(
|
||||
name="hindsight",
|
||||
label="Hindsight",
|
||||
fields=(
|
||||
ProviderField(
|
||||
key="mode",
|
||||
label="Mode",
|
||||
kind=KIND_SELECT,
|
||||
default="cloud",
|
||||
description="How Hermes connects to Hindsight.",
|
||||
options=(
|
||||
ProviderFieldOption(
|
||||
"cloud",
|
||||
"Cloud",
|
||||
"Hindsight Cloud API (lightweight, just needs an API key)",
|
||||
),
|
||||
ProviderFieldOption(
|
||||
"local_external",
|
||||
"Local External",
|
||||
"Connect to an existing Hindsight instance",
|
||||
),
|
||||
),
|
||||
inline=True,
|
||||
),
|
||||
ProviderField(
|
||||
key="api_key",
|
||||
label="API key",
|
||||
kind=KIND_SECRET,
|
||||
env_key="HINDSIGHT_API_KEY",
|
||||
description="Used to authenticate with the Hindsight API.",
|
||||
placeholder="Enter Hindsight API key",
|
||||
inline=True,
|
||||
),
|
||||
ProviderField(
|
||||
key="api_url",
|
||||
label="API URL",
|
||||
kind=KIND_TEXT,
|
||||
default="https://api.hindsight.vectorize.io",
|
||||
aliases=("apiUrl",),
|
||||
env_fallbacks=("HINDSIGHT_API_URL",),
|
||||
inline=True,
|
||||
),
|
||||
ProviderField(
|
||||
key="bank_id",
|
||||
label="Bank ID",
|
||||
kind=KIND_TEXT,
|
||||
default="hermes",
|
||||
aliases=("bankId",),
|
||||
inline=True,
|
||||
),
|
||||
ProviderField(
|
||||
key="recall_budget",
|
||||
label="Recall budget",
|
||||
kind=KIND_SELECT,
|
||||
default="mid",
|
||||
aliases=("budget",),
|
||||
options=(
|
||||
ProviderFieldOption("low", "low"),
|
||||
ProviderFieldOption("mid", "mid"),
|
||||
ProviderFieldOption("high", "high"),
|
||||
),
|
||||
inline=True,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -1,98 +1,17 @@
|
|||
"""Declarative configuration schema for desktop memory providers.
|
||||
"""Honcho's declared config surface — rendered by the generic desktop panel."""
|
||||
|
||||
Each memory provider *declares* its configurable surface here — the fields, their
|
||||
types, which values are secrets, and (for selects) the allowed options. A single
|
||||
generic renderer in the desktop UI and a single generic ``GET/PUT
|
||||
/api/memory/providers/{name}/config`` endpoint pair drive the whole experience,
|
||||
so adding a new provider is pure declaration with no bespoke UI components.
|
||||
|
||||
This module is intentionally pure data: it imports nothing from the config/env
|
||||
layer. ``web_server`` owns the generic read/write logic that interprets these
|
||||
declarations, dispatching on ``MemoryProvider.storage`` to the matching backend.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field as dataclass_field
|
||||
|
||||
# Field kinds understood by the generic renderer.
|
||||
KIND_TEXT = "text"
|
||||
KIND_SELECT = "select"
|
||||
KIND_SECRET = "secret"
|
||||
KIND_BOOL = "bool"
|
||||
KIND_NUMBER = "number"
|
||||
KIND_JSON = "json"
|
||||
|
||||
# Storage backends understood by web_server. ``flat_json`` persists non-secret
|
||||
# fields to ``<hermes_home>/<provider>/config.json``; ``honcho_host_block``
|
||||
# targets Honcho's real config (honcho.json, scoped to the active profile host).
|
||||
STORAGE_FLAT_JSON = "flat_json"
|
||||
STORAGE_HONCHO_HOST_BLOCK = "honcho_host_block"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderFieldOption:
|
||||
"""A single choice for a ``select`` field."""
|
||||
|
||||
value: str
|
||||
label: str
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderField:
|
||||
"""One configurable field on a memory provider.
|
||||
|
||||
A field is stored in exactly one place, decided by ``kind``:
|
||||
|
||||
* non-secret kinds — persisted to the provider's config via its storage
|
||||
backend under ``key``.
|
||||
* ``secret`` — persisted to the env store under ``env_key`` and never read
|
||||
back out over the API (only an ``is_set`` flag is surfaced).
|
||||
|
||||
``aliases`` and ``env_fallbacks`` let a field read legacy values written by
|
||||
earlier CLI/env setup without re-introducing per-provider code. ``inline``
|
||||
marks the curated subset shown in the compact panel; the rest surface only
|
||||
in the full-config modal. ``group`` buckets fields within that modal.
|
||||
"""
|
||||
|
||||
key: str
|
||||
label: str
|
||||
kind: str = KIND_TEXT
|
||||
default: str = ""
|
||||
description: str = ""
|
||||
placeholder: str = ""
|
||||
options: tuple[ProviderFieldOption, ...] = ()
|
||||
env_key: str | None = None
|
||||
aliases: tuple[str, ...] = ()
|
||||
env_fallbacks: tuple[str, ...] = ()
|
||||
inline: bool = False
|
||||
group: str = ""
|
||||
# Where a host-block backend stores the field: "host" (per-profile host
|
||||
# block) or "root" (config root). Ignored by the flat-json backend.
|
||||
scope: str = "host"
|
||||
|
||||
@property
|
||||
def is_secret(self) -> bool:
|
||||
return self.kind == KIND_SECRET
|
||||
|
||||
def allowed_values(self) -> set[str]:
|
||||
return {opt.value for opt in self.options}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MemoryProvider:
|
||||
"""A declared memory provider and its configurable fields."""
|
||||
|
||||
name: str
|
||||
label: str
|
||||
storage: str = STORAGE_FLAT_JSON
|
||||
# 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)
|
||||
|
||||
def inline_fields(self) -> tuple[ProviderField, ...]:
|
||||
return tuple(f for f in self.fields if f.inline)
|
||||
from plugins.memory.config_schema import (
|
||||
KIND_BOOL,
|
||||
KIND_JSON,
|
||||
KIND_NUMBER,
|
||||
KIND_SECRET,
|
||||
KIND_SELECT,
|
||||
KIND_TEXT,
|
||||
STORAGE_HONCHO_HOST_BLOCK,
|
||||
ProviderConfigSchema,
|
||||
ProviderField,
|
||||
ProviderFieldOption,
|
||||
)
|
||||
|
||||
|
||||
# Reasoning effort levels shared by dialectic-related selects.
|
||||
|
|
@ -105,7 +24,7 @@ _REASONING_LEVELS = (
|
|||
)
|
||||
|
||||
|
||||
HONCHO = MemoryProvider(
|
||||
CONFIG_SCHEMA = ProviderConfigSchema(
|
||||
name="honcho",
|
||||
label="Honcho",
|
||||
storage=STORAGE_HONCHO_HOST_BLOCK,
|
||||
|
|
@ -387,84 +306,3 @@ HONCHO = MemoryProvider(
|
|||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
HINDSIGHT = MemoryProvider(
|
||||
name="hindsight",
|
||||
label="Hindsight",
|
||||
fields=(
|
||||
ProviderField(
|
||||
key="mode",
|
||||
label="Mode",
|
||||
kind=KIND_SELECT,
|
||||
default="cloud",
|
||||
description="How Hermes connects to Hindsight.",
|
||||
options=(
|
||||
ProviderFieldOption(
|
||||
"cloud",
|
||||
"Cloud",
|
||||
"Hindsight Cloud API (lightweight, just needs an API key)",
|
||||
),
|
||||
ProviderFieldOption(
|
||||
"local_external",
|
||||
"Local External",
|
||||
"Connect to an existing Hindsight instance",
|
||||
),
|
||||
),
|
||||
inline=True,
|
||||
),
|
||||
ProviderField(
|
||||
key="api_key",
|
||||
label="API key",
|
||||
kind=KIND_SECRET,
|
||||
env_key="HINDSIGHT_API_KEY",
|
||||
description="Used to authenticate with the Hindsight API.",
|
||||
placeholder="Enter Hindsight API key",
|
||||
inline=True,
|
||||
),
|
||||
ProviderField(
|
||||
key="api_url",
|
||||
label="API URL",
|
||||
kind=KIND_TEXT,
|
||||
default="https://api.hindsight.vectorize.io",
|
||||
aliases=("apiUrl",),
|
||||
env_fallbacks=("HINDSIGHT_API_URL",),
|
||||
inline=True,
|
||||
),
|
||||
ProviderField(
|
||||
key="bank_id",
|
||||
label="Bank ID",
|
||||
kind=KIND_TEXT,
|
||||
default="hermes",
|
||||
aliases=("bankId",),
|
||||
inline=True,
|
||||
),
|
||||
ProviderField(
|
||||
key="recall_budget",
|
||||
label="Recall budget",
|
||||
kind=KIND_SELECT,
|
||||
default="mid",
|
||||
aliases=("budget",),
|
||||
options=(
|
||||
ProviderFieldOption("low", "low"),
|
||||
ProviderFieldOption("mid", "mid"),
|
||||
ProviderFieldOption("high", "high"),
|
||||
),
|
||||
inline=True,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# Registry of providers that expose a desktop config surface. Providers without
|
||||
# an entry here (e.g. ``builtin``) simply render no config panel. Honcho leads.
|
||||
MEMORY_PROVIDERS: dict[str, MemoryProvider] = {
|
||||
HONCHO.name: HONCHO,
|
||||
HINDSIGHT.name: HINDSIGHT,
|
||||
}
|
||||
|
||||
|
||||
def get_memory_provider(name: str) -> MemoryProvider | None:
|
||||
"""Return the declared provider for ``name``, or ``None`` if undeclared."""
|
||||
|
||||
return MEMORY_PROVIDERS.get(name)
|
||||
16
tests/plugins/memory/test_config_schema.py
Normal file
16
tests/plugins/memory/test_config_schema.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""Tests for config-schema loading from memory provider plugin dirs."""
|
||||
|
||||
from plugins.memory.config_schema import get_provider_config_schema
|
||||
|
||||
|
||||
def test_unknown_provider_is_none():
|
||||
assert get_provider_config_schema("builtin") is None
|
||||
|
||||
|
||||
def test_plugin_without_schema_is_none():
|
||||
# mem0 is a real plugin dir that declares no config_schema.py.
|
||||
assert get_provider_config_schema("mem0") is None
|
||||
|
||||
|
||||
def test_schemas_are_cached_per_provider():
|
||||
assert get_provider_config_schema("honcho") is get_provider_config_schema("honcho")
|
||||
51
tests/plugins/memory/test_hindsight_config_schema.py
Normal file
51
tests/plugins/memory/test_hindsight_config_schema.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
"""Tests for Hindsight's declared config surface."""
|
||||
|
||||
from plugins.memory.config_schema import (
|
||||
KIND_SECRET,
|
||||
KIND_SELECT,
|
||||
get_provider_config_schema,
|
||||
)
|
||||
|
||||
|
||||
def test_hindsight_is_declared():
|
||||
provider = get_provider_config_schema("hindsight")
|
||||
|
||||
assert provider is not None
|
||||
assert provider.label == "Hindsight"
|
||||
assert {field.key for field in provider.fields} == {
|
||||
"mode",
|
||||
"api_key",
|
||||
"api_url",
|
||||
"bank_id",
|
||||
"recall_budget",
|
||||
}
|
||||
|
||||
|
||||
def test_fields_are_all_inline():
|
||||
provider = get_provider_config_schema("hindsight")
|
||||
assert provider is not None
|
||||
|
||||
# Hindsight is simple enough to render fully in the compact panel, so it
|
||||
# never grows a Full config… modal.
|
||||
assert all(field.inline for field in provider.fields)
|
||||
|
||||
|
||||
def test_mode_gating_is_expressed_as_select_options():
|
||||
provider = get_provider_config_schema("hindsight")
|
||||
assert provider is not None
|
||||
|
||||
mode = next(field for field in provider.fields if field.key == "mode")
|
||||
assert mode.kind == KIND_SELECT
|
||||
assert mode.allowed_values() == {"cloud", "local_external"}
|
||||
# local_embedded is intentionally unsupported on desktop.
|
||||
assert "local_embedded" not in mode.allowed_values()
|
||||
|
||||
|
||||
def test_api_key_is_a_secret_bound_to_env():
|
||||
provider = get_provider_config_schema("hindsight")
|
||||
assert provider is not None
|
||||
|
||||
api_key = next(field for field in provider.fields if field.key == "api_key")
|
||||
assert api_key.kind == KIND_SECRET
|
||||
assert api_key.is_secret is True
|
||||
assert api_key.env_key == "HINDSIGHT_API_KEY"
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
"""Tests for the declarative memory-provider registry."""
|
||||
"""Tests for Honcho's declared config surface."""
|
||||
|
||||
from hermes_cli.memory_providers import (
|
||||
from plugins.memory.config_schema import (
|
||||
KIND_BOOL,
|
||||
KIND_JSON,
|
||||
KIND_NUMBER,
|
||||
KIND_SECRET,
|
||||
KIND_SELECT,
|
||||
MEMORY_PROVIDERS,
|
||||
STORAGE_HONCHO_HOST_BLOCK,
|
||||
get_memory_provider,
|
||||
get_provider_config_schema,
|
||||
)
|
||||
|
||||
# The curated set shown in the compact panel; everything else lives in the modal.
|
||||
|
|
@ -23,12 +22,8 @@ INLINE_KEYS = {
|
|||
}
|
||||
|
||||
|
||||
def test_registry_lists_honcho_before_hindsight():
|
||||
assert list(MEMORY_PROVIDERS) == ["honcho", "hindsight"]
|
||||
|
||||
|
||||
def test_honcho_is_declared():
|
||||
provider = get_memory_provider("honcho")
|
||||
provider = get_provider_config_schema("honcho")
|
||||
|
||||
assert provider is not None
|
||||
assert provider.label == "Honcho"
|
||||
|
|
@ -39,8 +34,8 @@ def test_honcho_is_declared():
|
|||
assert INLINE_KEYS <= set(keys)
|
||||
|
||||
|
||||
def test_honcho_inline_fields_are_the_curated_subset():
|
||||
provider = get_memory_provider("honcho")
|
||||
def test_inline_fields_are_the_curated_subset():
|
||||
provider = get_provider_config_schema("honcho")
|
||||
assert provider is not None
|
||||
|
||||
assert {field.key for field in provider.inline_fields()} == INLINE_KEYS
|
||||
|
|
@ -49,8 +44,8 @@ def test_honcho_inline_fields_are_the_curated_subset():
|
|||
assert {"writeFrequency", "recallMode", "userPeerAliases"} <= non_inline
|
||||
|
||||
|
||||
def test_honcho_declares_the_new_field_kinds():
|
||||
provider = get_memory_provider("honcho")
|
||||
def test_declares_the_new_field_kinds():
|
||||
provider = get_provider_config_schema("honcho")
|
||||
assert provider is not None
|
||||
|
||||
by_key = {f.key: f for f in provider.fields}
|
||||
|
|
@ -61,8 +56,8 @@ def test_honcho_declares_the_new_field_kinds():
|
|||
assert by_key["observationMode"].allowed_values() == {"directional", "unified"}
|
||||
|
||||
|
||||
def test_honcho_selects_constrain_their_values():
|
||||
provider = get_memory_provider("honcho")
|
||||
def test_selects_constrain_their_values():
|
||||
provider = get_provider_config_schema("honcho")
|
||||
assert provider is not None
|
||||
|
||||
environment = next(f for f in provider.fields if f.key == "environment")
|
||||
|
|
@ -74,8 +69,8 @@ def test_honcho_selects_constrain_their_values():
|
|||
assert strategy.allowed_values() == {"per-directory", "per-repo", "per-session", "global"}
|
||||
|
||||
|
||||
def test_honcho_api_key_is_a_secret_bound_to_env():
|
||||
provider = get_memory_provider("honcho")
|
||||
def test_api_key_is_a_secret_bound_to_env():
|
||||
provider = get_provider_config_schema("honcho")
|
||||
assert provider is not None
|
||||
|
||||
api_key = next(f for f in provider.fields if f.key == "apiKey")
|
||||
|
|
@ -84,8 +79,8 @@ def test_honcho_api_key_is_a_secret_bound_to_env():
|
|||
assert api_key.env_key == "HONCHO_API_KEY"
|
||||
|
||||
|
||||
def test_honcho_root_scoped_fields_are_exactly_the_global_ones():
|
||||
provider = get_memory_provider("honcho")
|
||||
def test_root_scoped_fields_are_exactly_the_global_ones():
|
||||
provider = get_provider_config_schema("honcho")
|
||||
assert provider is not None
|
||||
|
||||
scopes = {f.key: f.scope for f in provider.fields}
|
||||
|
|
@ -93,51 +88,3 @@ def test_honcho_root_scoped_fields_are_exactly_the_global_ones():
|
|||
# baseUrl, timeout and sessions live at the config root in Honcho's schema;
|
||||
# everything else is per-profile host-scoped.
|
||||
assert root_keys == {"baseUrl", "timeout", "sessions"}
|
||||
|
||||
|
||||
def test_hindsight_is_declared():
|
||||
provider = get_memory_provider("hindsight")
|
||||
|
||||
assert provider is not None
|
||||
assert provider.label == "Hindsight"
|
||||
assert {field.key for field in provider.fields} == {
|
||||
"mode",
|
||||
"api_key",
|
||||
"api_url",
|
||||
"bank_id",
|
||||
"recall_budget",
|
||||
}
|
||||
|
||||
|
||||
def test_hindsight_fields_are_all_inline():
|
||||
provider = get_memory_provider("hindsight")
|
||||
assert provider is not None
|
||||
|
||||
# Hindsight is simple enough to render fully in the compact panel, so it
|
||||
# never grows a Full config… modal.
|
||||
assert all(field.inline for field in provider.fields)
|
||||
|
||||
|
||||
def test_hindsight_mode_gating_is_expressed_as_select_options():
|
||||
provider = get_memory_provider("hindsight")
|
||||
assert provider is not None
|
||||
|
||||
mode = next(field for field in provider.fields if field.key == "mode")
|
||||
assert mode.kind == KIND_SELECT
|
||||
assert mode.allowed_values() == {"cloud", "local_external"}
|
||||
# local_embedded is intentionally unsupported on desktop.
|
||||
assert "local_embedded" not in mode.allowed_values()
|
||||
|
||||
|
||||
def test_hindsight_api_key_is_a_secret_bound_to_env():
|
||||
provider = get_memory_provider("hindsight")
|
||||
assert provider is not None
|
||||
|
||||
api_key = next(field for field in provider.fields if field.key == "api_key")
|
||||
assert api_key.kind == KIND_SECRET
|
||||
assert api_key.is_secret is True
|
||||
assert api_key.env_key == "HINDSIGHT_API_KEY"
|
||||
|
||||
|
||||
def test_unknown_provider_is_none():
|
||||
assert get_memory_provider("builtin") is None
|
||||
Loading…
Add table
Add a link
Reference in a new issue