|
|
|
|
@ -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())
|
|
|
|
|
|