From 1b5fee9866d7b588e77ae96438d5776307057783 Mon Sep 17 00:00:00 2001 From: tomtan Date: Tue, 2 Jun 2026 10:09:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=86=85=E5=AD=98=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=B7=A5=E5=85=B7=E5=92=8C=E7=94=A8=E6=88=B7=E5=AF=86?= =?UTF-8?q?=E9=92=A5=E6=94=AF=E6=8C=81=EF=BC=8C=E6=9B=B4=E6=96=B0=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=92=8C=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 168 +++++++++++++ plugins/memory/memory_system/README.md | 11 + plugins/memory/memory_system/__init__.py | 229 +++++++++++++++++- .../memory_system/memory_system.env.example | 1 + plugins/memory/memory_system/plugin.yaml | 2 +- skills/memory-system-api/SKILL.md | 19 +- skills/memory-system-api/agents/openai.yaml | 2 +- skills/memory-system-api/references/api.md | 90 +++++++ tests/test_hermes_memory_system_plugin.py | 156 +++++++++++- 9 files changed, 653 insertions(+), 25 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..447e606 --- /dev/null +++ b/AGENTS.md @@ -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 ``, ``, and ``. +- Keep destructive operations explicit. Deleting memories or resources maps to OpenViking `DELETE /api/v1/fs`; check `recursive` defaults and tests carefully. diff --git a/plugins/memory/memory_system/README.md b/plugins/memory/memory_system/README.md index ec380cb..1fcde0f 100644 --- a/plugins/memory/memory_system/README.md +++ b/plugins/memory/memory_system/README.md @@ -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. diff --git a/plugins/memory/memory_system/__init__.py b/plugins/memory/memory_system/__init__.py index d4fdbd5..09aeb99 100644 --- a/plugins/memory/memory_system/__init__.py +++ b/plugins/memory/memory_system/__init__.py @@ -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()) diff --git a/plugins/memory/memory_system/memory_system.env.example b/plugins/memory/memory_system/memory_system.env.example index a9e111b..0f6c12b 100644 --- a/plugins/memory/memory_system/memory_system.env.example +++ b/plugins/memory/memory_system/memory_system.env.example @@ -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 diff --git a/plugins/memory/memory_system/plugin.yaml b/plugins/memory/memory_system/plugin.yaml index a287a10..9fee52e 100644 --- a/plugins/memory/memory_system/plugin.yaml +++ b/plugins/memory/memory_system/plugin.yaml @@ -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 diff --git a/skills/memory-system-api/SKILL.md b/skills/memory-system-api/SKILL.md index e562dfe..6497806 100644 --- a/skills/memory-system-api/SKILL.md +++ b/skills/memory-system-api/SKILL.md @@ -1,6 +1,6 @@ --- 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 @@ -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. - `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 -{"account_id": "admin", "admin_user_id": "admin"} +{"account_id": "_account", "admin_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: @@ -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: ```text -viking://user//memories/ -viking://user// +viking://user/memories +viking://user/memories/... +viking://user/ ``` ## Endpoints @@ -65,6 +66,8 @@ Base path: `/memory-system` | `GET` | `/memories/content` | Read one OpenViking memory URI | Yes | | `POST` | `/memories` | Create, replace, or append an OpenViking memory via `content/write` | 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 | | `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" ``` +For directory-like or composite memories, retry deletion with `recursive=true` only after OpenViking reports that recursive deletion is required. + ## Response Handling 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. - Sending `account_id` to business APIs. - 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`. - Assuming the gateway stores these values only in memory; it persists them in SQLite. diff --git a/skills/memory-system-api/agents/openai.yaml b/skills/memory-system-api/agents/openai.yaml index 4c56bf4..71e8555 100644 --- a/skills/memory-system-api/agents/openai.yaml +++ b/skills/memory-system-api/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Memory System API" 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." diff --git a/skills/memory-system-api/references/api.md b/skills/memory-system-api/references/api.md index fdf6f5a..12dc5aa 100644 --- a/skills/memory-system-api/references/api.md +++ b/skills/memory-system-api/references/api.md @@ -20,6 +20,34 @@ If an API key is configured, add: curl -s /memory-system/health ``` +## Create User + +Create the business user before calling any business endpoint: + +```bash +curl -s -X POST /memory-system/users \ + -H "Content-Type: application/json" \ + -d '{"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": "_account", + "admin_user_id": "", + "user_key": "" + } + } +} +``` + +Do not send `account_id` to business endpoints. + ## Write Messages ```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/memories" \ + --data-urlencode "user_id=" \ + --data-urlencode "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/memories/content" \ + --data-urlencode "user_id=" \ + --data-urlencode "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/memories" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "", + "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/memories" \ + --data-urlencode "user_id=" \ + --data-urlencode "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 Without LLM planning: diff --git a/tests/test_hermes_memory_system_plugin.py b/tests/test_hermes_memory_system_plugin.py index f56503a..b579288 100644 --- a/tests/test_hermes_memory_system_plugin.py +++ b/tests/test_hermes_memory_system_plugin.py @@ -23,10 +23,23 @@ class FakeClient: def __init__(self, commit_status="success"): self.posts = [] self.gets = [] + self.deletes = [] self.commit_status = commit_status def post(self, path, payload=None): 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": return { "status": "success", @@ -52,12 +65,22 @@ class FakeClient: } if path.endswith("/commit"): return {"status": self.commit_status} + if path == "/memory-system/memories": + return {"status": "success", "memory": {"status": "ok", "result": {"uri": payload.get("uri")}}} return {"status": "success"} def get(self, 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"}} + def delete(self, path): + self.deletes.append(path) + return {"status": "success", "memory": {"status": "ok", "result": {"estimated_deleted_count": 1}}} + def make_provider(): module = load_plugin_module() @@ -65,6 +88,7 @@ def make_provider(): provider._client = FakeClient() provider._endpoint = "http://127.0.0.1:1934" provider._user_id = "user-1" + provider._user_key = "user-1-key" provider._session_id = "session-1" provider._commit_every_turns = 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_USER_ID=file-user", + "MEMORY_SYSTEM_USER_KEY=file-user-key", "MEMORY_SYSTEM_COMMIT_EVERY_TURNS=3", "MEMORY_SYSTEM_COMMIT_INTERVAL_SECONDS=60", "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._user_id == "file-user" + assert provider._user_key == "file-user-key" assert provider._commit_every_turns == 3 assert provider._commit_interval_seconds == 60 assert provider._timeout == 123 @@ -134,6 +160,7 @@ def test_sync_turn_posts_user_and_assistant_messages(): "/memory-system/messages", { "user_id": "user-1", + "user_key": "user-1-key", "session_id": "session-1", "user_message": "hello", "assistant_message": "hi there", @@ -151,7 +178,7 @@ def test_on_session_end_commits_after_turn_sync(): assert provider._client.posts[-1] == ( "/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] == ( "/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 @@ -221,12 +248,13 @@ def test_search_tool_uses_memory_system_api(): assert "vector" not in json.dumps(result) assert "original_data" not in json.dumps(result) assert provider._client.posts[-1] == ( - "/memory-system/search", - { - "user_id": "user-1", - "session_id": "session-1", - "query": "coffee", - "use_llm": False, + "/memory-system/search", + { + "user_id": "user-1", + "user_key": "user-1-key", + "session_id": "session-1", + "query": "coffee", + "use_llm": False, "limit": 3, }, ) @@ -238,7 +266,7 @@ def test_profile_tool_reads_user_profile(): result = json.loads(provider.handle_tool_call("memory_system_profile", {})) 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(): @@ -252,6 +280,7 @@ def test_remember_tool_writes_and_commits(): "/memory-system/messages", { "user_id": "user-1", + "user_key": "user-1-key", "session_id": "session-1", "user_message": "likes latte", "metadata": {"source": "hermes", "provider": "memory_system"}, @@ -259,11 +288,118 @@ def test_remember_tool_writes_and_commits(): ), ( "/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(): module = load_plugin_module()