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
63 changes: 23 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,6 @@ from xmemory import MemoryBank
bank = MemoryBank(
bank_id="my-agent",
db_url="postgresql://localhost/mydb",
llm_provider="openai",
llm_model="gpt-4o-mini",
embed_model="text-embedding-3-small"
)

# Retain a memory
Expand All @@ -83,27 +80,27 @@ bank.retain("User prefers dark mode in all applications")
# Recall relevant memories
results = bank.recall("What are the user's UI preferences?")
for memory in results:
print(f"[{memory.confidence:.2f}] {memory.text}")
print(memory.text)

# Get memory stats
stats = bank.stats()
print(f"Total memories: {stats.total}, Avg confidence: {stats.avg_confidence}")
print(f"Total memories: {stats.total_memories}, Avg confidence: {stats.avg_confidence}")
```

## Core Features

### 1. Semantic Deduplication
### 1. Semantic Recall

Automatically detects and removes semantically similar memories:
Use lexical search immediately, or semantic search when embeddings are available:

```python
# These will be deduplicated
bank.retain("The project uses Python 3.11")
bank.retain("Python 3.11 is used for the project") # → duplicate detected, skipped
bank.recall("Python 3.11")
bank.recall(
"UI preferences",
query_embedding=[0.1] * 384,
)
```

**Result:** 11.45% memory reduction in production.

### 2. Confidence Scoring

Every memory gets a confidence score (0.0–1.0) based on:
Expand All @@ -118,62 +115,48 @@ high_confidence = [m for m in results if m.confidence >= 0.8]

**Production stats:** Average confidence 0.953 across 11,687 memories.

### 3. Temporal Invalidation
### 3. Persistence and Filtering

Memories are automatically archived when they become outdated:
Memories can be stored with fact types, tags, and context:

```python
bank.retain("Server is running version 2.1") # Later...
bank.retain("Server upgraded to version 2.3") # → old memory archived
bank.retain(
"Server is running version 2.1",
fact_type="world",
context="Deployment note",
tags=["ops", "server"],
)
```

**Production stats:** 29.86% of memories naturally archived.

### 4. Graph Traversal

Navigate related memories through a knowledge graph:

```sql
SELECT * FROM graph_recall(
embedding => (SELECT embedding FROM memory_units WHERE id = 'target-id'),
bank_id => 'my-agent',
query_embedding => (SELECT embedding FROM memory_units WHERE id = 'target-id'),
query_bank_id => 'my-agent',
top_k => 10,
expansion_depth => 2,
min_weight => 0.5
);
```

**Production stats:** 820,016 links (entity: 648K, temporal: 87K, semantic: 83K).

### 5. Cross-Session Consolidation

Link memories across different agents and channels:

```python
# Agent A retains
bank_a.retain("Database migration scheduled for Friday")

# Agent B discovers the link
results = bank_b.recall("any scheduled changes?")
# → Finds the migration memory via cross-bank link
```

**Production stats:** 34,168 cross-bank links across 24 memory banks.
The schema supports cross-bank linking through `memory_links`, but any higher-level consolidation workflow should be verified against the current code before relying on it.

## API Reference

### Memory Operations

| Operation | Method | Description |
|-----------|--------|-------------|
| `recall` | POST | Semantic search with optional graph expansion |
| `retain` | POST | Store new memories with automatic fact extraction |
| `recall` | POST | Lexical search by default, semantic search when embeddings are provided |
| `retain` | POST | Store new memories with fact type, context, tags, and optional embedding |
| `list` | GET | List memories with filtering and pagination |
| `consolidate` | POST | Trigger cross-bank consolidation |
| `stats` | GET | Memory statistics (count, confidence, links) |
| `graph` | GET | Knowledge graph visualization data |
| `entities` | GET | Named entities extracted from memories |
| `tags` | GET | Memory tags and categories |
| `delete` | DELETE | Delete a memory by ID within the active bank |

### Memory Types

Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "xmemory-framework"
version = "1.0.0"
description = "Persistent semantic memory framework for AI agents"
readme = "README.md"
license = {text = "Apache-2.0"}
license = "Apache-2.0"
requires-python = ">=3.10"
authors = [
{name = "Jose Manuel Sabarís García", email = "joker@openclaw.ai"}
Expand All @@ -16,7 +16,6 @@ keywords = ["memory", "ai", "agents", "semantic", "vector", "pgvector"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
Expand Down Expand Up @@ -53,7 +52,6 @@ Issues = "https://github.com/llllJokerllll/xMemory-framework/issues"

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"

[tool.ruff]
target-version = "py310"
Expand All @@ -63,3 +61,6 @@ line-length = 100
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true

[tool.setuptools]
packages = ["xmemory"]
49 changes: 49 additions & 0 deletions tests/test_bank.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,55 @@ def test_recall_has_latency(self, bank):
result = bank.recall("test")
assert result.latency_ms >= 0

def test_recall_without_embedding_uses_lexical_fallback(self, monkeypatch):
class FakeCursor:
def __init__(self):
self.executed = []

def __enter__(self):
return self

def __exit__(self, exc_type, exc, tb):
return False

def execute(self, query, params):
self.executed.append((query, params))

def fetchall(self):
return [
(
"123",
"User prefers dark mode",
"observation",
None,
"Settings",
["ui"],
None,
None,
0,
{},
)
]

class FakeConnection:
def __init__(self):
self.cursor_obj = FakeCursor()

def cursor(self):
return self.cursor_obj

fake_conn = FakeConnection()
bank = MB(bank_id="test-bank", db_url="postgresql://localhost/xmemory_test")
monkeypatch.setattr(bank, "_get_conn", lambda: fake_conn)

result = bank.recall("dark mode")

assert result.total == 1
assert result.memories[0].text == "User prefers dark mode"
query, params = fake_conn.cursor_obj.executed[0]
assert "ILIKE" in query
assert params[1] == "%dark mode%"


# ─── List Tests ───────────────────────────────────────────────────────

Expand Down
42 changes: 40 additions & 2 deletions xmemory/bank.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import psycopg
from pgvector.psycopg import register_vector

from xmemory.models import Memory, MemoryStats, RecallResult, RetainRequest, RetainResult
from xmemory.models import Memory, MemoryStats, RecallResult, RetainResult


class MemoryBank:
Expand Down Expand Up @@ -120,7 +120,7 @@ def recall(
Semantic search for relevant memories.

Uses vector similarity (cosine) for ranking.
Optionally expands results via graph traversal.
Falls back to lexical search when no embedding is provided.
"""
start = time.time()
conn = self._get_conn()
Expand Down Expand Up @@ -163,6 +163,44 @@ def recall(
metadata=row[9] or {},
)
)
else:
with conn.cursor() as cur:
fact_filter = ""
like_query = f"%{query}%"
params: list = [self.bank_id, like_query, like_query, top_k]
if fact_types:
placeholders = ",".join(["%s"] * len(fact_types))
fact_filter = f" AND fact_type IN ({placeholders})"
params = [self.bank_id, like_query, like_query] + fact_types + [top_k]

cur.execute(
f"""
SELECT id, text, fact_type, confidence_score, context,
tags, mentioned_at, created_at, access_count, metadata
FROM memory_units
WHERE bank_id = %s
AND (text ILIKE %s OR context ILIKE %s)
{fact_filter}
ORDER BY created_at DESC
LIMIT %s
""",
params,
)
for row in cur.fetchall():
memories.append(
Memory(
id=str(row[0]),
text=row[1],
fact_type=row[2],
confidence=float(row[3]) if row[3] else None,
context=row[4] or "",
tags=row[5] or [],
mentioned_at=row[6],
created_at=row[7],
access_count=row[8] or 0,
metadata=row[9] or {},
)
)

elapsed = (time.time() - start) * 1000
return RecallResult(
Expand Down
7 changes: 3 additions & 4 deletions xmemory/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
from datetime import datetime
from typing import Optional

from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field


class Memory(BaseModel):
"""A single memory unit."""

model_config = ConfigDict(from_attributes=True)

id: str
text: str
fact_type: str = "observation"
Expand All @@ -22,9 +24,6 @@ class Memory(BaseModel):
metadata: dict = Field(default_factory=dict)
access_count: int = 0

class Config:
from_attributes = True


class RecallResult(BaseModel):
"""Result from a recall query."""
Expand Down