219 lines
8.2 KiB
Python
219 lines
8.2 KiB
Python
#!/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())
|