Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
110 changes: 110 additions & 0 deletions examples/migrations/manzoma/manzoma_migration_showcase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@

import os
import sys
import tempfile
from pathlib import Path
from datetime import datetime, timezone
from unittest.mock import MagicMock

# Ensure we can import memanto
sys.path.append(str(Path(__file__).parent.parent.parent.parent))

from memanto.cli.migrate.runner import run_migration
from memanto.cli.migrate.okf_loader import load_okf_bundle

def run_manzoma_freedom_showcase():
"""
SHOWCASE: The Manzoma Freedom Loop 🦋

Scenario: A retail business in Egypt (Manzoma ERP) wants to migrate
memories for 3 different branches (Alpha, Beta, Gamma) into Memanto
without losing agent context.
"""
print("🚀 Starting Manzoma -> Memanto Multi-Agent Migration Showcase")

with tempfile.TemporaryDirectory() as tmp_dir:
showcase_dir = Path(tmp_dir)

branches = {
"branch_alpha": "Branch in Damietta - Focus on high-volume furniture sales.",
"branch_beta": "Branch in Cairo - Focus on tech gadgets and accessories.",
"branch_gamma": "Branch in Alexandria - Focus on apparel and clothing."
}

for agent_id, context in branches.items():
content = f"""---
type: observation
title: {agent_id} Market Context
agent_id: {agent_id}
tags: [manzoma, retail, egypt]
timestamp: {datetime.now(timezone.utc).isoformat()}
---
{context}
Inventory sync status: 100% Verified.
Total volume processed: $10,000.
"""
(showcase_dir / f"{agent_id}.md").write_text(content)

print(f"✅ Prepared OKF bundle with {len(branches)} agent-specific memories in temporary directory.")

# 2. Setup a robust mock client that simulates agent isolation
mock_client = MagicMock()
mock_client.agent_id = "initial_agent"

# Track "stored" memories per agent
storage = {agent_id: [] for agent_id in branches.keys()}

def mock_batch_remember(agent_id, memories):
if agent_id not in storage:
storage[agent_id] = []
storage[agent_id].extend(memories)
return {"results": [], "successful": len(memories), "failed": 0}

def mock_activate_agent(agent_id):
print(f"🔄 Client: Activating session for agent '{agent_id}'")
mock_client.agent_id = agent_id
return {"status": "active", "agent_id": agent_id}

mock_client.batch_remember.side_effect = mock_batch_remember
mock_client.activate_agent.side_effect = mock_activate_agent

print("📦 Executing Multi-Agent Migration...")

# Load the bundle
bundle = load_okf_bundle(showcase_dir)

# Run the migration
summary, rows = run_migration(
provider="okf",
export=bundle,
client=mock_client,
agent_id="default_fallback",
dry_run=False,
on_progress=print
)

# 3. Verification of Round-Trip Fidelity and Agent Isolation
print("\n--- 📊 Migration Verification ---")

all_passed = True
for agent_id, expected_context in branches.items():
stored_memories = storage.get(agent_id, [])
if len(stored_memories) == 1:
stored_content = stored_memories[0]['content']
if expected_context in stored_content:
print(f"✅ Agent '{agent_id}': Data verified and isolated.")
else:
print(f"❌ Agent '{agent_id}': Data corruption detected!")
all_passed = False
else:
print(f"❌ Agent '{agent_id}': Expected 1 memory, found {len(stored_memories)}.")
all_passed = False

if all_passed:
print("\n🏆 SHOWCASE SUCCESS: All Manzoma branches migrated with perfect isolation!")
print("Proof of the 'Freedom Loop': Data -> OKF -> Multi-Agent Memanto.")
else:
print("\n❌ SHOWCASE FAILED: Verification errors found.")

if __name__ == "__main__":
run_manzoma_freedom_showcase()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
4 changes: 4 additions & 0 deletions memanto/app/services/okf_export_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,10 @@ def _render_okf_doc(self, mem: dict[str, Any], mem_type: str) -> str:
if source_ref:
frontmatter["resource"] = source_ref

agent_id = mem.get("agent_id")
if agent_id:
frontmatter["agent_id"] = agent_id

x_memanto: dict[str, Any] = {}
for key in ("id", "confidence", "provenance", "source", "status"):
val = mem.get(key)
Expand Down
8 changes: 8 additions & 0 deletions memanto/cli/migrate/mappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ def map_mem0(export: dict[str, Any]) -> list[dict[str, Any]]:

tags = list(dict.fromkeys(categories))
scope = mem.get("export_scope") or {}
agent_id = scope.get("agent_id")
scope_tag = _scope_tag(scope)
if scope_tag:
tags.append(scope_tag)
Expand Down Expand Up @@ -223,6 +224,7 @@ def map_mem0(export: dict[str, Any]) -> list[dict[str, Any]]:
"title": _title_from(content),
"content": _attach_footer(content, footer),
"type": memory_type,
"agent_id": agent_id,
"tags": tags,
"confidence": 0.8,
"source": "mem0",
Expand Down Expand Up @@ -282,6 +284,7 @@ def map_letta(export: dict[str, Any]) -> list[dict[str, Any]]:
"title": _title_from(content),
"content": _attach_footer(content, footer),
"type": "observation",
"agent_id": agent_id,
"tags": tags,
"confidence": 0.8,
"source": "letta",
Expand Down Expand Up @@ -344,6 +347,7 @@ def map_supermemory(export: dict[str, Any]) -> list[dict[str, Any]]:
"title": _title_from(content),
"content": _attach_footer(content, footer),
"type": None,
"agent_id": None,
"tags": tags,
"confidence": 0.8,
"source": "supermemory",
Expand Down Expand Up @@ -392,6 +396,7 @@ def map_supermemory(export: dict[str, Any]) -> list[dict[str, Any]]:
"title": _title_from(content),
"content": _attach_footer(content, footer),
"type": "artifact",
"agent_id": None,
"tags": doc_tags,
"confidence": 0.7,
"source": "supermemory",
Expand Down Expand Up @@ -441,6 +446,8 @@ def map_okf(export: dict[str, Any]) -> list[dict[str, Any]]:
okf_type = entry.get("type")
memory_type = _coerce_type(x_memanto.get("type")) or _coerce_type(okf_type)

agent_id = entry.get("agent_id")

tags = [str(t) for t in (entry.get("tags") or []) if t]
resource = entry.get("resource")

Expand Down Expand Up @@ -475,6 +482,7 @@ def map_okf(export: dict[str, Any]) -> list[dict[str, Any]]:
"title": title or _title_from(content),
"content": content,
"type": memory_type,
"agent_id": agent_id,
"tags": tags,
"confidence": confidence,
"source": source,
Expand Down
2 changes: 2 additions & 0 deletions memanto/cli/migrate/okf_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"resource",
"tags",
"timestamp",
"agent_id",
"x_memanto",
}

Expand Down Expand Up @@ -160,6 +161,7 @@ def _parse_entry(chunk: str, file_path: Path, rel_base: Path) -> dict[str, Any]
"resource": frontmatter.get("resource"),
"tags": tags,
"timestamp": frontmatter.get("timestamp"),
"agent_id": frontmatter.get("agent_id"),
"body": body,
"x_memanto": x_memanto,
"links": links,
Expand Down
43 changes: 36 additions & 7 deletions memanto/cli/migrate/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,18 +291,47 @@ def run_migration(
if dry_run or not rows:
return summary, rows

batches = list(chunked(rows, BATCH_LIMIT))
summary.batches = len(batches)
# Group rows by effective agent_id. If a row carries its own agent_id, it
# takes precedence over the CLI-supplied target_agent (the fallback).
grouped_rows: dict[str, list[dict[str, Any]]] = {}
for row in rows:
target = row.get("agent_id") or agent_id
if not isinstance(target, str) or not target.strip():
summary.failed += 1
summary.errors.append(
f"Skipping record: invalid or missing target agent_id '{target}'."
)
continue
grouped_rows.setdefault(target, []).append(row)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Flatten back into chunked batches, but now grouped by agent.
all_batches: list[tuple[str, list[dict[str, Any]]]] = []
for target_agent, agent_rows in grouped_rows.items():
for batch in chunked(agent_rows, BATCH_LIMIT):
all_batches.append((target_agent, batch))

summary.batches = len(all_batches)

from memanto.app.utils.errors import MemoryError

for idx, batch in enumerate(batches, 1):
for idx, (target_agent, batch) in enumerate(all_batches, 1):
if on_progress:
on_progress(
f"Importing batch {idx}/{len(batches)} ({len(batch)} memories)..."
)
msg = f"Importing batch {idx}/{len(all_batches)} ({len(batch)} memories)"
if len(grouped_rows) > 1:
msg += f" for agent '{target_agent}'"
on_progress(f"{msg}...")

try:
result = client.batch_remember(agent_id=agent_id, memories=batch)
# Ensure the client has an active session for this specific agent
if client and client.agent_id != target_agent:
try:
client.activate_agent(target_agent)
except Exception as exc:
summary.failed += len(batch)
summary.errors.append(f"batch {idx}: failed to activate agent '{target_agent}': {exc}")
continue

result = client.batch_remember(agent_id=target_agent, memories=batch)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
except MemoryError:
raise
except Exception as exc:
Expand Down
72 changes: 72 additions & 0 deletions tests/test_migrate_multi_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@

import pytest
from pathlib import Path
from memanto.cli.migrate.okf_loader import load_okf_bundle
from memanto.cli.migrate.mappers import map_okf
from memanto.cli.migrate.runner import run_migration
from unittest.mock import MagicMock

def test_okf_agent_id_preservation(tmp_path):
"""Verify that agent_id is preserved through loader and mapper."""
okf_content = """---
type: fact
title: Multi-Agent Test
agent_id: agent_alpha
tags: [test]
---
This is a memory for Alpha.
"""
test_file = tmp_path / "test_memory.md"
test_file.write_text(okf_content)

# 1. Test Loader
bundle = load_okf_bundle(test_file)
entry = bundle['memories'][0]
assert entry.get('agent_id') == 'agent_alpha'

# 2. Test Mapper
mapped = map_okf(bundle)
payload = mapped[0]
assert payload.get('agent_id') == 'agent_alpha'

def test_run_migration_multi_agent_grouping():
"""Verify that run_migration groups batches by agent_id correctly."""
# Mock data with different agents
rows = [
{"title": "M1", "content": "C1", "agent_id": "agent_1"},
{"title": "M2", "content": "C2", "agent_id": "agent_1"},
{"title": "M3", "content": "C3", "agent_id": "agent_2"},
]

mock_client = MagicMock()
mock_client.batch_remember.return_value = {"results": [], "successful": 1, "failed": 0}

# We need to mock map_export to return our custom rows
import memanto.cli.migrate.runner as runner
original_map_export = runner.map_export
runner.map_export = MagicMock(return_value=rows)

try:
summary, _ = run_migration(
provider="okf",
export={},
client=mock_client,
agent_id="default_agent", # This should be overridden by row['agent_id']
dry_run=False
)

# Verify batch_remember was called twice (once per agent)
assert mock_client.batch_remember.call_count == 2

# Verify first call was for agent_1
args, kwargs = mock_client.batch_remember.call_args_list[0]
assert kwargs['agent_id'] == 'agent_1'
assert len(kwargs['memories']) == 2

# Verify second call was for agent_2
args, kwargs = mock_client.batch_remember.call_args_list[1]
assert kwargs['agent_id'] == 'agent_2'
assert len(kwargs['memories']) == 1

finally:
runner.map_export = original_map_export
Loading