添加内存管理工具和用户密钥支持,更新文档和测试用例

This commit is contained in:
2026-06-02 10:09:21 +08:00
parent 68b2513043
commit 1b5fee9866
9 changed files with 653 additions and 25 deletions

View File

@ -8,6 +8,7 @@ It stores completed Hermes turns through:
- `POST /memory-system/sessions/{session_id}/commit` on session end
- `POST /memory-system/search` for recall
- `GET /memory-system/users/{user_id}/profile` for user profile reads
- `GET/POST/DELETE /memory-system/memories` for direct memory URI management
## Configure
@ -16,6 +17,7 @@ Put these values in the Hermes profile env file, usually `~/.hermes/.env`:
```dotenv
MEMORY_SYSTEM_ENDPOINT=http://127.0.0.1:1934
MEMORY_SYSTEM_USER_ID=default
MEMORY_SYSTEM_USER_KEY=
MEMORY_SYSTEM_API_KEY=
MEMORY_SYSTEM_SEARCH_USE_LLM=false
MEMORY_SYSTEM_COMMIT_EVERY_TURNS=5
@ -26,6 +28,9 @@ MEMORY_SYSTEM_TIMEOUT_SECONDS=180
You can also keep a separate file and point to it with `MEMORY_SYSTEM_ENV_FILE`.
Real environment variables still override file values.
`MEMORY_SYSTEM_USER_KEY` is the key returned by `POST /memory-system/users`.
If it is omitted, the plugin calls `/memory-system/users` during initialization and uses the returned key for the current process.
Then select this provider in Hermes memory config:
```yaml
@ -38,6 +43,12 @@ memory:
- `memory_system_search`: search OpenViking and EverOS via Memory System API.
- `memory_system_profile`: read the EverOS profile memory for the active user.
- `memory_system_remember`: explicitly write an important memory and commit the session.
- `memory_system_memory_list`: list OpenViking memory URIs under `viking://user/memories`.
- `memory_system_memory_read`: read one memory URI.
- `memory_system_memory_write`: create, replace, or append one memory URI.
- `memory_system_memory_delete`: delete one memory URI, non-recursive by default.
Use the direct memory URI tools only when the model needs to inspect or edit a specific `viking://user/memories/...` item. Normal conversation recall should still use `memory_system_search`, and normal explicit remembering should still use `memory_system_remember`.
The plugin commits after 5 new turns or 300 seconds by default, whichever comes first.
Set either value to `0` to disable that trigger. Session end still commits any new turns that were not already committed.

View File

@ -15,6 +15,7 @@ import threading
import time
from pathlib import Path
from typing import Any, Dict, List, Optional
from urllib.parse import urlencode
from agent.memory_provider import MemoryProvider
from tools.registry import tool_error
@ -27,6 +28,7 @@ _CONFIG_KEYS = {
"MEMORY_SYSTEM_ENDPOINT",
"MEMORY_SYSTEM_API_KEY",
"MEMORY_SYSTEM_USER_ID",
"MEMORY_SYSTEM_USER_KEY",
"MEMORY_SYSTEM_SEARCH_USE_LLM",
"MEMORY_SYSTEM_COMMIT_EVERY_TURNS",
"MEMORY_SYSTEM_COMMIT_INTERVAL_SECONDS",
@ -164,6 +166,11 @@ class _MemorySystemClient:
response.raise_for_status()
return response.json()
def delete(self, path: str) -> Dict[str, Any]:
response = self._httpx.delete(self._url(path), headers=self._headers(), timeout=self._timeout)
response.raise_for_status()
return response.json()
def post(self, path: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
response = self._httpx.post(
self._url(path),
@ -176,7 +183,11 @@ class _MemorySystemClient:
def health(self) -> bool:
try:
response = self._httpx.get(self._url("/memory-system/health"), timeout=3.0)
response = self._httpx.get(
self._url("/memory-system/health"),
headers=self._headers(),
timeout=3.0,
)
return response.status_code == 200
except Exception:
return False
@ -215,7 +226,11 @@ PROFILE_SCHEMA = {
"user_id": {
"type": "string",
"description": "Optional user override. Defaults to the active Hermes user.",
}
},
"user_key": {
"type": "string",
"description": "Required only when user_id overrides the active Hermes user.",
},
},
},
}
@ -239,6 +254,74 @@ REMEMBER_SCHEMA = {
},
}
MEMORY_LIST_SCHEMA = {
"name": "memory_system_memory_list",
"description": "List OpenViking memory URIs through Memory System API.",
"parameters": {
"type": "object",
"properties": {
"uri": {
"type": "string",
"description": "Memory root URI. Defaults to viking://user/memories.",
},
"recursive": {
"type": "boolean",
"description": "Whether to list recursively. Defaults to true.",
},
},
},
}
MEMORY_READ_SCHEMA = {
"name": "memory_system_memory_read",
"description": "Read one OpenViking memory URI through Memory System API.",
"parameters": {
"type": "object",
"properties": {
"uri": {"type": "string", "description": "Memory URI to read."},
},
"required": ["uri"],
},
}
MEMORY_WRITE_SCHEMA = {
"name": "memory_system_memory_write",
"description": "Create, replace, or append an OpenViking memory URI through Memory System API.",
"parameters": {
"type": "object",
"properties": {
"uri": {"type": "string", "description": "Target memory URI."},
"content": {"type": "string", "description": "Memory content to write."},
"mode": {
"type": "string",
"enum": ["create", "replace", "append"],
"description": "Write mode. Use replace to modify an existing memory.",
},
"wait": {
"type": "boolean",
"description": "Wait for Memory System processing/indexing. Defaults to true.",
},
},
"required": ["uri", "content"],
},
}
MEMORY_DELETE_SCHEMA = {
"name": "memory_system_memory_delete",
"description": "Delete one OpenViking memory URI through Memory System API.",
"parameters": {
"type": "object",
"properties": {
"uri": {"type": "string", "description": "Memory URI to delete."},
"recursive": {
"type": "boolean",
"description": "Delete recursively. Defaults to false; use true only for directory-like memories.",
},
},
"required": ["uri"],
},
}
class MemorySystemMemoryProvider(MemoryProvider):
"""Hermes MemoryProvider backed by Memory System API."""
@ -252,6 +335,7 @@ class MemorySystemMemoryProvider(MemoryProvider):
self._endpoint = config.get("MEMORY_SYSTEM_ENDPOINT", _DEFAULT_ENDPOINT)
self._api_key = config.get("MEMORY_SYSTEM_API_KEY", "")
self._user_id = config.get("MEMORY_SYSTEM_USER_ID", "default")
self._user_key = config.get("MEMORY_SYSTEM_USER_KEY", "")
self._session_id = ""
self._client: Optional[_MemorySystemClient] = None
self._sync_thread: Optional[threading.Thread] = None
@ -292,6 +376,16 @@ class MemorySystemMemoryProvider(MemoryProvider):
"default": "default",
"env_var": "MEMORY_SYSTEM_USER_ID",
},
{
"key": "user_key",
"description": (
"Memory System user key returned by /memory-system/users. "
"If omitted, the plugin creates or looks up the configured user on initialization."
),
"secret": True,
"required": False,
"env_var": "MEMORY_SYSTEM_USER_KEY",
},
{
"key": "commit_every_turns",
"description": "Commit after this many new turns. Set 0 to disable turn-based commits.",
@ -325,6 +419,7 @@ class MemorySystemMemoryProvider(MemoryProvider):
or kwargs.get("agent_identity")
or "default"
)
self._user_key = config.get("MEMORY_SYSTEM_USER_KEY") or kwargs.get("user_key") or ""
self._session_id = session_id
self._default_use_llm = _bool_value(config.get("MEMORY_SYSTEM_SEARCH_USE_LLM"), False)
self._commit_every_turns = _int_value(config.get("MEMORY_SYSTEM_COMMIT_EVERY_TURNS"), 5)
@ -341,6 +436,7 @@ class MemorySystemMemoryProvider(MemoryProvider):
logger.warning("Memory System API health check failed: %s", self._endpoint)
return
self._client = client
self._ensure_user_key()
except Exception as exc:
logger.warning("Memory System API initialization failed: %s", exc)
self._client = None
@ -352,7 +448,8 @@ class MemorySystemMemoryProvider(MemoryProvider):
"# Memory System\n"
"Persistent memory is active. Use memory_system_search for recall, "
"memory_system_profile for user profile, and memory_system_remember "
"for important information that should be stored."
"for important information that should be stored. Use memory_system_memory_* "
"tools only when you need to list, read, edit, or delete specific memory URIs."
)
def prefetch(self, query: str, *, session_id: str = "") -> str:
@ -375,6 +472,7 @@ class MemorySystemMemoryProvider(MemoryProvider):
"/memory-system/search",
{
"user_id": self._user_id,
"user_key": self._user_key,
"session_id": session_id or self._session_id,
"query": query,
"use_llm": self._default_use_llm,
@ -436,7 +534,15 @@ class MemorySystemMemoryProvider(MemoryProvider):
self._remember(content, session_id=self._session_id, commit=False)
def get_tool_schemas(self) -> List[Dict[str, Any]]:
return [SEARCH_SCHEMA, PROFILE_SCHEMA, REMEMBER_SCHEMA]
return [
SEARCH_SCHEMA,
PROFILE_SCHEMA,
REMEMBER_SCHEMA,
MEMORY_LIST_SCHEMA,
MEMORY_READ_SCHEMA,
MEMORY_WRITE_SCHEMA,
MEMORY_DELETE_SCHEMA,
]
def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs) -> str:
if not self._client:
@ -448,6 +554,14 @@ class MemorySystemMemoryProvider(MemoryProvider):
return self._tool_profile(args)
if tool_name == "memory_system_remember":
return self._tool_remember(args)
if tool_name == "memory_system_memory_list":
return self._tool_memory_list(args)
if tool_name == "memory_system_memory_read":
return self._tool_memory_read(args)
if tool_name == "memory_system_memory_write":
return self._tool_memory_write(args)
if tool_name == "memory_system_memory_delete":
return self._tool_memory_delete(args)
return tool_error(f"Unknown tool: {tool_name}")
except Exception as exc:
return tool_error(str(exc))
@ -466,6 +580,7 @@ class MemorySystemMemoryProvider(MemoryProvider):
) -> Dict[str, Any]:
payload: Dict[str, Any] = {
"user_id": self._user_id,
"user_key": self._user_key,
"session_id": session_id,
"metadata": {"source": "hermes", "provider": "memory_system"},
}
@ -551,7 +666,7 @@ class MemorySystemMemoryProvider(MemoryProvider):
def _commit_session(self, session_id: str) -> Dict[str, Any]:
response = self._client.post(
f"/memory-system/sessions/{session_id}/commit",
{"user_id": self._user_id},
self._user_payload(),
)
if response.get("status") == "success":
self._last_commit_turn = self._turn_count
@ -568,6 +683,7 @@ class MemorySystemMemoryProvider(MemoryProvider):
limit = max(1, min(limit, 100))
payload = {
"user_id": self._user_id,
"user_key": self._user_key,
"session_id": args.get("session_id") or self._session_id,
"query": query,
"use_llm": bool(args.get("use_llm", self._default_use_llm)),
@ -580,8 +696,16 @@ class MemorySystemMemoryProvider(MemoryProvider):
user_id = str(args.get("user_id") or self._user_id).strip()
if not user_id:
return tool_error("user_id is required")
user_key = (
self._user_key
if user_id == self._user_id
else str(args.get("user_key") or "").strip()
)
if not user_key:
return tool_error("user_key is required for profile reads")
path = f"/memory-system/users/{user_id}/profile?{urlencode({'user_key': user_key})}"
return json.dumps(
self._client.get(f"/memory-system/users/{user_id}/profile"),
self._client.get(path),
ensure_ascii=False,
)
@ -602,11 +726,102 @@ class MemorySystemMemoryProvider(MemoryProvider):
if commit:
commit_response = self._client.post(
f"/memory-system/sessions/{session_id}/commit",
{"user_id": self._user_id},
self._user_payload(),
)
return {"status": response.get("status"), "write": response, "commit": commit_response}
return response
def _ensure_user_key(self) -> None:
if self._user_key:
return
if not self._client:
return
response = self._client.post("/memory-system/users", {"user_id": self._user_id})
result = (
response.get("account", {}).get("result", {})
if isinstance(response.get("account"), dict)
else {}
)
user_key = result.get("user_key")
if not user_key:
raise ValueError("Memory System user creation response missing account.result.user_key")
self._user_key = str(user_key)
def _user_payload(self) -> Dict[str, str]:
if not self._user_key:
raise ValueError("Memory System user_key is required")
return {"user_id": self._user_id, "user_key": self._user_key}
def _query_path(self, path: str, params: Dict[str, Any]) -> str:
return f"{path}?{urlencode(params)}"
def _tool_memory_list(self, args: Dict[str, Any]) -> str:
uri = str(args.get("uri") or "viking://user/memories").strip()
recursive = self._arg_bool(args.get("recursive"), True)
path = self._query_path(
"/memory-system/memories",
{
"user_id": self._user_id,
"user_key": self._user_key,
"uri": uri,
"recursive": str(recursive).lower(),
},
)
return json.dumps(self._client.get(path), ensure_ascii=False)
def _tool_memory_read(self, args: Dict[str, Any]) -> str:
uri = str(args.get("uri", "")).strip()
if not uri:
return tool_error("uri is required")
path = self._query_path(
"/memory-system/memories/content",
{"user_id": self._user_id, "user_key": self._user_key, "uri": uri},
)
return json.dumps(self._client.get(path), ensure_ascii=False)
def _tool_memory_write(self, args: Dict[str, Any]) -> str:
uri = str(args.get("uri", "")).strip()
content = str(args.get("content", ""))
if not uri:
return tool_error("uri is required")
if not content:
return tool_error("content is required")
mode = str(args.get("mode") or "create").strip().lower()
if mode not in {"create", "replace", "append"}:
return tool_error("mode must be create, replace, or append")
payload = {
"user_id": self._user_id,
"user_key": self._user_key,
"uri": uri,
"content": content,
"mode": mode,
"wait": self._arg_bool(args.get("wait"), True),
}
return json.dumps(self._client.post("/memory-system/memories", payload), ensure_ascii=False)
def _tool_memory_delete(self, args: Dict[str, Any]) -> str:
uri = str(args.get("uri", "")).strip()
if not uri:
return tool_error("uri is required")
recursive = self._arg_bool(args.get("recursive"), False)
path = self._query_path(
"/memory-system/memories",
{
"user_id": self._user_id,
"user_key": self._user_key,
"uri": uri,
"recursive": str(recursive).lower(),
},
)
return json.dumps(self._client.delete(path), ensure_ascii=False)
def _arg_bool(self, value: Any, default: bool) -> bool:
if isinstance(value, bool):
return value
if value is None:
return default
return _bool_value(str(value), default)
def register(ctx) -> None:
ctx.register_memory_provider(MemorySystemMemoryProvider())

View File

@ -1,5 +1,6 @@
MEMORY_SYSTEM_ENDPOINT=http://127.0.0.1:1934
MEMORY_SYSTEM_USER_ID=default
MEMORY_SYSTEM_USER_KEY=
MEMORY_SYSTEM_API_KEY=
MEMORY_SYSTEM_SEARCH_USE_LLM=false
MEMORY_SYSTEM_COMMIT_EVERY_TURNS=5

View File

@ -1,5 +1,5 @@
name: memory_system
version: 0.1.1
version: 0.1.2
description: "Memory System API provider for Hermes, combining OpenViking session memory and EverOS user profile memory."
pip_dependencies:
- httpx