Add Memory Gateway agent plugin
This commit is contained in:
187
plugins/memory-gateway-agent/scripts/cleanup_test_memories.py
Normal file
187
plugins/memory-gateway-agent/scripts/cleanup_test_memories.py
Normal 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())
|
||||
218
plugins/memory-gateway-agent/scripts/gateway_e2e_check.py
Normal file
218
plugins/memory-gateway-agent/scripts/gateway_e2e_check.py
Normal 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())
|
||||
32
plugins/memory-gateway-agent/scripts/health.py
Normal file
32
plugins/memory-gateway-agent/scripts/health.py
Normal 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()
|
||||
174
plugins/memory-gateway-agent/scripts/hermes_hook_probe.py
Normal file
174
plugins/memory-gateway-agent/scripts/hermes_hook_probe.py
Normal 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())
|
||||
@ -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())
|
||||
73
plugins/memory-gateway-agent/scripts/hermes_smoke_check.py
Normal file
73
plugins/memory-gateway-agent/scripts/hermes_smoke_check.py
Normal 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()
|
||||
34
plugins/memory-gateway-agent/scripts/smoke_test.py
Normal file
34
plugins/memory-gateway-agent/scripts/smoke_test.py
Normal 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()
|
||||
Reference in New Issue
Block a user