Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@
"link": "https://mastra.ai",
"logo": "https://www.google.com/s2/favicons?sz=32&domain=mastra.ai"
},
"memoryhub": {
"key": "memoryhub",
"description": "MemoryHub: Kubernetes-native agent memory with hybrid vector + keyword search, cross-encoder reranking, and reciprocal-rank fusion. PostgreSQL + pgvector backend.",
"kind": "cloud",
"link": "https://github.com/redhat-ai-americas/memory-hub",
"logo": null
},
"mem0": {
"link": "https://mem0.ai",
"logo": "https://www.google.com/s2/favicons?sz=32&domain=mem0.ai",
Expand Down
Binary file added outputs/personamem/memoryhub/rag/32k.json.gz
Binary file not shown.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies = [
"hindsight-all>=0.4",
"supermemory>=0.1",
"httpx>=0.27",
"memoryhub>=0.15",
"qdrant-client>=1.13",
"fastapi[standard]>=0.135.1",
"uvicorn>=0.41.0",
Expand Down
16 changes: 16 additions & 0 deletions results-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -756,5 +756,21 @@
"wall_s": 10665.0,
"tokens_in": 67479034,
"tokens_out": 1049086
},
{
"path": "outputs/personamem/memoryhub/rag/32k.json",
"dataset": "personamem",
"run_name": "memoryhub",
"memory": "memoryhub",
"mode": "rag",
"split": "32k",
"total_queries": 589,
"correct": 493,
"accuracy": 0.8370118845500849,
"ingestion_time_ms": 0.0,
"ingested_docs": 0,
"avg_retrieve_time_ms": 1344.3,
"avg_context_tokens": 159989.6,
"category": null
}
]
2 changes: 2 additions & 0 deletions src/memory_bench/memory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .mastra_om import MastraOMMemoryProvider
from .mem0 import Mem0MemoryProvider
from .mem0_cloud import Mem0CloudMemoryProvider
from .memoryhub import MemoryHubProvider
from .hybrid_search import HybridSearchMemoryProvider
from .ogham import OghamMemoryProvider
from .supermemory import SupermemoryMemoryProvider
Expand All @@ -25,6 +26,7 @@
"mastra-om": MastraOMMemoryProvider,
"mem0": Mem0MemoryProvider,
"mem0-cloud": Mem0CloudMemoryProvider,
"memoryhub": MemoryHubProvider,
"ogham": OghamMemoryProvider,
"qdrant": HybridSearchMemoryProvider,
"supermemory": SupermemoryMemoryProvider,
Expand Down
146 changes: 146 additions & 0 deletions src/memory_bench/memory/memoryhub.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""MemoryHub provider for AMB.

Uses the memoryhub SDK to talk to a MemoryHub MCP server over
streamable-HTTP. MemoryHub stores verbatim conversations and retrieves
via hybrid vector + keyword search with reciprocal-rank fusion and
cross-encoder reranking.

Required env vars
-----------------
MEMORYHUB_URL MCP server endpoint (e.g. https://…/mcp/)
MEMORYHUB_API_KEY API key for session auth

Optional env vars
-----------------
MEMORYHUB_PROJECT_ID Project for memory isolation (default: amb-benchmark)
MEMORYHUB_K Retrieval depth (default: 70)
"""

from __future__ import annotations

import asyncio
import logging
import os
from pathlib import Path
from typing import Any

from memoryhub import MemoryHubClient

from ..models import Document
from .base import MemoryProvider

logger = logging.getLogger(__name__)


class MemoryHubProvider(MemoryProvider):
name = "memoryhub"
description = (
"MemoryHub: hybrid vector + keyword search with "
"cross-encoder reranking and reciprocal-rank fusion."
)
kind = "cloud"
link = "https://github.com/redhat-ai-americas/memory-hub"
concurrency = 1

def __init__(self):
self._url: str | None = None
self._api_key: str | None = None
self._project_id: str | None = None
self._k: int = 70

def prepare(
self,
store_dir: Path,
unit_ids: set[str] | None = None,
reset: bool = True,
) -> None:
self._url = os.environ.get("MEMORYHUB_URL")
self._api_key = os.environ.get("MEMORYHUB_API_KEY")
self._project_id = os.environ.get("MEMORYHUB_PROJECT_ID", "amb-benchmark")
self._k = int(os.environ.get("MEMORYHUB_K", "70"))
if not self._url or not self._api_key:
raise RuntimeError(
"MEMORYHUB_URL and MEMORYHUB_API_KEY are required. "
"Point MEMORYHUB_URL at the MCP server's streamable-HTTP endpoint."
)

# ------------------------------------------------------------------
# Ingest
# ------------------------------------------------------------------

def ingest(self, documents: list[Document]) -> None:
asyncio.run(self._ingest(documents))

async def _ingest(self, documents: list[Document]) -> None:
async with MemoryHubClient(url=self._url, api_key=self._api_key) as client:
try:
await client.create_project(
self._project_id,
description="AMB benchmark memory isolation",
)
logger.info("Created project %s", self._project_id)
except Exception:
logger.debug("Project %s already exists", self._project_id)

for i, doc in enumerate(documents):
owner = f"amb-{doc.user_id}" if doc.user_id else "amb-default"
result = await client.write(
content=doc.content,
scope="project",
project_id=self._project_id,
owner_id=owner,
content_type="experiential",
force=True,
)

if not (result.memory):
logger.warning(
"Write returned no memory for doc %s", doc.id,
)

if (i + 1) % 50 == 0:
logger.info("Ingested %d/%d documents", i + 1, len(documents))

logger.info("Ingestion complete: %d documents", len(documents))

# ------------------------------------------------------------------
# Retrieve
# ------------------------------------------------------------------

def retrieve(
self,
query: str,
k: int | None = None,
user_id: str | None = None,
query_timestamp: str | None = None,
) -> tuple[list[Document], dict | None]:
return asyncio.run(self._retrieve(query, k, user_id))

async def _retrieve(
self,
query: str,
k: int | None,
user_id: str | None,
) -> tuple[list[Document], dict | None]:
effective_k = self._k if (k is None or k == 10) else k
owner = f"amb-{user_id}" if user_id else "amb-default"

async with MemoryHubClient(url=self._url, api_key=self._api_key) as client:
results = await client.search(
query=query,
max_results=effective_k,
owner_id=owner,
project_id=self._project_id,
weight_threshold=0.0,
mode="full_only",
max_response_tokens=0,
)

documents = [
Document(id=m.id, content=m.content, user_id=user_id)
for m in results.results
]
return documents, None

def cleanup(self) -> None:
pass