Skip to content
Merged
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
14 changes: 13 additions & 1 deletion agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ def _initialize_agent_by_type(self, agent_config, dataset_config):
self._initialize_cognee_agent(agent_config, dataset_config)
elif self._is_agent_type("zep"):
self._initialize_zep_agent(agent_config)
elif self._is_agent_type("knowl"):
from methods.knowl import initialize_knowl_agent
initialize_knowl_agent(self, agent_config)
elif self._is_agent_type("agentmemory"):
from methods.agentmemory import initialize_agentmemory_agent
initialize_agentmemory_agent(self, agent_config)
elif self._is_agent_type("rag"):
self._initialize_rag_agent(agent_config, dataset_config)
else:
Expand Down Expand Up @@ -271,7 +277,7 @@ def send_message(self, message, memorizing=False, query_id=None, context_id=None
# Route to appropriate agent handler based on agent type
if 'Long_context_agent' in self.agent_name:
return self._handle_long_context_agent(message, memorizing)
elif any(self._is_agent_type(agent_type) for agent_type in ["letta", "cognee", "mem0", "zep"]):
elif any(self._is_agent_type(agent_type) for agent_type in ["letta", "cognee", "mem0", "zep", "knowl", "agentmemory"]):
return self._handle_memory_agent(message, memorizing, query_id, context_id)
elif self._is_agent_type("rag"):
return self._handle_rag_agent(message, memorizing, query_id, context_id)
Expand Down Expand Up @@ -406,6 +412,12 @@ def _handle_memory_agent(self, message, memorizing, query_id, context_id):
return self._handle_mem0_agent(message, memorizing, query_id, context_id)
elif self._is_agent_type("zep"):
return self._handle_zep_agent(message, memorizing, query_id, context_id)
elif self._is_agent_type("knowl"):
from methods.knowl import handle_knowl_agent
return handle_knowl_agent(self, message, memorizing, query_id, context_id)
elif self._is_agent_type("agentmemory"):
from methods.agentmemory import handle_agentmemory_agent
return handle_agentmemory_agent(self, message, memorizing, query_id, context_id)
else:
raise NotImplementedError(f"Memory agent type not supported: {self.agent_name}")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# agentmemory v0.9.29, driven over REST. Every value except the output_dir is copied from
# Simple_rag_bm25 so the row sits beside the published baselines on the same terms:
# retrieve_num 10 is what BM25, Zep, Cognee, HippoRAG-v2 and the embedding baselines use.
agent_name: Agentic_memory_agentmemory
model: gpt-4o-mini
temperature: 0.7
input_length_limit: 10000000
buffer_length: 200
output_dir: ./outputs/agentmemory-gpt-4o-mini

retrieve_num: 10
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Knowl, supersession OFF -- the ablation arm.
#
# Every value except the knowl_* pair is copied from the baselines rather than chosen:
# temperature and input_length_limit match every gpt-4o-mini config in the repo, buffer_length
# matches Simple_rag_bm25, and retrieve_num 10 is what BM25, Zep, Cognee, HippoRAG-v2, RAPTOR,
# GraphRAG, Self-RAG and all four embedding baselines use. Mem0 is the outlier at 100.
agent_name: Agentic_memory_knowl_nosupersede
model: gpt-4o-mini
temperature: 0.7
input_length_limit: 10000000
buffer_length: 200
output_dir: ./outputs/knowl-gpt-4o-mini-nosupersede

retrieve_num: 10
knowl_supersede: false
knowl_vector: true
16 changes: 16 additions & 0 deletions configs/agent_conf/RAG_Agents/gpt-4o-mini/Knowl_gpt-4o-mini.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Knowl, supersession ON. This is the published arm.
#
# Every value except the knowl_* pair is copied from the baselines rather than chosen:
# temperature and input_length_limit match every gpt-4o-mini config in the repo, buffer_length
# matches Simple_rag_bm25, and retrieve_num 10 is what BM25, Zep, Cognee, HippoRAG-v2, RAPTOR,
# GraphRAG, Self-RAG and all four embedding baselines use. Mem0 is the outlier at 100.
agent_name: Agentic_memory_knowl
model: gpt-4o-mini
temperature: 0.7
input_length_limit: 10000000
buffer_length: 200
output_dir: ./outputs/knowl-gpt-4o-mini

retrieve_num: 10
knowl_supersede: true
knowl_vector: true
190 changes: 190 additions & 0 deletions methods/agentmemory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
"""agentmemory as a memory method for MemoryAgentBench.

agentmemory (github.com/rohitg00/agentmemory) is a Node service built on the iii engine. It is
driven here over its REST surface, so nothing is installed into this venv:

cd /path/to/agentmemory && node dist/cli.mjs # REST on :3111
export AGENTMEMORY_URL=http://127.0.0.1:3111 # optional, this is the default

It is not evaluated on MemoryAgentBench upstream -- their published numbers are LongMemEval-S
R@5, a retrieval-recall metric on a different task -- so this is a new measurement rather than a
reproduction, and there is no vendor figure to check it against.

NORMALIZED INPUT. Both systems receive the identical parsed fact list, in context order, one
record per fact. `parse_fact_lines` below is a faithful port of `facts.ts:parseFactLines` from the
Knowl repository, marker rule included. That is the standing benchmark decision: normalized
retrieval compares identical prepared records with no system-specific extraction, so neither side
gets a cleaner corpus than the other.

Feeding raw 4096-char chunks instead was considered and rejected. agentmemory stores one memory
per `remember` call, so a chunk would land as a single record holding ~70 facts -- supersession
could never fire and retrieval would return a wall of text. That would measure our chunking
choice, not their memory.

ISOLATION. Every run writes under a unique `project`. agentmemory's supersession guard skips a
candidate only when both sides carry an explicit and different project (an unscoped record is
treated as a wildcard), so a per-run project name keeps runs from seeing each other as long as
every write is scoped -- which it is here.
"""

import json
import os
import re
import time
import urllib.error
import urllib.request

DEFAULT_URL = "http://127.0.0.1:3111"


def _post(base_url, path, payload, timeout=120):
data = json.dumps(payload).encode("utf-8")
request = urllib.request.Request(
f"{base_url}{path}",
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))


def strip_trailing_period(text):
text = text.strip()
return text[:-1].strip() if text.endswith(".") else text


def parse_fact_lines(context):
"""Port of facts.ts:parseFactLines.

The CR context is a numbered list, `0.` through `N.`. A marker counts only when its number is
the one expected next, so a stray "3." inside a fact's own text is kept as text rather than
splitting it, and any header before "0." is dropped for free.
"""
starts = []
expected = 0
for match in re.finditer(r"(\d+)\.", context):
if int(match.group(1)) != expected:
continue
starts.append(match.end())
expected += 1

if not starts:
return []

facts = []
for position, start in enumerate(starts):
if position + 1 < len(starts):
end = context.rfind(f"{position + 1}.", 0, starts[position + 1])
else:
end = len(context)
facts.append(strip_trailing_period(context[start:end]))
return [f for f in facts if f]


class AgentMemoryClient:
def __init__(self, base_url, project):
self.base_url = base_url.rstrip("/")
self.project = project
self.chunks = []
self.flushed = False
self.facts = 0
self.superseded = 0

def add(self, text):
self.chunks.append(text)

def flush(self):
"""Write every parsed fact, in context order. Idempotent, like the Knowl bridge's flush.

Order is the only recency signal the task provides -- nothing marks a fact as an update,
which is the whole point of FactConsolidation.
"""
if self.flushed:
return {"facts": self.facts, "superseded": self.superseded}

facts = parse_fact_lines("".join(self.chunks))
superseded = 0
for fact in facts:
result = _post(
self.base_url,
"/agentmemory/remember",
{"content": fact, "project": self.project},
)
memory = result.get("memory") or {}
if memory.get("supersedes"):
superseded += 1

self.facts = len(facts)
self.superseded = superseded
self.flushed = True
print(f"\nagentmemory flush: {self.facts} facts, {superseded} superseded at write\n")
return {"facts": self.facts, "superseded": superseded}

def query(self, text, k):
result = _post(
self.base_url,
"/agentmemory/search",
{"query": text, "limit": k, "project": self.project},
)
contents = []
for row in (result.get("results") or [])[:k]:
observation = row.get("observation") or {}
content = observation.get("narrative") or observation.get("title") or ""
if not content:
facts = observation.get("facts") or []
content = "\n".join(facts)
if content:
contents.append(content)
return contents


def initialize_agentmemory_agent(agent, agent_config=None):
config = agent_config or {}
agent.retrieve_num = config["retrieve_num"]
agent.context = ""
agent.agent_start_time = time.time()

base_url = os.environ.get("AGENTMEMORY_URL", DEFAULT_URL)
project = f"mab_{agent.sub_dataset}_{os.getpid()}_{int(time.time())}"
agent.agentmemory = AgentMemoryClient(base_url, project)
print(f"\n\nagentmemory at {base_url}, project={project}\n\n")


def handle_agentmemory_agent(agent, message, memorizing, query_id, context_id):
"""Mirror `_handle_bm25_rag`: same query extraction, same reader assembly."""
from methods.knowl import build_reader_messages, format_retrieval_memory_string
from utils.templates import get_template

if memorizing:
agent.agentmemory.add(message)
return "Memorized"

start_time = time.time()
stats = agent.agentmemory.flush()
memory_construction_time = time.time() - start_time

retrieval_query = agent._extract_retrieval_query(message)
contents = agent.agentmemory.query(retrieval_query, agent.retrieve_num)
retrieval_memory_string = format_retrieval_memory_string(contents)

system_message = get_template(agent.sub_dataset, "system", agent.agent_name)
format_message = build_reader_messages(retrieval_memory_string, message, system_message)

response = agent._create_oai_client().chat.completions.create(
model=agent.model,
messages=format_message,
temperature=agent.temperature,
max_tokens=agent.max_tokens if "gpt-4" in agent.model else None,
)

query_time_len = time.time() - start_time - memory_construction_time
print(f"\nagentmemory stats: {stats}\n")

return agent._create_standard_response(
response.choices[0].message.content,
response.usage.prompt_tokens,
response.usage.completion_tokens,
memory_construction_time,
query_time_len,
)
Loading