Add generic memory gateway v1

This commit is contained in:
2026-05-05 16:18:31 +08:00
parent ba84b1ddb3
commit e65731a273
54 changed files with 4082 additions and 49 deletions

View File

@ -1,54 +1,211 @@
---
name: memory-gateway
description: Use this skill when an agent or harness needs reusable memory: search prior context, retrieve OpenViking resources, upload documents into knowledge, summarize arbitrary content with the Memory Gateway LLM, commit final conclusions, or cite related Obsidian notes. This skill is domain-neutral.
version: 2.0.0
description: Use this skill when Hermes needs shared long-term memory, user-scoped preferences/profile, workspace memory, session episode capture, Memory Gateway retrieval, OpenViking context search, Obsidian document upload/review, or session commit through the standalone EverMemOS service. This skill is domain-neutral.
version: 3.1.0
metadata:
hermes:
tags: [memory, openviking, obsidian, knowledge, retrieval, summarization, document-ingestion, agent-context]
tags: [memory, memory-gateway, openviking, obsidian, evermemos, long-term-memory, retrieval, agent-context]
---
# Memory Gateway
Use this skill as a generic memory layer for any agent / harness. It connects Hermes to the local Memory Gateway at `http://127.0.0.1:1934`, which fronts OpenViking and an Obsidian vault.
Use this skill as Hermes' generic memory layer. It connects Hermes to the local Memory Gateway at `http://127.0.0.1:1934`.
## Trigger Rule
The gateway provides:
Use this skill when the user asks to:
- search prior memory or retrieve related context
- upload a document and make it reusable knowledge
- summarize content and store it as memory/resource
- commit final conclusions, decisions, lessons learned, or research notes
- cite related OpenViking resources or Obsidian notes
- prepare context for another agent or workflow
Do not assume any domain-specific workflow. Treat Memory Gateway as a reusable memory and knowledge entrypoint.
- v1 user/agent/workspace/session aware memory APIs backed by SQLite metadata.
- ACL and namespace routing before retrieval.
- OpenViking fan-out search for visible namespaces.
- Session episode capture and commit through the standalone EverMemOS HTTP service, with Gateway local fallback only when configured.
- Obsidian review drafts for high-value or conflicting long-term memory candidates.
- Legacy summary/document upload endpoints for LLM summarization and Obsidian knowledge ingestion.
## Environment
Defaults:
- Memory Gateway URL: `http://127.0.0.1:1934`
- EverMemOS URL through Gateway config: `http://127.0.0.1:1995`
- Obsidian vault: `/home/tom/memory-gateway/obsidian-vault`
- Default namespace: `memory-gateway`
- Default review queue: `/home/tom/memory-gateway/obsidian-vault/Reviews/Queue`
Optional env vars:
- `MEMORY_GATEWAY_URL`
- `MEMORY_GATEWAY_API_KEY`
- `MEMORY_GATEWAY_OBSIDIAN_VAULT`
## Core Workflows
## Recommended Hermes Workflow
### 1. Retrieve Context
For normal agent work:
1. Search memory before answering if prior context may matter.
2. Append important session episodes while working.
3. Commit the session at the end so EverMemOS can promote stable memories.
4. Use feedback to mark incorrect, duplicate, outdated, or useful memories.
5. Upload documents only when they are reusable knowledge, not raw noisy logs.
Do not write full transcripts to long-term memory. Use episodes for temporary process capture and commit only stable conclusions.
## v1 Memory Commands
### Check EverMemOS
```bash
python /home/tom/.hermes/skills/memory-gateway/scripts/retrieve_memory.py \
--query "project decision memory gateway LLM summary" \
--uri viking://resources \
python /home/tom/.hermes/skills/memory-gateway/scripts/evermemos_health.py
```
Expected healthy response includes `status: ok` and `response.service: evermemos-local`.
### Create User
```bash
python /home/tom/.hermes/skills/memory-gateway/scripts/memory_create_user.py \
--user-id user_tom \
--display-name "Tom" \
--preference language=zh-CN
```
### Search ACL-Aware Memory
```bash
python /home/tom/.hermes/skills/memory-gateway/scripts/memory_search.py \
--user-id user_tom \
--agent-id agent_hermes \
--workspace-id ws_memory_gateway \
--query "namespace ACL decision" \
--limit 5
```
Use retrieval before answering when prior context may materially improve correctness.
Equivalent backward-compatible command:
### 2. Summarize And Commit
```bash
python /home/tom/.hermes/skills/memory-gateway/scripts/retrieve_memory.py \
--user-id user_tom \
--agent-id agent_hermes \
--workspace-id ws_memory_gateway \
--query "namespace ACL decision" \
--limit 5
```
If `retrieve_memory.py` is called without `--user-id`, it falls back to the legacy `/api/search` endpoint.
### Upsert Long-Term Memory
Use this only for stable, concise, reusable memory.
```bash
python /home/tom/.hermes/skills/memory-gateway/scripts/memory_upsert.py \
--user-id user_tom \
--agent-id agent_hermes \
--workspace-id ws_memory_gateway \
--memory-type preference \
--visibility private \
--importance 0.8 \
--confidence 0.9 \
--tag preference \
--summary "中文、结构化、轻量 POC 优先" \
--text "用户偏好中文输出,结构化但不要过度工程化。"
```
### Append Session Episode
Use this during a task to record useful process notes without immediately polluting long-term memory.
```bash
python /home/tom/.hermes/skills/memory-gateway/scripts/memory_append_episode.py \
--user-id user_tom \
--agent-id agent_hermes \
--workspace-id ws_memory_gateway \
--session-id sess_demo \
--tag decision \
--text "结论:这个项目必须保留用户隔离和 namespace ACL。"
```
### Commit Session Through EverMemOS
This asks Memory Gateway to call the standalone EverMemOS service configured in `config.yaml`.
For local POC the default service is `http://127.0.0.1:1995`. If `evermemos.fallback_to_local` is true and the service is unavailable, Gateway returns `evermemos_backend: local-fallback`.
- extracts candidate memories from session episodes
- deduplicates exact repeated candidates
- detects simple conflicts
- promotes normal stable memories into SQLite long-term memory
- sends high-value or conflicting candidates to Obsidian review drafts
```bash
python /home/tom/.hermes/skills/memory-gateway/scripts/memory_commit_session.py \
--user-id user_tom \
--agent-id agent_hermes \
--workspace-id ws_memory_gateway \
--session-id sess_demo \
--min-importance 0.6
```
Review drafts are written under:
```text
/home/tom/memory-gateway/obsidian-vault/Reviews/Queue/
```
### Get Profile
```bash
python /home/tom/.hermes/skills/memory-gateway/scripts/memory_get_profile.py \
--user-id user_tom
```
### List Visible Namespaces
```bash
python /home/tom/.hermes/skills/memory-gateway/scripts/memory_list_namespaces.py \
--user-id user_tom \
--agent-id agent_hermes \
--workspace-id ws_memory_gateway \
--session-id sess_demo
```
### Patch Memory
```bash
python /home/tom/.hermes/skills/memory-gateway/scripts/memory_patch.py \
--memory-id mem_xxx \
--user-id user_tom \
--agent-id agent_hermes \
--workspace-id ws_memory_gateway \
--summary "用户偏好中文、结构化、少废话。" \
--importance 0.9 \
--tag preference \
--tag confirmed
```
### Feedback
```bash
python /home/tom/.hermes/skills/memory-gateway/scripts/memory_feedback.py \
--memory-id mem_xxx \
--user-id user_tom \
--agent-id agent_hermes \
--workspace-id ws_memory_gateway \
--feedback incorrect \
--comment "这是临时偏好,不应长期保留。"
```
### Delete Memory
```bash
python /home/tom/.hermes/skills/memory-gateway/scripts/memory_delete.py \
--memory-id mem_xxx \
--user-id user_tom \
--agent-id agent_hermes \
--workspace-id ws_memory_gateway
```
## Knowledge And Obsidian Commands
### Summarize And Commit Via Legacy LLM Endpoint
Use this for high-value text that should become an OpenViking resource or summarized memory.
```bash
python /home/tom/.hermes/skills/memory-gateway/scripts/commit_summary.py \
@ -60,9 +217,9 @@ python /home/tom/.hermes/skills/memory-gateway/scripts/commit_summary.py \
--text "<final conclusion or reusable knowledge>"
```
This calls `POST /api/summary`, which uses the configured LLM and writes to OpenViking when `persist-as` is not `none`.
This calls `POST /api/summary`.
### 3. Upload Document As Knowledge
### Upload Document As Knowledge
```bash
python /home/tom/.hermes/skills/memory-gateway/scripts/upload_knowledge.py \
@ -76,7 +233,7 @@ python /home/tom/.hermes/skills/memory-gateway/scripts/upload_knowledge.py \
This calls `POST /api/knowledge/upload`: document -> MarkItDown Markdown -> Obsidian note -> LLM summary -> OpenViking resource.
### 4. Search Obsidian Notes
### Search Local Obsidian Notes
```bash
python /home/tom/.hermes/skills/memory-gateway/scripts/search_obsidian.py \
@ -84,6 +241,21 @@ python /home/tom/.hermes/skills/memory-gateway/scripts/search_obsidian.py \
--limit 5
```
## MCP Tool Names
The gateway also exposes these v1 tools through `/mcp/rpc`:
- `memory_search`
- `memory_upsert`
- `memory_append_episode`
- `memory_commit_session`
- `memory_get_profile`
- `memory_list_namespaces`
- `memory_delete`
- `memory_feedback`
Use MCP tools when Hermes has an MCP bridge available. Use the scripts above when Hermes runs skills as shell commands.
## Output Template
When using this skill, answer with:
@ -92,25 +264,27 @@ When using this skill, answer with:
## Answer
<direct answer or synthesis>
## Memory / Resource References
- `<title or URI>``<viking://...>` — why it matters
## Memory References
- `<memory_id or URI>``<namespace>` — why it matters
## Obsidian References
- `<note.md>``<relative path>` — why it matters
## Obsidian Review
- `<draft path>` — why it needs review
## Suggested Memory Commit
- commit: yes/no
- namespace:
- memory_type:
- tags:
- resource_uri: if committed
## Memory Action
- searched: yes/no
- appended_episode: yes/no
- committed_session: yes/no
- promoted_memory_count:
- review_draft_count:
```
## Guardrails
- Do not store raw noisy data as long-term memory when a concise summary is enough.
- Prefer LLM summaries and structured artifacts over full chat transcripts.
- Do not store raw noisy data as long-term memory.
- Use `memory_append_episode.py` for temporary process notes.
- Use `memory_commit_session.py` at task end to let EverMemOS decide what should persist.
- Use `memory_upsert.py` directly only for stable, concise, user-approved memory.
- Do not commit secrets, credentials, tokens, private keys, or unnecessary personal data.
- If content is sensitive, summarize and redact before committing.
- If retrieval quality looks noisy, state that and cite only useful results.
- Always report whether a commit/upload actually succeeded and include the returned resource URI when available.
- High-value or conflicting candidates should go to Obsidian review drafts before becoming durable memory.
- Always report whether retrieval, episode append, session commit, or upload actually succeeded.

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import json
import os
import urllib.parse
import urllib.request
from typing import Any
@ -17,3 +18,35 @@ def post_json(path: str, payload: dict[str, Any], gateway_url: str = DEFAULT_GAT
req.add_header("X-API-Key", api_key)
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
def get_json(path: str, params: dict[str, Any] | None = None, gateway_url: str = DEFAULT_GATEWAY_URL, api_key: str = DEFAULT_GATEWAY_API_KEY, timeout: int = 120) -> dict[str, Any] | list[Any]:
query = urllib.parse.urlencode({k: v for k, v in (params or {}).items() if v not in (None, "")})
url = gateway_url.rstrip("/") + path + (f"?{query}" if query else "")
req = urllib.request.Request(url, method="GET")
if api_key:
req.add_header("X-API-Key", api_key)
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
def patch_json(path: str, payload: dict[str, Any], params: dict[str, Any] | None = None, gateway_url: str = DEFAULT_GATEWAY_URL, api_key: str = DEFAULT_GATEWAY_API_KEY, timeout: int = 120) -> dict[str, Any]:
query = urllib.parse.urlencode({k: v for k, v in (params or {}).items() if v not in (None, "")})
url = gateway_url.rstrip("/") + path + (f"?{query}" if query else "")
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(url, data=data, method="PATCH")
req.add_header("Content-Type", "application/json")
if api_key:
req.add_header("X-API-Key", api_key)
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
def delete_json(path: str, params: dict[str, Any] | None = None, gateway_url: str = DEFAULT_GATEWAY_URL, api_key: str = DEFAULT_GATEWAY_API_KEY, timeout: int = 120) -> dict[str, Any]:
query = urllib.parse.urlencode({k: v for k, v in (params or {}).items() if v not in (None, "")})
url = gateway_url.rstrip("/") + path + (f"?{query}" if query else "")
req = urllib.request.Request(url, method="DELETE")
if api_key:
req.add_header("X-API-Key", api_key)
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))

View File

@ -0,0 +1,19 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from _client import DEFAULT_GATEWAY_API_KEY, DEFAULT_GATEWAY_URL, get_json
def main() -> None:
parser = argparse.ArgumentParser(description="Check standalone EverMemOS health through Memory Gateway.")
parser.add_argument("--gateway-url", default=DEFAULT_GATEWAY_URL)
parser.add_argument("--api-key", default=DEFAULT_GATEWAY_API_KEY)
args = parser.parse_args()
print(json.dumps(get_json("/v1/evermemos/health", gateway_url=args.gateway_url, api_key=args.api_key), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,54 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from _client import DEFAULT_GATEWAY_API_KEY, DEFAULT_GATEWAY_URL, post_json
def load_content(args: argparse.Namespace) -> str:
if args.file:
return Path(args.file).read_text(encoding="utf-8")
if args.text:
return args.text
return sys.stdin.read().strip()
def main() -> None:
parser = argparse.ArgumentParser(description="Append session episode memory without directly promoting it.")
parser.add_argument("--user-id", required=True)
parser.add_argument("--session-id", required=True)
parser.add_argument("--agent-id", default="")
parser.add_argument("--workspace-id", default="")
parser.add_argument("--namespace", default="")
parser.add_argument("--text", default="")
parser.add_argument("--file", default="")
parser.add_argument("--tag", action="append", default=[])
parser.add_argument("--source", default="conversation")
parser.add_argument("--gateway-url", default=DEFAULT_GATEWAY_URL)
parser.add_argument("--api-key", default=DEFAULT_GATEWAY_API_KEY)
args = parser.parse_args()
content = load_content(args)
if not content:
parser.error("No episode content provided via --text, --file, or stdin")
payload = {
"user_id": args.user_id,
"agent_id": args.agent_id or None,
"workspace_id": args.workspace_id or None,
"session_id": args.session_id,
"namespace": args.namespace or None,
"content": content,
"tags": args.tag,
"source": args.source,
}
print(json.dumps(post_json("/v1/episodes", payload, args.gateway_url, args.api_key), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,37 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from _client import DEFAULT_GATEWAY_API_KEY, DEFAULT_GATEWAY_URL, post_json
def main() -> None:
parser = argparse.ArgumentParser(description="Commit a session through the minimal EverMemOS consolidation worker.")
parser.add_argument("--user-id", required=True)
parser.add_argument("--session-id", required=True)
parser.add_argument("--agent-id", default="")
parser.add_argument("--workspace-id", default="")
parser.add_argument("--target-namespace", default="")
parser.add_argument("--min-importance", type=float, default=0.6)
parser.add_argument("--no-promote", action="store_true")
parser.add_argument("--gateway-url", default=DEFAULT_GATEWAY_URL)
parser.add_argument("--api-key", default=DEFAULT_GATEWAY_API_KEY)
args = parser.parse_args()
payload = {
"user_id": args.user_id,
"agent_id": args.agent_id or None,
"workspace_id": args.workspace_id or None,
"session_id": args.session_id,
"promote": not args.no_promote,
"min_importance": args.min_importance,
"target_namespace": args.target_namespace or None,
}
print(json.dumps(post_json(f"/v1/sessions/{args.session_id}/commit", payload, args.gateway_url, args.api_key), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from _client import DEFAULT_GATEWAY_API_KEY, DEFAULT_GATEWAY_URL, post_json
def main() -> None:
parser = argparse.ArgumentParser(description="Create or replace a Memory Gateway v1 user.")
parser.add_argument("--user-id", required=True)
parser.add_argument("--display-name", required=True)
parser.add_argument("--preference", action="append", default=[], help="Preference as key=value; repeatable")
parser.add_argument("--gateway-url", default=DEFAULT_GATEWAY_URL)
parser.add_argument("--api-key", default=DEFAULT_GATEWAY_API_KEY)
args = parser.parse_args()
preferences = {}
for item in args.preference:
if "=" not in item:
parser.error(f"Invalid --preference {item!r}; expected key=value")
key, value = item.split("=", 1)
preferences[key.strip()] = value.strip()
payload = {
"user_id": args.user_id,
"display_name": args.display_name,
"preferences": preferences,
}
print(json.dumps(post_json("/v1/users", payload, args.gateway_url, args.api_key), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,31 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from _client import DEFAULT_GATEWAY_API_KEY, DEFAULT_GATEWAY_URL, delete_json
def main() -> None:
parser = argparse.ArgumentParser(description="Delete a MemoryRecord if the caller has access.")
parser.add_argument("--memory-id", required=True)
parser.add_argument("--user-id", required=True)
parser.add_argument("--agent-id", default="")
parser.add_argument("--workspace-id", default="")
parser.add_argument("--session-id", default="")
parser.add_argument("--gateway-url", default=DEFAULT_GATEWAY_URL)
parser.add_argument("--api-key", default=DEFAULT_GATEWAY_API_KEY)
args = parser.parse_args()
params = {
"user_id": args.user_id,
"agent_id": args.agent_id,
"workspace_id": args.workspace_id,
"session_id": args.session_id,
}
print(json.dumps(delete_json(f"/v1/memory/{args.memory_id}", params=params, gateway_url=args.gateway_url, api_key=args.api_key), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from _client import DEFAULT_GATEWAY_API_KEY, DEFAULT_GATEWAY_URL, post_json
def main() -> None:
parser = argparse.ArgumentParser(description="Attach quality feedback to a MemoryRecord.")
parser.add_argument("--memory-id", required=True)
parser.add_argument("--user-id", required=True)
parser.add_argument("--agent-id", default="")
parser.add_argument("--workspace-id", default="")
parser.add_argument("--session-id", default="")
parser.add_argument("--feedback", required=True, choices=["useful", "not_useful", "incorrect", "duplicate", "outdated"])
parser.add_argument("--comment", default="")
parser.add_argument("--gateway-url", default=DEFAULT_GATEWAY_URL)
parser.add_argument("--api-key", default=DEFAULT_GATEWAY_API_KEY)
args = parser.parse_args()
payload = {
"user_id": args.user_id,
"agent_id": args.agent_id or None,
"workspace_id": args.workspace_id or None,
"session_id": args.session_id or None,
"feedback": args.feedback,
"comment": args.comment or None,
}
print(json.dumps(post_json(f"/v1/memory/{args.memory_id}/feedback", payload, args.gateway_url, args.api_key), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,21 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from _client import DEFAULT_GATEWAY_API_KEY, DEFAULT_GATEWAY_URL, get_json
def main() -> None:
parser = argparse.ArgumentParser(description="Get a user's Memory Gateway profile.")
parser.add_argument("--user-id", required=True)
parser.add_argument("--gateway-url", default=DEFAULT_GATEWAY_URL)
parser.add_argument("--api-key", default=DEFAULT_GATEWAY_API_KEY)
args = parser.parse_args()
print(json.dumps(get_json(f"/v1/users/{args.user_id}/profile", gateway_url=args.gateway_url, api_key=args.api_key), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from _client import DEFAULT_GATEWAY_API_KEY, DEFAULT_GATEWAY_URL, get_json
def main() -> None:
parser = argparse.ArgumentParser(description="List namespaces visible to a user/agent/workspace/session context.")
parser.add_argument("--user-id", required=True)
parser.add_argument("--agent-id", default="")
parser.add_argument("--workspace-id", default="")
parser.add_argument("--session-id", default="")
parser.add_argument("--gateway-url", default=DEFAULT_GATEWAY_URL)
parser.add_argument("--api-key", default=DEFAULT_GATEWAY_API_KEY)
args = parser.parse_args()
params = {
"user_id": args.user_id,
"agent_id": args.agent_id,
"workspace_id": args.workspace_id,
"session_id": args.session_id,
}
print(json.dumps(get_json("/v1/namespaces", params=params, gateway_url=args.gateway_url, api_key=args.api_key), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,54 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from _client import DEFAULT_GATEWAY_API_KEY, DEFAULT_GATEWAY_URL, patch_json
def main() -> None:
parser = argparse.ArgumentParser(description="Patch a MemoryRecord.")
parser.add_argument("--memory-id", required=True)
parser.add_argument("--user-id", required=True)
parser.add_argument("--agent-id", default="")
parser.add_argument("--workspace-id", default="")
parser.add_argument("--session-id", default="")
parser.add_argument("--content", default="")
parser.add_argument("--summary", default="")
parser.add_argument("--tag", action="append", default=None)
parser.add_argument("--importance", type=float, default=None)
parser.add_argument("--confidence", type=float, default=None)
parser.add_argument("--visibility", choices=["private", "agent-only", "workspace-shared", "global"], default=None)
parser.add_argument("--gateway-url", default=DEFAULT_GATEWAY_URL)
parser.add_argument("--api-key", default=DEFAULT_GATEWAY_API_KEY)
args = parser.parse_args()
payload = {}
if args.content:
payload["content"] = args.content
if args.summary:
payload["summary"] = args.summary
if args.tag is not None:
payload["tags"] = args.tag
if args.importance is not None:
payload["importance"] = args.importance
if args.confidence is not None:
payload["confidence"] = args.confidence
if args.visibility:
payload["visibility"] = args.visibility
if not payload:
parser.error("No patch fields provided")
params = {
"user_id": args.user_id,
"agent_id": args.agent_id,
"workspace_id": args.workspace_id,
"session_id": args.session_id,
}
print(json.dumps(patch_json(f"/v1/memory/{args.memory_id}", payload, params=params, gateway_url=args.gateway_url, api_key=args.api_key), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from _client import DEFAULT_GATEWAY_API_KEY, DEFAULT_GATEWAY_URL, post_json
def main() -> None:
parser = argparse.ArgumentParser(description="Search v1 Memory Gateway with user/agent/workspace/session ACL.")
parser.add_argument("--query", required=True)
parser.add_argument("--user-id", required=True)
parser.add_argument("--agent-id", default="")
parser.add_argument("--workspace-id", default="")
parser.add_argument("--session-id", default="")
parser.add_argument("--namespace", action="append", default=[], help="Allowed namespace to search; repeatable")
parser.add_argument("--memory-type", action="append", default=[], help="Memory type filter; repeatable")
parser.add_argument("--tag", action="append", default=[], help="Tag filter; repeatable")
parser.add_argument("--limit", type=int, default=5)
parser.add_argument("--gateway-url", default=DEFAULT_GATEWAY_URL)
parser.add_argument("--api-key", default=DEFAULT_GATEWAY_API_KEY)
args = parser.parse_args()
payload = {
"user_id": args.user_id,
"agent_id": args.agent_id or None,
"workspace_id": args.workspace_id or None,
"session_id": args.session_id or None,
"query": args.query,
"namespaces": args.namespace,
"memory_types": args.memory_type,
"tags": args.tag,
"limit": args.limit,
}
print(json.dumps(post_json("/v1/memory/search", payload, args.gateway_url, args.api_key), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,64 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from _client import DEFAULT_GATEWAY_API_KEY, DEFAULT_GATEWAY_URL, post_json
def load_content(args: argparse.Namespace) -> str:
if args.file:
return Path(args.file).read_text(encoding="utf-8")
if args.text:
return args.text
return sys.stdin.read().strip()
def main() -> None:
parser = argparse.ArgumentParser(description="Create a v1 MemoryRecord through Memory Gateway.")
parser.add_argument("--user-id", required=True)
parser.add_argument("--agent-id", default="")
parser.add_argument("--workspace-id", default="")
parser.add_argument("--session-id", default="")
parser.add_argument("--namespace", default="")
parser.add_argument("--memory-type", default="fact")
parser.add_argument("--text", default="")
parser.add_argument("--file", default="")
parser.add_argument("--summary", default="")
parser.add_argument("--tag", action="append", default=[])
parser.add_argument("--importance", type=float, default=0.5)
parser.add_argument("--confidence", type=float, default=0.8)
parser.add_argument("--visibility", choices=["private", "agent-only", "workspace-shared", "global"], default="private")
parser.add_argument("--source", default="manual")
parser.add_argument("--gateway-url", default=DEFAULT_GATEWAY_URL)
parser.add_argument("--api-key", default=DEFAULT_GATEWAY_API_KEY)
args = parser.parse_args()
content = load_content(args)
if not content:
parser.error("No memory content provided via --text, --file, or stdin")
payload = {
"user_id": args.user_id,
"agent_id": args.agent_id or None,
"workspace_id": args.workspace_id or None,
"session_id": args.session_id or None,
"namespace": args.namespace or None,
"memory_type": args.memory_type,
"content": content,
"summary": args.summary or None,
"tags": args.tag,
"importance": args.importance,
"confidence": args.confidence,
"visibility": args.visibility,
"source": args.source,
}
print(json.dumps(post_json("/v1/memory", payload, args.gateway_url, args.api_key), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@ -7,21 +7,37 @@ from _client import DEFAULT_GATEWAY_API_KEY, DEFAULT_GATEWAY_URL, post_json
def main() -> None:
parser = argparse.ArgumentParser(description="Retrieve memory/resources from Memory Gateway.")
parser = argparse.ArgumentParser(description="Retrieve memory/resources from Memory Gateway. Defaults to v1 ACL-aware search when --user-id is provided.")
parser.add_argument("--query", required=True, help="Search query")
parser.add_argument("--uri", default="", help="Optional OpenViking URI scope, e.g. viking://resources/project")
parser.add_argument("--namespace", default="", help="Optional namespace if URI is not provided")
parser.add_argument("--user-id", default="", help="Use v1 ACL-aware search when provided")
parser.add_argument("--agent-id", default="")
parser.add_argument("--workspace-id", default="")
parser.add_argument("--session-id", default="")
parser.add_argument("--limit", type=int, default=5)
parser.add_argument("--gateway-url", default=DEFAULT_GATEWAY_URL)
parser.add_argument("--api-key", default=DEFAULT_GATEWAY_API_KEY)
args = parser.parse_args()
payload = {"query": args.query, "limit": args.limit}
if args.uri:
payload["uri"] = args.uri
if args.namespace:
payload["namespace"] = args.namespace
result = post_json("/api/search", payload, args.gateway_url, args.api_key)
if args.user_id:
payload = {
"user_id": args.user_id,
"agent_id": args.agent_id or None,
"workspace_id": args.workspace_id or None,
"session_id": args.session_id or None,
"query": args.query,
"namespaces": [args.namespace] if args.namespace else [],
"limit": args.limit,
}
result = post_json("/v1/memory/search", payload, args.gateway_url, args.api_key)
else:
payload = {"query": args.query, "limit": args.limit}
if args.uri:
payload["uri"] = args.uri
if args.namespace:
payload["namespace"] = args.namespace
result = post_json("/api/search", payload, args.gateway_url, args.api_key)
print(json.dumps(result, ensure_ascii=False, indent=2))