From de81093feae78011646b3dfedc60f900784bf31d Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 12 Feb 2026 04:17:35 +0000 Subject: [PATCH] Add agent-memory provider Adds support for benchmarking agent-memory (github.com/g1itchbot8888-del/agent-memory): - HTTP bench server integration (port 9876) - Semantic search with graph-enhanced retrieval - Memory relations (extends, contradicts, supersedes) - LearningMachine for meta-memory Run with: bun run benchmark --provider agent-memory Requires agent-memory bench server running locally. --- src/providers/agent-memory/index.ts | 133 ++++++++++++++++++++++++++ src/providers/agent-memory/prompts.ts | 30 ++++++ src/providers/index.ts | 4 +- src/types/provider.ts | 2 +- src/utils/config.ts | 4 + 5 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 src/providers/agent-memory/index.ts create mode 100644 src/providers/agent-memory/prompts.ts diff --git a/src/providers/agent-memory/index.ts b/src/providers/agent-memory/index.ts new file mode 100644 index 0000000..9eecb7c --- /dev/null +++ b/src/providers/agent-memory/index.ts @@ -0,0 +1,133 @@ +import type { + Provider, + ProviderConfig, + IngestOptions, + IngestResult, + SearchOptions, + IndexingProgressCallback, +} from "../../types/provider" +import type { UnifiedSession } from "../../types/unified" +import { logger } from "../../utils/logger" +import { AGENT_MEMORY_PROMPTS } from "./prompts" + +/** + * agent-memory provider for MemoryBench. + * + * Connects to a local agent-memory bench server (Python HTTP wrapper). + * The server manages per-container SQLite databases with semantic embeddings + * and graph-based memory relationships. + * + * Start the server: python -m agent_memory.bench_server --port 9876 + */ +export class AgentMemoryProvider implements Provider { + name = "agent-memory" + prompts = AGENT_MEMORY_PROMPTS + concurrency = { + default: 10, // Local, so moderate concurrency + } + private baseUrl: string = "http://127.0.0.1:9876" + + async initialize(config: ProviderConfig): Promise { + if (config.baseUrl) { + this.baseUrl = config.baseUrl + } + + // Health check + try { + const res = await fetch(`${this.baseUrl}/health`) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data = await res.json() as { status: string } + logger.info(`Connected to agent-memory bench server: ${data.status}`) + } catch (e) { + throw new Error( + `Cannot connect to agent-memory bench server at ${this.baseUrl}. ` + + `Start it with: python -m agent_memory.bench_server --port 9876\n` + + `Error: ${e}` + ) + } + } + + async ingest(sessions: UnifiedSession[], options: IngestOptions): Promise { + // Send sessions in batches to avoid overwhelming the server + const batchSize = 5 + const allDocIds: string[] = [] + + for (let i = 0; i < sessions.length; i += batchSize) { + const batch = sessions.slice(i, i + batchSize) + + const res = await fetch(`${this.baseUrl}/ingest`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + containerTag: options.containerTag, + sessions: batch, + }), + }) + + if (!res.ok) { + const text = await res.text() + throw new Error(`Ingest failed: ${text}`) + } + + const data = await res.json() as { documentIds: string[], count: number } + allDocIds.push(...data.documentIds) + + if (i % 20 === 0 && i > 0) { + logger.info(`Ingested ${i}/${sessions.length} sessions (${allDocIds.length} memories)`) + } + } + + logger.info(`Ingested ${sessions.length} sessions → ${allDocIds.length} memories`) + return { documentIds: allDocIds } + } + + async awaitIndexing( + result: IngestResult, + _containerTag: string, + onProgress?: IndexingProgressCallback + ): Promise { + // agent-memory indexes synchronously on ingest, no waiting needed + const total = result.documentIds.length + onProgress?.({ + completedIds: result.documentIds, + failedIds: [], + total, + }) + } + + async search(query: string, options: SearchOptions): Promise { + const res = await fetch(`${this.baseUrl}/search`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + containerTag: options.containerTag, + query, + limit: options.limit || 30, + }), + }) + + if (!res.ok) { + const text = await res.text() + throw new Error(`Search failed: ${text}`) + } + + const data = await res.json() as { results: unknown[] } + return data.results ?? [] + } + + async clear(containerTag: string): Promise { + const res = await fetch(`${this.baseUrl}/clear`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ containerTag }), + }) + + if (!res.ok) { + logger.warn(`Clear failed for ${containerTag}`) + } else { + logger.info(`Cleared memories for: ${containerTag}`) + } + } +} + +export default AgentMemoryProvider diff --git a/src/providers/agent-memory/prompts.ts b/src/providers/agent-memory/prompts.ts new file mode 100644 index 0000000..34a6430 --- /dev/null +++ b/src/providers/agent-memory/prompts.ts @@ -0,0 +1,30 @@ +import type { ProviderPrompts } from "../../types/prompts" + +export const AGENT_MEMORY_PROMPTS: ProviderPrompts = { + answerPrompt: (question: string, context: unknown[], questionDate?: string) => { + const memories = (context as Array<{ memory?: string; score?: number }>) + .map((r, i) => { + const memory = r.memory || JSON.stringify(r) + const score = r.score ? ` (relevance: ${r.score.toFixed(2)})` : "" + return `${i + 1}. ${memory}${score}` + }) + .join("\n") + + return `You are answering questions based on memories from past conversations. + +${questionDate ? `Current date context: ${questionDate}\n` : ""} +Retrieved memories: +${memories || "(no relevant memories found)"} + +Question: ${question} + +Instructions: +- Answer ONLY based on the retrieved memories above +- If the memories don't contain enough information, say so +- Be specific and cite details from the memories +- For temporal questions, pay attention to dates and sequence of events +- If memories contradict each other, prefer the most recent one + +Answer:` + }, +} diff --git a/src/providers/index.ts b/src/providers/index.ts index d1ed0f8..826ac7c 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -3,11 +3,13 @@ import type { ConcurrencyConfig } from "../types/concurrency" import { SupermemoryProvider } from "./supermemory" import { Mem0Provider } from "./mem0" import { ZepProvider } from "./zep" +import { AgentMemoryProvider } from "./agent-memory" const providers: Record Provider> = { supermemory: SupermemoryProvider, mem0: Mem0Provider, zep: ZepProvider, + "agent-memory": AgentMemoryProvider, } export function createProvider(name: ProviderName): Provider { @@ -35,4 +37,4 @@ export function getProviderInfo(name: ProviderName): { } } -export { SupermemoryProvider, Mem0Provider, ZepProvider } +export { SupermemoryProvider, Mem0Provider, ZepProvider, AgentMemoryProvider } diff --git a/src/types/provider.ts b/src/types/provider.ts index 8bdbd71..4f44228 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -47,4 +47,4 @@ export interface Provider { clear(containerTag: string): Promise } -export type ProviderName = "supermemory" | "mem0" | "zep" +export type ProviderName = "supermemory" | "mem0" | "zep" | "agent-memory" diff --git a/src/utils/config.ts b/src/utils/config.ts index 23b51f6..8d4b403 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -3,6 +3,7 @@ export interface Config { supermemoryBaseUrl: string mem0ApiKey: string zepApiKey: string + agentMemoryBaseUrl: string openaiApiKey: string anthropicApiKey: string googleApiKey: string @@ -13,6 +14,7 @@ export const config: Config = { supermemoryBaseUrl: process.env.SUPERMEMORY_BASE_URL || "https://api.supermemory.ai", mem0ApiKey: process.env.MEM0_API_KEY || "", zepApiKey: process.env.ZEP_API_KEY || "", + agentMemoryBaseUrl: process.env.AGENT_MEMORY_BASE_URL || "http://127.0.0.1:9876", openaiApiKey: process.env.OPENAI_API_KEY || "", anthropicApiKey: process.env.ANTHROPIC_API_KEY || "", googleApiKey: process.env.GOOGLE_API_KEY || "", @@ -26,6 +28,8 @@ export function getProviderConfig(provider: string): { apiKey: string; baseUrl?: return { apiKey: config.mem0ApiKey } case "zep": return { apiKey: config.zepApiKey } + case "agent-memory": + return { apiKey: "local", baseUrl: config.agentMemoryBaseUrl } default: throw new Error(`Unknown provider: ${provider}`) }