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

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

168
AGENTS.md Normal file
View File

@ -0,0 +1,168 @@
# AGENTS.md
Guidance for coding agents working in this repository.
## Project Overview
Memory Gateway is a lightweight Python HTTP gateway that exposes one business-facing Memory System API over two memory backends:
- OpenViking handles sessions, session archives, extracted long-term memories, resources, and semantic search.
- EverOS handles profile and episodic-memory style recall.
The main package is `memory_system_api`. Treat it as a narrow gateway, not a replacement for either backend. Callers should interact with users, sessions, messages, resources, memories, search, and profile endpoints under `/memory-system`.
## Key Contract
Business endpoints are gated by `user_id + user_key`.
- Create a user first with `POST /memory-system/users`.
- Save `account.result.user_key` from the response.
- Pass `user_id` and `user_key` on later business calls.
- Do not ask callers to pass `account_id`.
- Do not ask callers to pass OpenViking root keys.
- `X-API-Key` is only for protecting the Memory Gateway server itself when `server.api_key` is configured.
OpenViking user calls use the OpenViking user key as `X-API-Key`. Admin calls, such as account creation, use the configured OpenViking root key internally.
## Important Paths
- `memory_system_api/api.py`: FastAPI routes under `/memory-system`.
- `memory_system_api/service.py`: orchestration across OpenViking and EverOS.
- `memory_system_api/clients.py`: async HTTP clients for backend APIs.
- `memory_system_api/schemas.py`: Pydantic request and response models.
- `memory_system_api/store.py`: SQLite persistence for user keys, sessions, tasks, and archive metadata.
- `memory_system_api/config.py`: YAML and environment-based configuration.
- `plugins/memory/memory_system/`: Hermes memory provider plugin that talks to this API.
- `skills/memory-system-api/`: local agent skill and reference docs for using this API.
- `eval/hermes_memory_eval/`: LoCoMo-style evaluation runner for Hermes memory behavior.
- `tests/`: unit tests for clients, service orchestration, server routes, store behavior, and plugin behavior.
## API Surface
Current Memory System endpoints include:
- `GET /memory-system/health`
- `POST /memory-system/users`
- `POST /memory-system/messages`
- `POST /memory-system/sessions/{session_id}/commit`
- `POST /memory-system/sessions/{session_id}/extract`
- `GET|POST /memory-system/sessions/{session_id}/context`
- `GET|POST /memory-system/openviking/tasks/{task_id}`
- `GET /memory-system/memories`
- `GET /memory-system/memories/content`
- `POST /memory-system/memories`
- `DELETE /memory-system/memories`
- `POST /memory-system/resources`
- `DELETE /memory-system/resources`
- `POST /memory-system/search`
- `GET|POST /memory-system/users/{user_id}/profile`
Memory writes are implemented through OpenViking `POST /api/v1/content/write` with `mode` set to `create`, `replace`, or `append`. Memory reads and deletes use `content/read`, `fs/ls`, and `fs`.
## Development Setup
The project uses Python 3.10+.
Common setup:
```bash
python -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -e ".[dev]"
```
If `uv` is available, it is also reasonable to use:
```bash
uv run --extra dev pytest -q
```
In sandboxed environments, `uv` may need a writable cache directory:
```bash
uv --cache-dir /private/tmp/uv-cache run --extra dev pytest -q
```
## Running The Server
Copy the example config before local runs:
```bash
cp config.example.yaml config.yaml
```
Start OpenViking and EverOS first, then run:
```bash
python -m memory_system_api.server --config config.yaml --host 0.0.0.0 --port 1934
```
The default Memory System base URL is:
```text
http://127.0.0.1:1934/memory-system
```
## Validation
Run focused tests for touched areas whenever possible:
```bash
pytest -q tests/test_memory_system_clients.py
pytest -q tests/test_memory_system_service.py
pytest -q tests/test_memory_system_server.py
pytest -q tests/test_memory_system_store.py
pytest -q tests/test_hermes_memory_system_plugin.py
```
Run the full suite before handing off broad changes:
```bash
pytest -q
python -m compileall -q memory_system_api plugins eval tests
```
If the environment lacks `pytest` and dependencies cannot be installed, at minimum run:
```bash
.venv/bin/python -m compileall -q memory_system_api plugins eval tests
```
Then report the missing test dependency clearly.
## Coding Guidelines
- Keep changes scoped to the relevant API, service, client, schema, plugin, or docs surface.
- Prefer existing patterns in `api.py`, `service.py`, `clients.py`, and tests.
- Add or update tests before changing behavior.
- Preserve the `BackendStatus` response pattern: top-level status plus per-backend status, result, and error.
- Keep OpenViking-only operations from accidentally touching EverOS.
- Keep search responses compact. Do not return embedding vectors or large raw EverOS `original_data` blobs.
- Do not introduce in-memory identity caches; SQLite is the source of truth for user keys, sessions, tasks, and archive URIs.
- Do not commit local `config.yaml`, SQLite databases, real user keys, root keys, or API secrets.
- Use `rg` for searches and prefer small, targeted patches.
## Testing Patterns
The tests use lightweight fake clients and stores. When adding backend client behavior:
- Add client tests that assert exact HTTP path, headers, params, and JSON body.
- Add service tests that assert `credential_for_user` is called and the correct backend receives the operation.
- Add server route tests when new endpoints are exposed.
- For plugin changes, update `tests/test_hermes_memory_system_plugin.py`.
## Documentation Updates
When changing public behavior, update the relevant docs:
- `README.md` for user-facing setup and API details.
- `skills/memory-system-api/SKILL.md` for agent-facing usage.
- `skills/memory-system-api/references/api.md` when the longer API reference needs to match.
- `plugins/memory/memory_system/README.md` when Hermes plugin behavior changes.
## Safety Notes
- Never include real OpenViking root keys, user keys, Memory System API keys, or EverOS keys in examples.
- Prefer placeholders such as `<USER_KEY>`, `<MEMORY_SYSTEM_API_KEY>`, and `<OPENVIKING_ROOT_KEY>`.
- Keep destructive operations explicit. Deleting memories or resources maps to OpenViking `DELETE /api/v1/fs`; check `recursive` defaults and tests carefully.

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/sessions/{session_id}/commit` on session end
- `POST /memory-system/search` for recall - `POST /memory-system/search` for recall
- `GET /memory-system/users/{user_id}/profile` for user profile reads - `GET /memory-system/users/{user_id}/profile` for user profile reads
- `GET/POST/DELETE /memory-system/memories` for direct memory URI management
## Configure ## Configure
@ -16,6 +17,7 @@ Put these values in the Hermes profile env file, usually `~/.hermes/.env`:
```dotenv ```dotenv
MEMORY_SYSTEM_ENDPOINT=http://127.0.0.1:1934 MEMORY_SYSTEM_ENDPOINT=http://127.0.0.1:1934
MEMORY_SYSTEM_USER_ID=default MEMORY_SYSTEM_USER_ID=default
MEMORY_SYSTEM_USER_KEY=
MEMORY_SYSTEM_API_KEY= MEMORY_SYSTEM_API_KEY=
MEMORY_SYSTEM_SEARCH_USE_LLM=false MEMORY_SYSTEM_SEARCH_USE_LLM=false
MEMORY_SYSTEM_COMMIT_EVERY_TURNS=5 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`. You can also keep a separate file and point to it with `MEMORY_SYSTEM_ENV_FILE`.
Real environment variables still override file values. 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: Then select this provider in Hermes memory config:
```yaml ```yaml
@ -38,6 +43,12 @@ memory:
- `memory_system_search`: search OpenViking and EverOS via Memory System API. - `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_profile`: read the EverOS profile memory for the active user.
- `memory_system_remember`: explicitly write an important memory and commit the session. - `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. 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. 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 import time
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from urllib.parse import urlencode
from agent.memory_provider import MemoryProvider from agent.memory_provider import MemoryProvider
from tools.registry import tool_error from tools.registry import tool_error
@ -27,6 +28,7 @@ _CONFIG_KEYS = {
"MEMORY_SYSTEM_ENDPOINT", "MEMORY_SYSTEM_ENDPOINT",
"MEMORY_SYSTEM_API_KEY", "MEMORY_SYSTEM_API_KEY",
"MEMORY_SYSTEM_USER_ID", "MEMORY_SYSTEM_USER_ID",
"MEMORY_SYSTEM_USER_KEY",
"MEMORY_SYSTEM_SEARCH_USE_LLM", "MEMORY_SYSTEM_SEARCH_USE_LLM",
"MEMORY_SYSTEM_COMMIT_EVERY_TURNS", "MEMORY_SYSTEM_COMMIT_EVERY_TURNS",
"MEMORY_SYSTEM_COMMIT_INTERVAL_SECONDS", "MEMORY_SYSTEM_COMMIT_INTERVAL_SECONDS",
@ -164,6 +166,11 @@ class _MemorySystemClient:
response.raise_for_status() response.raise_for_status()
return response.json() 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]: def post(self, path: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
response = self._httpx.post( response = self._httpx.post(
self._url(path), self._url(path),
@ -176,7 +183,11 @@ class _MemorySystemClient:
def health(self) -> bool: def health(self) -> bool:
try: 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 return response.status_code == 200
except Exception: except Exception:
return False return False
@ -215,7 +226,11 @@ PROFILE_SCHEMA = {
"user_id": { "user_id": {
"type": "string", "type": "string",
"description": "Optional user override. Defaults to the active Hermes user.", "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): class MemorySystemMemoryProvider(MemoryProvider):
"""Hermes MemoryProvider backed by Memory System API.""" """Hermes MemoryProvider backed by Memory System API."""
@ -252,6 +335,7 @@ class MemorySystemMemoryProvider(MemoryProvider):
self._endpoint = config.get("MEMORY_SYSTEM_ENDPOINT", _DEFAULT_ENDPOINT) self._endpoint = config.get("MEMORY_SYSTEM_ENDPOINT", _DEFAULT_ENDPOINT)
self._api_key = config.get("MEMORY_SYSTEM_API_KEY", "") self._api_key = config.get("MEMORY_SYSTEM_API_KEY", "")
self._user_id = config.get("MEMORY_SYSTEM_USER_ID", "default") self._user_id = config.get("MEMORY_SYSTEM_USER_ID", "default")
self._user_key = config.get("MEMORY_SYSTEM_USER_KEY", "")
self._session_id = "" self._session_id = ""
self._client: Optional[_MemorySystemClient] = None self._client: Optional[_MemorySystemClient] = None
self._sync_thread: Optional[threading.Thread] = None self._sync_thread: Optional[threading.Thread] = None
@ -292,6 +376,16 @@ class MemorySystemMemoryProvider(MemoryProvider):
"default": "default", "default": "default",
"env_var": "MEMORY_SYSTEM_USER_ID", "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", "key": "commit_every_turns",
"description": "Commit after this many new turns. Set 0 to disable turn-based commits.", "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 kwargs.get("agent_identity")
or "default" or "default"
) )
self._user_key = config.get("MEMORY_SYSTEM_USER_KEY") or kwargs.get("user_key") or ""
self._session_id = session_id self._session_id = session_id
self._default_use_llm = _bool_value(config.get("MEMORY_SYSTEM_SEARCH_USE_LLM"), False) 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) 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) logger.warning("Memory System API health check failed: %s", self._endpoint)
return return
self._client = client self._client = client
self._ensure_user_key()
except Exception as exc: except Exception as exc:
logger.warning("Memory System API initialization failed: %s", exc) logger.warning("Memory System API initialization failed: %s", exc)
self._client = None self._client = None
@ -352,7 +448,8 @@ class MemorySystemMemoryProvider(MemoryProvider):
"# Memory System\n" "# Memory System\n"
"Persistent memory is active. Use memory_system_search for recall, " "Persistent memory is active. Use memory_system_search for recall, "
"memory_system_profile for user profile, and memory_system_remember " "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: def prefetch(self, query: str, *, session_id: str = "") -> str:
@ -375,6 +472,7 @@ class MemorySystemMemoryProvider(MemoryProvider):
"/memory-system/search", "/memory-system/search",
{ {
"user_id": self._user_id, "user_id": self._user_id,
"user_key": self._user_key,
"session_id": session_id or self._session_id, "session_id": session_id or self._session_id,
"query": query, "query": query,
"use_llm": self._default_use_llm, "use_llm": self._default_use_llm,
@ -436,7 +534,15 @@ class MemorySystemMemoryProvider(MemoryProvider):
self._remember(content, session_id=self._session_id, commit=False) self._remember(content, session_id=self._session_id, commit=False)
def get_tool_schemas(self) -> List[Dict[str, Any]]: 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: def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs) -> str:
if not self._client: if not self._client:
@ -448,6 +554,14 @@ class MemorySystemMemoryProvider(MemoryProvider):
return self._tool_profile(args) return self._tool_profile(args)
if tool_name == "memory_system_remember": if tool_name == "memory_system_remember":
return self._tool_remember(args) 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}") return tool_error(f"Unknown tool: {tool_name}")
except Exception as exc: except Exception as exc:
return tool_error(str(exc)) return tool_error(str(exc))
@ -466,6 +580,7 @@ class MemorySystemMemoryProvider(MemoryProvider):
) -> Dict[str, Any]: ) -> Dict[str, Any]:
payload: Dict[str, Any] = { payload: Dict[str, Any] = {
"user_id": self._user_id, "user_id": self._user_id,
"user_key": self._user_key,
"session_id": session_id, "session_id": session_id,
"metadata": {"source": "hermes", "provider": "memory_system"}, "metadata": {"source": "hermes", "provider": "memory_system"},
} }
@ -551,7 +666,7 @@ class MemorySystemMemoryProvider(MemoryProvider):
def _commit_session(self, session_id: str) -> Dict[str, Any]: def _commit_session(self, session_id: str) -> Dict[str, Any]:
response = self._client.post( response = self._client.post(
f"/memory-system/sessions/{session_id}/commit", f"/memory-system/sessions/{session_id}/commit",
{"user_id": self._user_id}, self._user_payload(),
) )
if response.get("status") == "success": if response.get("status") == "success":
self._last_commit_turn = self._turn_count self._last_commit_turn = self._turn_count
@ -568,6 +683,7 @@ class MemorySystemMemoryProvider(MemoryProvider):
limit = max(1, min(limit, 100)) limit = max(1, min(limit, 100))
payload = { payload = {
"user_id": self._user_id, "user_id": self._user_id,
"user_key": self._user_key,
"session_id": args.get("session_id") or self._session_id, "session_id": args.get("session_id") or self._session_id,
"query": query, "query": query,
"use_llm": bool(args.get("use_llm", self._default_use_llm)), "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() user_id = str(args.get("user_id") or self._user_id).strip()
if not user_id: if not user_id:
return tool_error("user_id is required") 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( return json.dumps(
self._client.get(f"/memory-system/users/{user_id}/profile"), self._client.get(path),
ensure_ascii=False, ensure_ascii=False,
) )
@ -602,11 +726,102 @@ class MemorySystemMemoryProvider(MemoryProvider):
if commit: if commit:
commit_response = self._client.post( commit_response = self._client.post(
f"/memory-system/sessions/{session_id}/commit", 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 {"status": response.get("status"), "write": response, "commit": commit_response}
return 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: def register(ctx) -> None:
ctx.register_memory_provider(MemorySystemMemoryProvider()) ctx.register_memory_provider(MemorySystemMemoryProvider())

View File

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

View File

@ -1,5 +1,5 @@
name: memory_system 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." description: "Memory System API provider for Hermes, combining OpenViking session memory and EverOS user profile memory."
pip_dependencies: pip_dependencies:
- httpx - httpx

View File

@ -1,6 +1,6 @@
--- ---
name: memory-system-api name: memory-system-api
description: "Use when an AI agent needs to operate this repository's Memory System API, including creating users, writing session messages, committing or extracting memory, searching memories, reading profiles, or debugging OpenViking/EverOS backend results." description: "Use when an AI agent needs to operate this repository's Memory System API, including creating users, writing session messages, committing or extracting memory, managing memory URIs, searching memories, reading profiles, or debugging OpenViking/EverOS backend results."
--- ---
# Memory System API # Memory System API
@ -26,13 +26,13 @@ Do not assume business APIs will auto-create users. Non-user-creation endpoints
- `user_key`: OpenViking key returned when the user is created. - `user_key`: OpenViking key returned when the user is created.
- `session_id`: conversation ID and OpenViking session ID. - `session_id`: conversation ID and OpenViking session ID.
Internally the gateway always uses the fixed OpenViking admin workspace: Internally the gateway creates one isolated OpenViking account per business user:
```json ```json
{"account_id": "admin", "admin_user_id": "admin"} {"account_id": "<user_id>_account", "admin_user_id": "<user_id>"}
``` ```
On the first user creation, if the admin workspace is not stored in SQLite yet, the gateway creates it first and then creates the requested user. After that, later users go straight to `/api/v1/admin/accounts/admin/users`. The gateway does not call `/api/v1/admin/accounts/admin/users`. User creation goes through `/api/v1/admin/accounts`, stores the returned `user_key`, and later business calls use that user key directly.
The SQLite store is the source of truth for: The SQLite store is the source of truth for:
@ -44,8 +44,9 @@ The SQLite store is the source of truth for:
Session memory is retrieved under OpenViking using the explicit user/session URI paths: Session memory is retrieved under OpenViking using the explicit user/session URI paths:
```text ```text
viking://user/<user_id>/memories/ viking://user/memories
viking://user/<user_id>/<session_id> viking://user/memories/...
viking://user/<session_id>
``` ```
## Endpoints ## Endpoints
@ -65,6 +66,8 @@ Base path: `/memory-system`
| `GET` | `/memories/content` | Read one OpenViking memory URI | Yes | | `GET` | `/memories/content` | Read one OpenViking memory URI | Yes |
| `POST` | `/memories` | Create, replace, or append an OpenViking memory via `content/write` | Yes | | `POST` | `/memories` | Create, replace, or append an OpenViking memory via `content/write` | Yes |
| `DELETE` | `/memories` | Delete an OpenViking memory URI via `fs` | Yes | | `DELETE` | `/memories` | Delete an OpenViking memory URI via `fs` | Yes |
| `POST` | `/resources` | Upload local file or remote URL to OpenViking resources | Yes |
| `DELETE` | `/resources` | Delete OpenViking resource URI via `fs` | Yes |
| `POST` | `/search` | Search OpenViking and EverOS | Yes | | `POST` | `/search` | Search OpenViking and EverOS | Yes |
| `GET` | `/users/{user_id}/profile` | Read EverOS profile | Yes | | `GET` | `/users/{user_id}/profile` | Read EverOS profile | Yes |
@ -195,6 +198,8 @@ curl -sS -X DELETE -G "$BASE/memory-system/memories" \
--data-urlencode "recursive=false" --data-urlencode "recursive=false"
``` ```
For directory-like or composite memories, retry deletion with `recursive=true` only after OpenViking reports that recursive deletion is required.
## Response Handling ## Response Handling
Top-level `status` is one of: Top-level `status` is one of:
@ -213,6 +218,8 @@ Session context responses include OpenViking context under `context`, EverOS rec
- Omitting `user_key` on business calls. - Omitting `user_key` on business calls.
- Sending `account_id` to business APIs. - Sending `account_id` to business APIs.
- Confusing `X-API-Key` with `user_key`. - Confusing `X-API-Key` with `user_key`.
- Assuming memory updates use a PATCH endpoint; use `POST /memory-system/memories` with `mode=replace` or `mode=append`.
- Deleting memories recursively by default; use `recursive=false` unless the target is a directory or composite resource.
- Expecting `backends.everos.result` to still contain full `episodes` or `original_data`. - Expecting `backends.everos.result` to still contain full `episodes` or `original_data`.
- Assuming the gateway stores these values only in memory; it persists them in SQLite. - Assuming the gateway stores these values only in memory; it persists them in SQLite.

View File

@ -1,4 +1,4 @@
interface: interface:
display_name: "Memory System API" display_name: "Memory System API"
short_description: "Use the configured Memory System API endpoint from AI agents." short_description: "Use the configured Memory System API endpoint from AI agents."
default_prompt: "Use the Memory System API skill with the configured endpoint to create users, write conversation memory with user_id and user_key, trigger extraction, search memory, and read user profiles." default_prompt: "Use the Memory System API skill with the configured endpoint to create users, write session messages with user_id and user_key, commit or extract sessions, manage memory URIs, search memory, and read user profiles."

View File

@ -20,6 +20,34 @@ If an API key is configured, add:
curl -s <MEMORY_SYSTEM_BASE_URL>/memory-system/health curl -s <MEMORY_SYSTEM_BASE_URL>/memory-system/health
``` ```
## Create User
Create the business user before calling any business endpoint:
```bash
curl -s -X POST <MEMORY_SYSTEM_BASE_URL>/memory-system/users \
-H "Content-Type: application/json" \
-d '{"user_id": "<USER_ID>"}'
```
Save the returned `account.result.user_key` and pass it as `user_key` on later calls:
```json
{
"status": "success",
"account": {
"status": "ok",
"result": {
"account_id": "<USER_ID>_account",
"admin_user_id": "<USER_ID>",
"user_key": "<USER_KEY>"
}
}
}
```
Do not send `account_id` to business endpoints.
## Write Messages ## Write Messages
```bash ```bash
@ -134,6 +162,68 @@ Response shape:
} }
``` ```
## Manage Memories
Memory management operates on OpenViking memory URIs, usually under `viking://user/memories`. There is no separate PATCH endpoint. Update existing memory by writing directly to its URI.
### List Memory URIs
```bash
curl -sS --get "<MEMORY_SYSTEM_BASE_URL>/memory-system/memories" \
--data-urlencode "user_id=<USER_ID>" \
--data-urlencode "user_key=<USER_KEY>" \
--data-urlencode "uri=viking://user/memories" \
--data-urlencode "recursive=true"
```
This maps to OpenViking `GET /api/v1/fs/ls`.
### Read Memory Content
```bash
curl -sS --get "<MEMORY_SYSTEM_BASE_URL>/memory-system/memories/content" \
--data-urlencode "user_id=<USER_ID>" \
--data-urlencode "user_key=<USER_KEY>" \
--data-urlencode "uri=viking://user/memories/preferences/python.md"
```
This maps to OpenViking `GET /api/v1/content/read`.
### Create, Replace, Or Append Memory
```bash
curl -sS -X POST "<MEMORY_SYSTEM_BASE_URL>/memory-system/memories" \
-H "Content-Type: application/json" \
-d '{
"user_id": "<USER_ID>",
"user_key": "<USER_KEY>",
"uri": "viking://user/memories/preferences/python.md",
"content": "# Python 偏好\n\n用户偏好使用 Python 做数据分析,常用 pandas。",
"mode": "replace",
"wait": true
}'
```
`mode` supports:
- `create`: create a new memory URI.
- `replace`: fully replace an existing memory.
- `append`: append content to an existing memory.
This maps to OpenViking `POST /api/v1/content/write`. OpenViking refreshes semantic and vector indexes after writing.
### Delete Memory
```bash
curl -sS -X DELETE --get "<MEMORY_SYSTEM_BASE_URL>/memory-system/memories" \
--data-urlencode "user_id=<USER_ID>" \
--data-urlencode "user_key=<USER_KEY>" \
--data-urlencode "uri=viking://user/memories/preferences/python.md" \
--data-urlencode "recursive=false"
```
Default to `recursive=false`. If OpenViking reports that the URI is a directory or composite resource, retry with `recursive=true`.
## Search ## Search
Without LLM planning: Without LLM planning:

View File

@ -23,10 +23,23 @@ class FakeClient:
def __init__(self, commit_status="success"): def __init__(self, commit_status="success"):
self.posts = [] self.posts = []
self.gets = [] self.gets = []
self.deletes = []
self.commit_status = commit_status self.commit_status = commit_status
def post(self, path, payload=None): def post(self, path, payload=None):
self.posts.append((path, payload or {})) self.posts.append((path, payload or {}))
if path == "/memory-system/users":
return {
"status": "success",
"account": {
"status": "ok",
"result": {
"account_id": f"{payload['user_id']}_account",
"admin_user_id": payload["user_id"],
"user_key": f"{payload['user_id']}-key",
},
},
}
if path == "/memory-system/search": if path == "/memory-system/search":
return { return {
"status": "success", "status": "success",
@ -52,12 +65,22 @@ class FakeClient:
} }
if path.endswith("/commit"): if path.endswith("/commit"):
return {"status": self.commit_status} return {"status": self.commit_status}
if path == "/memory-system/memories":
return {"status": "success", "memory": {"status": "ok", "result": {"uri": payload.get("uri")}}}
return {"status": "success"} return {"status": "success"}
def get(self, path): def get(self, path):
self.gets.append(path) self.gets.append(path)
if path.startswith("/memory-system/memories/content"):
return {"status": "success", "memory": {"status": "ok", "result": {"content": "# Python"}}}
if path.startswith("/memory-system/memories"):
return {"status": "success", "memory": {"status": "ok", "result": {"children": []}}}
return {"status": "success", "profile": {"coffee": "latte"}} return {"status": "success", "profile": {"coffee": "latte"}}
def delete(self, path):
self.deletes.append(path)
return {"status": "success", "memory": {"status": "ok", "result": {"estimated_deleted_count": 1}}}
def make_provider(): def make_provider():
module = load_plugin_module() module = load_plugin_module()
@ -65,6 +88,7 @@ def make_provider():
provider._client = FakeClient() provider._client = FakeClient()
provider._endpoint = "http://127.0.0.1:1934" provider._endpoint = "http://127.0.0.1:1934"
provider._user_id = "user-1" provider._user_id = "user-1"
provider._user_key = "user-1-key"
provider._session_id = "session-1" provider._session_id = "session-1"
provider._commit_every_turns = 0 provider._commit_every_turns = 0
provider._commit_interval_seconds = 0 provider._commit_interval_seconds = 0
@ -87,6 +111,7 @@ def test_initialize_loads_config_from_hermes_env_file(tmp_path, monkeypatch):
[ [
"MEMORY_SYSTEM_ENDPOINT=http://127.0.0.1:1934", "MEMORY_SYSTEM_ENDPOINT=http://127.0.0.1:1934",
"MEMORY_SYSTEM_USER_ID=file-user", "MEMORY_SYSTEM_USER_ID=file-user",
"MEMORY_SYSTEM_USER_KEY=file-user-key",
"MEMORY_SYSTEM_COMMIT_EVERY_TURNS=3", "MEMORY_SYSTEM_COMMIT_EVERY_TURNS=3",
"MEMORY_SYSTEM_COMMIT_INTERVAL_SECONDS=60", "MEMORY_SYSTEM_COMMIT_INTERVAL_SECONDS=60",
"MEMORY_SYSTEM_TIMEOUT_SECONDS=123", "MEMORY_SYSTEM_TIMEOUT_SECONDS=123",
@ -116,6 +141,7 @@ def test_initialize_loads_config_from_hermes_env_file(tmp_path, monkeypatch):
assert provider._endpoint == "http://127.0.0.1:1934" assert provider._endpoint == "http://127.0.0.1:1934"
assert provider._user_id == "file-user" assert provider._user_id == "file-user"
assert provider._user_key == "file-user-key"
assert provider._commit_every_turns == 3 assert provider._commit_every_turns == 3
assert provider._commit_interval_seconds == 60 assert provider._commit_interval_seconds == 60
assert provider._timeout == 123 assert provider._timeout == 123
@ -134,6 +160,7 @@ def test_sync_turn_posts_user_and_assistant_messages():
"/memory-system/messages", "/memory-system/messages",
{ {
"user_id": "user-1", "user_id": "user-1",
"user_key": "user-1-key",
"session_id": "session-1", "session_id": "session-1",
"user_message": "hello", "user_message": "hello",
"assistant_message": "hi there", "assistant_message": "hi there",
@ -151,7 +178,7 @@ def test_on_session_end_commits_after_turn_sync():
assert provider._client.posts[-1] == ( assert provider._client.posts[-1] == (
"/memory-system/sessions/session-1/commit", "/memory-system/sessions/session-1/commit",
{"user_id": "user-1"}, {"user_id": "user-1", "user_key": "user-1-key"},
) )
@ -167,7 +194,7 @@ def test_sync_turn_commits_every_configured_turns():
assert provider._client.posts[-1] == ( assert provider._client.posts[-1] == (
"/memory-system/sessions/session-1/commit", "/memory-system/sessions/session-1/commit",
{"user_id": "user-1"}, {"user_id": "user-1", "user_key": "user-1-key"},
) )
assert provider._last_commit_turn == 2 assert provider._last_commit_turn == 2
@ -221,12 +248,13 @@ def test_search_tool_uses_memory_system_api():
assert "vector" not in json.dumps(result) assert "vector" not in json.dumps(result)
assert "original_data" not in json.dumps(result) assert "original_data" not in json.dumps(result)
assert provider._client.posts[-1] == ( assert provider._client.posts[-1] == (
"/memory-system/search", "/memory-system/search",
{ {
"user_id": "user-1", "user_id": "user-1",
"session_id": "session-1", "user_key": "user-1-key",
"query": "coffee", "session_id": "session-1",
"use_llm": False, "query": "coffee",
"use_llm": False,
"limit": 3, "limit": 3,
}, },
) )
@ -238,7 +266,7 @@ def test_profile_tool_reads_user_profile():
result = json.loads(provider.handle_tool_call("memory_system_profile", {})) result = json.loads(provider.handle_tool_call("memory_system_profile", {}))
assert result["profile"] == {"coffee": "latte"} assert result["profile"] == {"coffee": "latte"}
assert provider._client.gets == ["/memory-system/users/user-1/profile"] assert provider._client.gets == ["/memory-system/users/user-1/profile?user_key=user-1-key"]
def test_remember_tool_writes_and_commits(): def test_remember_tool_writes_and_commits():
@ -252,6 +280,7 @@ def test_remember_tool_writes_and_commits():
"/memory-system/messages", "/memory-system/messages",
{ {
"user_id": "user-1", "user_id": "user-1",
"user_key": "user-1-key",
"session_id": "session-1", "session_id": "session-1",
"user_message": "likes latte", "user_message": "likes latte",
"metadata": {"source": "hermes", "provider": "memory_system"}, "metadata": {"source": "hermes", "provider": "memory_system"},
@ -259,11 +288,118 @@ def test_remember_tool_writes_and_commits():
), ),
( (
"/memory-system/sessions/session-1/commit", "/memory-system/sessions/session-1/commit",
{"user_id": "user-1"}, {"user_id": "user-1", "user_key": "user-1-key"},
), ),
] ]
def test_initialize_creates_user_when_user_key_is_not_configured(tmp_path, monkeypatch):
module = load_plugin_module()
for key in list(os.environ):
if key.startswith("MEMORY_SYSTEM_"):
monkeypatch.delenv(key, raising=False)
monkeypatch.setenv("MEMORY_SYSTEM_ENDPOINT", "http://127.0.0.1:1934")
monkeypatch.setenv("MEMORY_SYSTEM_USER_ID", "new-user")
monkeypatch.chdir(tmp_path)
class InitClient(FakeClient):
def __init__(self, endpoint, api_key="", timeout=0):
super().__init__()
self.endpoint = endpoint
def health(self):
return True
monkeypatch.setattr(module, "_MemorySystemClient", InitClient)
provider = module.MemorySystemMemoryProvider()
provider.initialize("session-1")
assert provider._user_key == "new-user-key"
assert provider._client.posts == [("/memory-system/users", {"user_id": "new-user"})]
def test_memory_tool_schemas_include_memory_management_tools():
provider = make_provider()
tool_names = {schema["name"] for schema in provider.get_tool_schemas()}
assert {
"memory_system_memory_list",
"memory_system_memory_read",
"memory_system_memory_write",
"memory_system_memory_delete",
} <= tool_names
def test_memory_list_tool_calls_memories_endpoint():
provider = make_provider()
result = json.loads(provider.handle_tool_call("memory_system_memory_list", {"recursive": True}))
assert result["status"] == "success"
assert provider._client.gets == [
"/memory-system/memories?user_id=user-1&user_key=user-1-key&uri=viking%3A%2F%2Fuser%2Fmemories&recursive=true"
]
def test_memory_read_tool_calls_memory_content_endpoint():
provider = make_provider()
result = json.loads(provider.handle_tool_call(
"memory_system_memory_read",
{"uri": "viking://user/memories/preferences/python.md"},
))
assert result["status"] == "success"
assert provider._client.gets == [
"/memory-system/memories/content?user_id=user-1&user_key=user-1-key&uri=viking%3A%2F%2Fuser%2Fmemories%2Fpreferences%2Fpython.md"
]
def test_memory_write_tool_posts_memory_payload():
provider = make_provider()
result = json.loads(provider.handle_tool_call(
"memory_system_memory_write",
{
"uri": "viking://user/memories/preferences/python.md",
"content": "# Python",
"mode": "replace",
"wait": True,
},
))
assert result["status"] == "success"
assert provider._client.posts == [
(
"/memory-system/memories",
{
"user_id": "user-1",
"user_key": "user-1-key",
"uri": "viking://user/memories/preferences/python.md",
"content": "# Python",
"mode": "replace",
"wait": True,
},
)
]
def test_memory_delete_tool_deletes_memory_uri_non_recursive_by_default():
provider = make_provider()
result = json.loads(provider.handle_tool_call(
"memory_system_memory_delete",
{"uri": "viking://user/memories/preferences/python.md"},
))
assert result["status"] == "success"
assert provider._client.deletes == [
"/memory-system/memories?user_id=user-1&user_key=user-1-key&uri=viking%3A%2F%2Fuser%2Fmemories%2Fpreferences%2Fpython.md&recursive=false"
]
def test_register_adds_provider(): def test_register_adds_provider():
module = load_plugin_module() module = load_plugin_module()