Add Memory Gateway agent plugin

This commit is contained in:
2026-05-06 16:10:04 +08:00
parent e65731a273
commit c44af407d4
48 changed files with 3111 additions and 0 deletions

View File

@ -0,0 +1,187 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
if str(PLUGIN_ROOT) not in sys.path:
sys.path.insert(0, str(PLUGIN_ROOT))
from memory_gateway_plugin.output import dumps_safe, short_id, summarize_data
USER_ID = "test_user_memory_gateway_plugin"
AGENT_ID = "test_hermes_memory_gateway_plugin"
WORKSPACE_ID = "test_workspace_memory_gateway_plugin"
SESSION_ID = "test_session_memory_gateway_plugin_001"
def _assert_test_user(user_id: str) -> None:
if not user_id.startswith("test_user_"):
raise ValueError("cleanup_refuses_non_test_user")
def _gateway_url() -> str:
return os.environ.get("MEMORY_GATEWAY_URL", "http://127.0.0.1:1934").rstrip("/")
def _api_key() -> str:
return os.environ.get("MEMORY_GATEWAY_API_KEY", "")
def _request(method: str, endpoint: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
headers = {"Content-Type": "application/json"}
if _api_key():
headers["X-API-Key"] = _api_key()
body = None if payload is None else json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(_gateway_url() + endpoint, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=15) as response:
raw = response.read().decode("utf-8")
return {"ok": True, "status_code": getattr(response, "status", 200), "data": json.loads(raw) if raw else {}}
except urllib.error.HTTPError as exc:
return {"ok": False, "status_code": exc.code, "error": str(exc.reason)[:300]}
except Exception as exc:
return {"ok": False, "status_code": None, "error": str(exc)[:300]}
def _search_candidates() -> dict[str, Any]:
return _request(
"POST",
"/v1/memory/search",
{
"query": "integration_test plugin safe_to_delete Memory Gateway plugin",
"user_id": USER_ID,
"agent_id": AGENT_ID,
"workspace_id": WORKSPACE_ID,
"session_id": SESSION_ID,
"tags": ["integration_test"],
"limit": 100,
},
)
def _audit_candidate_ids() -> list[str]:
result = _request("GET", "/v1/audit?limit=1000")
if not result.get("ok"):
return []
ids: list[str] = []
for row in result.get("data") or []:
if row.get("actor_user_id") != USER_ID:
continue
if row.get("actor_agent_id") not in {AGENT_ID, None, ""}:
continue
if row.get("target_type") == "memory" and row.get("action") in {"upsert_memory", "feedback:incorrect", "feedback:duplicate", "feedback:outdated"}:
target_id = row.get("target_id")
if target_id and target_id not in ids:
ids.append(target_id)
return ids
def _memory_from_result(item: dict[str, Any]) -> dict[str, Any] | None:
memory = item.get("memory")
if isinstance(memory, dict) and memory.get("id"):
return memory
return None
def _is_cleanup_candidate(memory: dict[str, Any]) -> bool:
if memory.get("user_id") != USER_ID:
return False
tags = set(memory.get("tags") or [])
return bool(tags.intersection({"integration_test", "safe_to_delete", "plugin"}))
def _delete_memory(memory_id: str) -> dict[str, Any]:
query = urllib.parse.urlencode({"user_id": USER_ID, "agent_id": AGENT_ID, "workspace_id": WORKSPACE_ID, "session_id": SESSION_ID})
return _request("DELETE", f"/v1/memory/{urllib.parse.quote(memory_id)}?{query}")
def _feedback_memory(memory_id: str) -> dict[str, Any]:
return _request(
"POST",
f"/v1/memory/{urllib.parse.quote(memory_id)}/feedback",
{
"user_id": USER_ID,
"agent_id": AGENT_ID,
"workspace_id": WORKSPACE_ID,
"session_id": SESSION_ID,
"feedback": "incorrect",
"comment": "cleanup marker for integration test memory",
},
)
def run(user_id: str = USER_ID) -> dict[str, Any]:
_assert_test_user(user_id)
if user_id != USER_ID:
return {"ok": False, "error": "script_is_scoped_to_fixed_test_user", "user_id": user_id}
search = _search_candidates()
if not search.get("ok"):
return {"ok": False, "search": {"ok": False, "status_code": search.get("status_code"), "error": search.get("error")}, "deleted": 0, "feedback_marked": 0, "skipped": 0}
rows = (search.get("data") or {}).get("results") or []
memory_ids = _audit_candidate_ids()
deleted = 0
feedback_marked = 0
skipped = 0
unable: list[dict[str, Any]] = []
touched: list[str] = []
for item in rows:
memory = _memory_from_result(item)
if not memory or not _is_cleanup_candidate(memory):
skipped += 1
continue
memory_id = memory["id"]
if memory_id not in memory_ids:
memory_ids.append(memory_id)
for memory_id in memory_ids:
deletion = _delete_memory(memory_id)
if deletion.get("ok"):
deleted += 1
touched.append(short_id(memory_id))
continue
if deletion.get("status_code") == 404:
skipped += 1
continue
feedback = _feedback_memory(memory_id)
if feedback.get("ok"):
feedback_marked += 1
touched.append(short_id(memory_id))
else:
unable.append({"memory_id": short_id(memory_id), "delete_status": deletion.get("status_code"), "feedback_status": feedback.get("status_code"), "reason": feedback.get("error") or deletion.get("error")})
return {
"ok": not unable,
"search": {"ok": True, "status_code": search.get("status_code"), "data": summarize_data(search.get("data"))},
"deleted": deleted,
"feedback_marked": feedback_marked,
"skipped": skipped,
"unable_count": len(unable),
"unable": unable,
"touched_memory_ids": touched,
"limitation": "search API returns local MemoryRecord rows plus OpenViking context; cleanup only deletes local MemoryRecord rows for the fixed test user.",
}
def main() -> int:
try:
result = run(os.environ.get("MEMORY_GATEWAY_CLEANUP_USER_ID", USER_ID))
except ValueError as exc:
result = {"ok": False, "error": str(exc), "deleted": 0, "feedback_marked": 0, "skipped": 0}
print(dumps_safe(result))
return 0 if result.get("ok") else 1
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,218 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
import sys
import urllib.error
import urllib.request
from dataclasses import dataclass
from pathlib import Path
from typing import Any
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
if str(PLUGIN_ROOT) not in sys.path:
sys.path.insert(0, str(PLUGIN_ROOT))
from memory_gateway_plugin.client import MemoryGatewayClient
from memory_gateway_plugin.config import PluginConfig
from memory_gateway_plugin.output import debug_raw_enabled, dumps_safe, redact, short_id, summarize_data, summarize_result
from memory_gateway_plugin.tools import (
memory_append_episode,
memory_commit_session,
memory_feedback,
memory_search,
memory_upsert,
)
USER_ID = "test_user_memory_gateway_plugin"
AGENT_ID = "test_hermes_memory_gateway_plugin"
WORKSPACE_ID = "test_workspace_memory_gateway_plugin"
SESSION_ID = "test_session_memory_gateway_plugin_001"
def _short(value: Any, max_chars: int = 700) -> str:
text = json.dumps(redact(value), ensure_ascii=False, default=str)
return text[:max_chars]
def _summary_data(data: dict[str, Any] | None) -> dict[str, Any]:
return summarize_data(data) if isinstance(summarize_data(data), dict) else {}
def _result_detail(result: dict[str, Any]) -> str:
return _short(summarize_result(result))
@dataclass
class Step:
name: str
ok: bool
endpoint: str = ""
status_code: int | None = None
detail: str = ""
data: dict[str, Any] | None = None
def to_dict(self) -> dict[str, Any]:
return {
"name": self.name,
"ok": self.ok,
"endpoint": self.endpoint,
"status_code": self.status_code,
"detail": self.detail,
"data": _summary_data(self.data),
}
def _request(method: str, url: str, payload: dict[str, Any] | None = None, api_key: str = "") -> dict[str, Any]:
headers = {"Content-Type": "application/json"}
if api_key:
headers["X-API-Key"] = api_key
body = None if payload is None else json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=10) as response:
raw = response.read().decode("utf-8")
return {"ok": True, "status_code": getattr(response, "status", 200), "data": json.loads(raw) if raw else {}}
except urllib.error.HTTPError as exc:
try:
body_text = exc.read().decode("utf-8")
except Exception:
body_text = str(exc.reason)
return {"ok": False, "status_code": exc.code, "error": body_text[:700]}
except Exception as exc:
return {"ok": False, "status_code": None, "error": str(exc)[:700]}
def _health(config: PluginConfig) -> Step:
endpoint = "/health"
result = _request("GET", config.gateway_url.rstrip("/") + endpoint, api_key=config.api_key)
return Step("health", bool(result.get("ok")), endpoint, result.get("status_code"), _result_detail(result), result.get("data"))
def _ensure_user(config: PluginConfig) -> Step:
endpoint = "/v1/users"
result = _request(
"POST",
config.gateway_url.rstrip("/") + endpoint,
{"user_id": USER_ID, "display_name": "Memory Gateway Plugin Integration Test", "preferences": {"purpose": "integration_test"}},
api_key=config.api_key,
)
ok = bool(result.get("ok")) or result.get("status_code") in {200, 201, 409}
return Step("ensure_user", ok, endpoint, result.get("status_code"), _result_detail(result), result.get("data"))
def _client(config: PluginConfig) -> MemoryGatewayClient:
return MemoryGatewayClient(config)
def run() -> dict[str, Any]:
config = PluginConfig.from_env()
client = _client(config)
steps: list[Step] = []
steps.append(_health(config))
if not steps[-1].ok:
return {"ok": False, "steps": [s.to_dict() for s in steps]}
steps.append(_ensure_user(config))
search_1 = memory_search(
query="Memory Gateway plugin integration test",
user_id=USER_ID,
agent_id=AGENT_ID,
workspace_id=WORKSPACE_ID,
session_id=SESSION_ID,
limit=5,
client=client,
)
steps.append(Step("memory_search_initial", bool(search_1.get("ok")), "/v1/memory/search", search_1.get("status_code"), _result_detail(search_1), search_1.get("data")))
episode = memory_append_episode(
user_id=USER_ID,
agent_id=AGENT_ID,
workspace_id=WORKSPACE_ID,
session_id=SESSION_ID,
content="Integration test: user prefers Memory Gateway plugin to store only summarized episodes, not raw transcripts.",
tags=["integration_test", "plugin"],
source="agent",
importance=0.2,
confidence=0.5,
client=client,
)
steps.append(Step("memory_append_episode", bool(episode.get("ok")), "/v1/episodes", episode.get("status_code"), _result_detail(episode), episode.get("data")))
commit = memory_commit_session(
user_id=USER_ID,
agent_id=AGENT_ID,
workspace_id=WORKSPACE_ID,
session_id=SESSION_ID,
promote=True,
min_importance=0.1,
client=client,
)
commit_detail = _result_detail(commit)
if commit.get("ok") and not (commit.get("data") or {}).get("promoted"):
commit_detail += " | promotion may be empty while commit endpoint succeeded"
steps.append(Step("memory_commit_session", bool(commit.get("ok")), f"/v1/sessions/{SESSION_ID}/commit", commit.get("status_code"), commit_detail, commit.get("data")))
upsert = memory_upsert(
user_id=USER_ID,
agent_id=AGENT_ID,
workspace_id=WORKSPACE_ID,
namespace=f"user/{USER_ID}/long_term",
memory_type="preference",
content="Integration test memory: this should be removable or clearly tagged as test data.",
tags=["integration_test", "plugin", "safe_to_delete"],
importance=0.1,
confidence=0.5,
source="agent",
client=client,
)
steps.append(Step("memory_upsert", bool(upsert.get("ok")), "/v1/memory", upsert.get("status_code"), _result_detail(upsert), upsert.get("data")))
search_2 = memory_search(
query="Integration test memory summarized episodes raw transcripts",
user_id=USER_ID,
agent_id=AGENT_ID,
workspace_id=WORKSPACE_ID,
session_id=SESSION_ID,
limit=10,
client=client,
)
result_count = len((search_2.get("data") or {}).get("results", []))
detail = _result_detail(search_2)
if search_2.get("ok") and result_count == 0:
detail += " | search succeeded but returned no results; indexing or OpenViking sync may be asynchronous"
steps.append(Step("memory_search_after_write", bool(search_2.get("ok")), "/v1/memory/search", search_2.get("status_code"), detail, search_2.get("data")))
memory_id = ((upsert.get("data") or {}).get("memory") or upsert.get("data") or {}).get("id")
if memory_id:
feedback = memory_feedback(
user_id=USER_ID,
agent_id=AGENT_ID,
workspace_id=WORKSPACE_ID,
session_id=SESSION_ID,
memory_id=memory_id,
feedback="reject",
comment="Integration test cleanup marker; safe to ignore/delete.",
client=client,
)
steps.append(Step("memory_feedback", bool(feedback.get("ok")), f"/v1/memory/{memory_id}/feedback", feedback.get("status_code"), _result_detail(feedback), feedback.get("data")))
else:
steps.append(Step("memory_feedback", False, "/v1/memory/{memory_id}/feedback", None, "skipped because memory_upsert did not return memory id"))
required = {"health", "memory_search_initial", "memory_append_episode", "memory_commit_session", "memory_upsert", "memory_search_after_write", "memory_feedback"}
ok = all(step.ok for step in steps if step.name in required)
return {"ok": ok, "gateway_url": config.gateway_url, "debug_raw": debug_raw_enabled(), "test_identity": {"user_id": USER_ID, "agent_id": AGENT_ID, "workspace_id": WORKSPACE_ID, "session_id": SESSION_ID}, "steps": [s.to_dict() for s in steps]}
def main() -> int:
result = run()
print(dumps_safe(result))
return 0 if result.get("ok") else 1
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,32 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import sys
import urllib.error
import urllib.request
from pathlib import Path
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
if str(PLUGIN_ROOT) not in sys.path:
sys.path.insert(0, str(PLUGIN_ROOT))
from memory_gateway_plugin.config import load_config
from memory_gateway_plugin.output import dumps_safe, summarize_data
def main() -> None:
config = load_config()
request = urllib.request.Request(config.gateway_url.rstrip("/") + "/health", method="GET")
if config.api_key:
request.add_header("X-API-Key", config.api_key)
try:
with urllib.request.urlopen(request, timeout=config.timeout) as response:
payload = json.loads(response.read().decode("utf-8"))
print(dumps_safe({"ok": True, "endpoint": "/health", "status_code": getattr(response, "status", 200), "data": summarize_data(payload)}))
except urllib.error.URLError as exc:
print(dumps_safe({"ok": False, "endpoint": "/health", "status_code": None, "error": str(exc)[:300]}))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,174 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
import sys
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any
USER_ID = "test_user_memory_gateway_plugin"
AGENT_ID = "test_hermes_memory_gateway_plugin"
WORKSPACE_ID = "test_workspace_memory_gateway_plugin"
SESSION_ID = "test_session_memory_gateway_plugin_001"
def _ensure_paths() -> None:
plugin_root = Path(__file__).resolve().parents[1]
hermes_repo = Path(os.environ.get("HERMES_REPO", "/home/tom/.hermes/hermes-agent"))
hermes_cli = hermes_repo / "hermes_cli"
for path in [plugin_root, hermes_repo, hermes_cli]:
if str(path) not in sys.path:
sys.path.insert(0, str(path))
_ensure_paths()
from memory_gateway_plugin.output import dumps_safe, summarize_data
def _request(method: str, url: str, payload: dict[str, Any] | None = None, api_key: str = "") -> dict[str, Any]:
headers = {"Content-Type": "application/json"}
if api_key:
headers["X-API-Key"] = api_key
body = None if payload is None else json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=10) as response:
raw = response.read().decode("utf-8")
return {"ok": True, "status_code": getattr(response, "status", 200), "data": json.loads(raw) if raw else {}}
except urllib.error.HTTPError as exc:
try:
body_text = exc.read().decode("utf-8")
except Exception:
body_text = str(exc.reason)
return {"ok": False, "status_code": exc.code, "error": body_text[:500]}
except Exception as exc:
return {"ok": False, "status_code": None, "error": str(exc)[:500]}
def _ensure_user() -> dict[str, Any]:
gateway_url = os.environ.get("MEMORY_GATEWAY_URL", "http://127.0.0.1:1934").rstrip("/")
api_key = os.environ.get("MEMORY_GATEWAY_API_KEY", "")
return _request(
"POST",
gateway_url + "/v1/users",
{"user_id": USER_ID, "display_name": "Memory Gateway Hook Probe", "preferences": {"purpose": "hook_probe"}},
api_key=api_key,
)
def _audit_count(action: str) -> int:
gateway_url = os.environ.get("MEMORY_GATEWAY_URL", "http://127.0.0.1:1934").rstrip("/")
api_key = os.environ.get("MEMORY_GATEWAY_API_KEY", "")
result = _request("GET", gateway_url + "/v1/audit?limit=1000", api_key=api_key)
if not result.get("ok"):
return -1
rows = result.get("data") or []
return sum(
1
for row in rows
if row.get("action") == action
and row.get("actor_user_id") == USER_ID
and row.get("actor_agent_id") == AGENT_ID
)
def _hook_report(manager: Any, hook_name: str, payload: dict[str, Any], audit_action: str = "") -> dict[str, Any]:
registered = hook_name in getattr(manager, "_hooks", {}) and bool(manager._hooks[hook_name])
before = _audit_count(audit_action) if audit_action else -1
try:
result = manager.invoke_hook(hook_name, **payload)
after = _audit_count(audit_action) if audit_action else -1
return {
"registered": registered,
"invoked": True,
"result_type": type(result).__name__,
"result": summarize_data(result),
"audit_action": audit_action,
"audit_delta": (after - before) if before >= 0 and after >= 0 else None,
"error": "",
}
except Exception as exc:
return {
"registered": registered,
"invoked": False,
"result_type": "",
"result": None,
"audit_action": audit_action,
"audit_delta": None,
"error": str(exc)[:500],
}
def run(auto_commit: bool = False) -> dict[str, Any]:
os.environ.setdefault("MEMORY_GATEWAY_URL", "http://127.0.0.1:1934")
os.environ["MEMORY_GATEWAY_DEFAULT_USER_ID"] = USER_ID
os.environ["MEMORY_GATEWAY_DEFAULT_AGENT_ID"] = AGENT_ID
os.environ["MEMORY_GATEWAY_DEFAULT_WORKSPACE_ID"] = WORKSPACE_ID
os.environ["MEMORY_GATEWAY_AUTO_COMMIT_SESSION"] = "true" if auto_commit else "false"
from plugins import PluginManager
ensure_user = _ensure_user()
manager = PluginManager()
manager.discover_and_load()
base = {
"user_id": USER_ID,
"agent_id": AGENT_ID,
"workspace_id": WORKSPACE_ID,
"session_id": SESSION_ID,
"task_id": SESSION_ID,
"model": "hook-probe",
"platform": "cli",
}
hooks = {
"on_session_start": dict(base),
"pre_llm_call": {
**base,
"user_message": "Memory Gateway plugin integration test memory preference",
"conversation_history": [],
"is_first_turn": True,
},
"post_llm_call": {
**base,
"user_message": "请记住Memory Gateway plugin hook probe 偏好保存简短摘要型 episode。",
"assistant_response": "已记录为候选摘要,后续由 session commit 判断是否提升为长期记忆。",
},
"on_session_end": dict(base),
}
audit_actions = {
"pre_llm_call": "memory_search",
"post_llm_call": "append_episode",
"on_session_end": "commit_session",
}
reports = {name: _hook_report(manager, name, payload, audit_actions.get(name, "")) for name, payload in hooks.items()}
plugin = manager._plugins.get("memory-gateway-agent")
return {
"ok": all(item["registered"] and item["invoked"] for item in reports.values()),
"auto_commit": auto_commit,
"ensure_user": {"ok": ensure_user.get("ok"), "status_code": ensure_user.get("status_code"), "data": summarize_data(ensure_user.get("data"))},
"plugin": {
"enabled": bool(plugin and plugin.enabled),
"tools_registered": sorted(getattr(plugin, "tools_registered", []) if plugin else []),
"hooks_registered": sorted(getattr(plugin, "hooks_registered", []) if plugin else []),
"error": getattr(plugin, "error", None) if plugin else "plugin_not_found",
},
"hooks": reports,
}
def main() -> int:
auto_commit = os.environ.get("MEMORY_GATEWAY_AUTO_COMMIT_SESSION", "").strip().lower() in {"1", "true", "yes", "on"}
result = run(auto_commit=auto_commit)
print(dumps_safe(result))
return 0 if result.get("ok") else 1
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,147 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
if str(PLUGIN_ROOT) not in sys.path:
sys.path.insert(0, str(PLUGIN_ROOT))
from memory_gateway_plugin.output import dumps_safe, short_id, summarize_data
USER_ID = "test_user_memory_gateway_plugin"
AGENT_ID = "test_hermes_memory_gateway_plugin"
WORKSPACE_ID = "test_workspace_memory_gateway_plugin"
SESSION_ID = "test_session_memory_gateway_plugin_interactive_002"
PROMPT = "Please remember this integration test preference: Memory Gateway plugin should store summarized episodes, not raw transcripts."
def _request(method: str, url: str, payload: dict[str, Any] | None = None, api_key: str = "") -> dict[str, Any]:
headers = {"Content-Type": "application/json"}
if api_key:
headers["X-API-Key"] = api_key
body = None if payload is None else json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=10) as response:
raw = response.read().decode("utf-8")
return {"ok": True, "status_code": getattr(response, "status", 200), "data": json.loads(raw) if raw else {}}
except urllib.error.HTTPError as exc:
return {"ok": False, "status_code": exc.code, "error": str(exc.reason)[:300]}
except Exception as exc:
return {"ok": False, "status_code": None, "error": str(exc)[:300]}
def _gateway_url() -> str:
return os.environ.get("MEMORY_GATEWAY_URL", "http://127.0.0.1:1934").rstrip("/")
def _api_key() -> str:
return os.environ.get("MEMORY_GATEWAY_API_KEY", "")
def _audit_counts() -> dict[str, int]:
result = _request("GET", _gateway_url() + "/v1/audit?limit=1000", api_key=_api_key())
rows = result.get("data") or []
actions = {"memory_search": 0, "append_episode": 0, "commit_session": 0}
for row in rows:
if row.get("actor_user_id") != USER_ID or row.get("actor_agent_id") != AGENT_ID:
continue
action = row.get("action")
if action in actions:
actions[action] += 1
return actions
def _run_cmd(args: list[str], timeout: int = 20, env: dict[str, str] | None = None) -> dict[str, Any]:
try:
completed = subprocess.run(args, capture_output=True, text=True, timeout=timeout, env=env, check=False)
return {"ok": completed.returncode == 0, "returncode": completed.returncode, "stdout_chars": len(completed.stdout), "stderr_chars": len(completed.stderr)}
except FileNotFoundError:
return {"ok": False, "returncode": None, "error": "command_not_found"}
except subprocess.TimeoutExpired:
return {"ok": False, "returncode": None, "error": "timeout"}
def _manual_instructions(reason: str) -> dict[str, Any]:
return {
"mode": "manual",
"reason": reason,
"commands": [
"hermes plugins list",
"hermes tools list",
"MEMORY_GATEWAY_URL=http://127.0.0.1:1934 MEMORY_GATEWAY_DEFAULT_USER_ID=test_user_memory_gateway_plugin MEMORY_GATEWAY_DEFAULT_AGENT_ID=test_hermes_memory_gateway_plugin MEMORY_GATEWAY_DEFAULT_WORKSPACE_ID=test_workspace_memory_gateway_plugin MEMORY_GATEWAY_AUTO_COMMIT_SESSION=false hermes chat -Q -q 'Please remember this integration test preference: Memory Gateway plugin should store summarized episodes, not raw transcripts.' --source memory-gateway-plugin-test --toolsets memory_gateway",
"python plugins/memory-gateway-agent/scripts/hermes_interactive_session_check.py",
],
"expected": [
"Gateway audit memory_search count increases for the test user/agent.",
"Gateway audit append_episode count increases for the test user/agent.",
"commit_session count does not increase while MEMORY_GATEWAY_AUTO_COMMIT_SESSION=false.",
],
}
def run() -> dict[str, Any]:
hermes = shutil.which("hermes") or "/home/tom/.local/bin/hermes"
plugin_list = _run_cmd([hermes, "plugins", "list"], timeout=10)
tools_list = _run_cmd([hermes, "tools", "list"], timeout=10)
health = _request("GET", _gateway_url() + "/health", api_key=_api_key())
if not health.get("ok"):
return {"ok": False, "mode": "blocked", "plugin_list": plugin_list, "tools_list": tools_list, "gateway_health": {"ok": False, "status_code": health.get("status_code"), "error": health.get("error")}, "manual": _manual_instructions("gateway_unhealthy")}
before = _audit_counts()
env = os.environ.copy()
env.update(
{
"MEMORY_GATEWAY_URL": _gateway_url(),
"MEMORY_GATEWAY_DEFAULT_USER_ID": USER_ID,
"MEMORY_GATEWAY_DEFAULT_AGENT_ID": AGENT_ID,
"MEMORY_GATEWAY_DEFAULT_WORKSPACE_ID": WORKSPACE_ID,
"MEMORY_GATEWAY_AUTO_SEARCH": "true",
"MEMORY_GATEWAY_AUTO_APPEND_EPISODE": "true",
"MEMORY_GATEWAY_AUTO_COMMIT_SESSION": os.environ.get("MEMORY_GATEWAY_AUTO_COMMIT_SESSION", "false"),
}
)
chat = _run_cmd(
[hermes, "chat", "-Q", "-q", PROMPT, "--source", "memory-gateway-plugin-test", "--toolsets", "memory_gateway"],
timeout=int(os.environ.get("MEMORY_GATEWAY_PLUGIN_CHAT_TIMEOUT", "180")),
env=env,
)
after = _audit_counts()
delta = {key: after.get(key, 0) - before.get(key, 0) for key in before}
auto_commit = env["MEMORY_GATEWAY_AUTO_COMMIT_SESSION"].strip().lower() in {"1", "true", "yes", "on"}
expected_commit = delta.get("commit_session", 0) > 0 if auto_commit else delta.get("commit_session", 0) == 0
passed = chat.get("ok") and delta.get("memory_search", 0) > 0 and delta.get("append_episode", 0) > 0 and expected_commit
return {
"ok": bool(passed),
"mode": "auto" if chat.get("ok") else "manual",
"plugin_list": plugin_list,
"tools_list": tools_list,
"gateway_health": {"ok": True, "status_code": health.get("status_code"), "data": summarize_data(health.get("data"))},
"chat": chat,
"auto_commit": auto_commit,
"audit_before": before,
"audit_after": after,
"audit_delta": delta,
"test_identity": {"user_id": USER_ID, "agent_id": AGENT_ID, "workspace_id": WORKSPACE_ID, "session_id": short_id(SESSION_ID)},
"manual": None if chat.get("ok") else _manual_instructions(chat.get("error", "chat_command_failed")),
}
def main() -> int:
result = run()
print(dumps_safe(result))
return 0 if result.get("ok") else 1
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,73 @@
#!/usr/bin/env python3
from __future__ import annotations
import importlib.util
import json
import sys
import types
from pathlib import Path
class FakeHermesContext:
def __init__(self) -> None:
self.registered_tools = []
self.registered_hooks = []
def register_tool(self, name, toolset, schema, handler, **kwargs):
self.registered_tools.append(
{
"name": name,
"toolset": toolset,
"schema": schema,
"handler_callable": callable(handler),
"kwargs": kwargs,
}
)
def register_hook(self, hook_name, callback):
self.registered_hooks.append({"name": hook_name, "handler_callable": callable(callback)})
def load_plugin_module():
plugin_dir = Path(__file__).resolve().parents[1]
init_file = plugin_dir / "__init__.py"
module_name = "hermes_plugins.memory_gateway_agent_smoke"
if "hermes_plugins" not in sys.modules:
parent = types.ModuleType("hermes_plugins")
parent.__path__ = []
sys.modules["hermes_plugins"] = parent
spec = importlib.util.spec_from_file_location(
module_name,
init_file,
submodule_search_locations=[str(plugin_dir)],
)
if spec is None or spec.loader is None:
raise RuntimeError("Cannot create plugin module spec")
module = importlib.util.module_from_spec(spec)
module.__package__ = module_name
module.__path__ = [str(plugin_dir)]
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module
def main() -> None:
module = load_plugin_module()
ctx = FakeHermesContext()
module.register(ctx)
print(
json.dumps(
{
"ok": True,
"has_register": callable(getattr(module, "register", None)),
"registered_tools": ctx.registered_tools,
"registered_hooks": ctx.registered_hooks,
},
ensure_ascii=False,
indent=2,
)
)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,34 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import sys
import uuid
from pathlib import Path
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
if str(PLUGIN_ROOT) not in sys.path:
sys.path.insert(0, str(PLUGIN_ROOT))
from memory_gateway_plugin.output import dumps_safe, summarize_result
from memory_gateway_plugin.tools import memory_append_episode, memory_commit_session, memory_search
def main() -> None:
user_id = "plugin_smoke_user"
agent_id = "plugin_smoke_agent"
session_id = f"plugin_smoke_{uuid.uuid4().hex[:8]}"
episode = memory_append_episode(
user_id=user_id,
agent_id=agent_id,
session_id=session_id,
episode_summary="结论Memory Gateway Agent Plugin smoke test 写入短期 episode。",
tags=["smoke-test"],
)
commit = memory_commit_session(user_id=user_id, agent_id=agent_id, session_id=session_id)
search = memory_search(query="Memory Gateway Agent Plugin smoke test", user_id=user_id, agent_id=agent_id, session_id=session_id)
print(dumps_safe({"episode": summarize_result(episode), "commit": summarize_result(commit), "search": summarize_result(search)}))
if __name__ == "__main__":
main()