Human-like memory engine for LLM agents
Project description
mengram
mengram is a lightweight memory engine for AI agents.
It gives you:
- A local, persistent store for “memories” (facts, events, rules) on top of SQLite.
- Hybrid lexical + vector recall.
- Prospective memory rules that react to events (e.g. repeated tool errors).
- An LLM-driven auto-ingest pipeline to extract semantic/episodic memories from interaction history.
- Procedural memory (PRAXIS-lite) for env-gated experience reuse.
- Short-term session context tools (turn-aware trimming, summaries, prompt builder).
- An optional FastAPI server if you want to expose everything over HTTP.
Context engineering (short-term memory)
Use SummarizingSession to keep chat history bounded:
- Turn-aware trimming: keep last N user turns (assistant/tool replies stay attached).
- Tool-output trimming:
tool_max_charsdefends against giant tool payloads. - Summarization: injects synthetic summary items when turn limits are exceeded.
- Ingestion window:
window_for_ingest()gives only new items since last checkpoint.
Example (see examples/chat_with_session.py):
from mengram import SummarizingSession, ContextBuilder, SessionItem, Summarizer
session = SummarizingSession(
context_limit_turns=3,
keep_last_n_turns=2,
tool_max_chars=800,
summarizer=my_summarizer, # callable -> SummaryBlock
)
# ... add SessionItem(role="user"/"assistant"/"tool") as your chat runs ...
# Build prompt-ready messages
builder = ContextBuilder(system_prompt="You are concise.")
messages = builder.build_messages(session_items=session.get_items(), memories=long_term_hits)
Invariants to expect:
- Summary appears as synthetic
system+assistantitems (kind=history_summary_prompt/history_summary). - Last
keep_last_n_turnsare preserved verbatim (including their tool/assistant replies). window_for_ingest()returns only new, non-synthetic items since the lastmark_ingested().
Token-budgeted context building
Use ContextBuilder.build_messages_with_budget(...) with a TokenBudgetPolicy and a token counter (defaults to a simple estimator):
from mengram import ContextBuilder, TokenBudgetPolicy
from app.session.tokens import SimpleTokenCounter
policy = TokenBudgetPolicy(
max_input_tokens=8000,
reserved_output_tokens=1000,
max_tokens_memories=800,
max_tokens_summary=1200,
max_tokens_recent_history=4000,
max_tokens_tool_items=1200,
)
builder = ContextBuilder(system_prompt="You are concise.")
messages, report = builder.build_messages_with_budget(
session_items=session.get_items(),
memories=long_term_hits,
policy=policy,
token_counter=SimpleTokenCounter(),
)
Suggested presets:
- Small local/Ollama (4k ctx):
max_input_tokens=3200,reserved_output_tokens=800, tighten section caps proportionally. - Large GPT-4/4.1 (128k ctx):
max_input_tokens=80000,reserved_output_tokens=4000, relax section caps (e.g., memories 4000, summary 8000, history 30000).
Simple Mode (ChatMemory)
Wrap session management, recall, budgeting, and optional auto-ingest with presets:
from mengram import ChatMemory
chat = ChatMemory(client, scope="user", entity_id="demo") # CHAT_DEFAULT preset
result = chat.step("Hi there") # prompt-ready messages + report
def llm_fn(msgs): ...
reply = chat.step_with_llm("Tell me a joke", llm_fn)
# Tool output
chat.add_tool_output(name="search", content="tool output text")
Default summarizer prompt
- Built-in template:
generic.v1(structured headings, UNVERIFIED/Superseded handling, “do not invent facts”, latest-wins). - Uses
SummarizerConfig(template_name="generic.v1", max_summary_words=200, tool_trim_chars=600)by default. - Override via
ChatMemoryConfig(summarizer_config=SummarizerConfig(template_name="generic.v1"))or supply your own template.
You can use it purely as a Python library, or run it as a local service.
Quickstart (Python library)
Install:
pip install mengram
Initialize the schema once per database and start using the client:
from mengram import MemoryClient, init_memory_os_schema
# Initialize the DB schema (safe to call multiple times)
init_memory_os_schema()
client = MemoryClient()
# Store an episodic memory
memory = client.remember(
content="Talked to Alice about refund policy.",
type="episodic",
scope="session",
entity_id="sess-123",
tags=["support", "refund"],
)
# Recall related memories (hybrid lexical + vector search)
results = client.recall(
query="refund policy",
scope="session",
entity_id="sess-123",
)
# Define a prospective rule (e.g. repeated tool errors)
rule = client.create_rule(
condition={
"event_type": "tool:error",
"tool_name": "node_forecast",
"window_minutes": 10,
"threshold_count": 3,
},
actions={
"actions": [
{
"type": "notify",
"channel": "stdout",
"target": "#ops",
"message": "node_forecast failed 3 times in 10 minutes.",
},
{
"type": "inject_memory",
"content": "node_forecast is unstable, consider fallback model.",
},
]
},
)
# Record an event (rules are evaluated and any actions are returned)
event_result = client.record_event(
event_type="tool:error",
tool_name="node_forecast",
scope="session",
entity_id="sess-123",
payload={"error_code": "TIMEOUT"},
)
Auto-ingest with an LLM extractor (golden path)
mengram can automatically extract long-term memories from recent interactions using an LLM-driven extractor.
from mengram import (
MemoryClient,
init_memory_os_schema,
Interaction,
LLMMemoryExtractor,
interactions_from_dicts,
)
# 1) Initialize database + client
init_memory_os_schema()
client = MemoryClient()
# 2) Build interactions (dicts → Interaction helper)
history = [
{"role": "user", "content": "Hi, my name is Dhruv."},
{"role": "assistant", "content": "Nice to meet you Dhruv!"},
{"role": "user", "content": "I work in AI & Analytics at Capgemini."},
{"role": "user", "content": "I prefer morning deliveries if possible."},
]
interactions = interactions_from_dicts(history)
# 3) Wire an LLM client (replace with your provider call)
def llm_client(
prompt: str,
model: str | None = None,
temperature: float | None = None,
) -> str:
"""
Call your LLM provider here and return the raw text response as a string.
This function is responsible for talking to OpenAI/Anthropic/Bedrock/local, etc.
"""
...
extractor = LLMMemoryExtractor(
llm_client=llm_client,
model="gpt-4.1", # optional, forwarded to your client
max_memories=5,
temperature=0.0,
)
# 4) Auto-ingest memories
stored = client.auto_ingest(
interactions=interactions,
extractor=extractor,
scope="user",
entity_id="dhruv",
min_importance=0.0,
)
print("Stored memories:")
for m in stored:
print("-", m.content, f"({m.type}, importance={getattr(m, 'importance', None)})")
Concepts
Memory types
mengram currently focuses on two core memory types:
-
Semantic memory Stable facts or preferences that persist beyond a single session. Examples:
"User works in AI & Analytics at Capgemini.""User prefers morning deliveries."
-
Episodic memory Specific events that might matter later. Examples:
"On 2025-01-15 the user switched from plan A to plan B.""We agreed to review the forecast model next week."
Additional types (e.g. procedural) can be layered on later, but semantic + episodic already cover many useful cases.
Auto-ingest pipeline
The LLM-powered auto-ingest path looks like:
Interaction → Extractor → MemoryCandidate → stored via
MemoryClient.remember()
Where:
Interactiondescribes a turn or event (role, content, timestamp, metadata).- An
Extractoris any callable:Callable[[list[Interaction]], list[MemoryCandidate]]. MemoryCandidateis a proposed memory (content, type, importance, scope/entity, tags, metadata).MemoryClient.auto_ingest(...)takes these candidates, normalizes them, and persists them using your existing memory store.
LLM extractor
LLMMemoryExtractor is a reference implementation of an Extractor that uses an LLM you provide.
- It formats recent interactions as a small transcript.
- Sends a prompt asking the LLM to propose a small set of long-term memories.
- Enforces a JSON array output matching the
MemoryCandidateschema. - Parses the array into
MemoryCandidateobjects and passes them toauto_ingest.
Prospective memory (rules)
Mengram can also encode "when X happens, do Y" behaviors locally. You define rules over events, and record_event(...) evaluates them synchronously and returns actions for your orchestrator to execute.
from mengram import MemoryClient, RuleCondition, NotifyAction, InjectMemoryAction
client = MemoryClient()
rule = client.create_rule(
condition=RuleCondition.tool_error(
tool_name="node_forecast",
window_minutes=10,
threshold_count=3,
scope="user",
entity_id="dhruv",
),
actions=[
NotifyAction(
channel="stdout",
target="#ops",
message="node_forecast failed 3 times in 10 minutes.",
),
InjectMemoryAction(
content="node_forecast is unstable, consider fallback model.",
scope="user",
entity_id="dhruv",
importance=0.7,
),
],
)
result = client.record_event(
event_type="tool:error",
tool_name="node_forecast",
scope="user",
entity_id="dhruv",
payload={"error_code": "TIMEOUT"},
)
for action in result.actions:
# orchestrator executes actions (notify, inject memory, etc.)
...
Rules are evaluated per (scope, entity_id) with a sliding time window. Mengram returns actions; you decide how to execute them (send Slack, insert into prompt, log, etc.).
The record_event(...) return shape is a RuleEvaluationResult with:
triggered_rule_ids: list of rule ids that firedactions: list ofNotifyAction/InjectMemoryActionnew_memories: list of created memory payloads for inject actions
Important: mengram never calls an LLM provider directly.
You supply the llm_client(prompt: str, model: Optional[str], temperature: Optional[float]) -> str function that talks to OpenAI/Anthropic/Bedrock/local models, etc.
Optional: HTTP API server
If you prefer to talk to mengram over HTTP, you can run the bundled FastAPI server.
Install server dependencies
If you’re working from source:
pip install -r requirements.txt
(If you’ve defined an extra in your pyproject, you can also do something like pip install "mengram[server]" from PyPI.)
Run the API with Uvicorn
From the project root:
uvicorn app.main:app --reload
This will start a local service and create a SQLite database (memory.db) in the project root if it doesn’t already exist.
HTTP endpoints
The service exposes:
-
GET /healthzHealth probe. -
POST /v0/rememberStore a memory with optional TTL and tags. -
GET /v0/recallHybrid recall with vector + lexical scoring. -
POST /v0/reflectNaive episodic → semantic session summary. -
POST /v0/planStore prospective-memory rules. -
POST /v0/forgetDelete memories by id or policy. -
POST /v0/eventPersist incoming events and synchronously return triggered rule actions.
Prospective memory rules (V0)
Rules capture simple “pattern → action” contracts that /v0/event enforces:
POST /v0/plan
{
"if": {
"event_type": "tool:error",
"tool_name": "node_forecast",
"window_minutes": 10,
"threshold_count": 3
},
"then": {
"actions": [
{
"type": "notify",
"channel": "slack",
"target": "#ops",
"message": "node_forecast is erroring frequently"
},
{
"type": "inject_memory",
"content": "Last 10 minutes: node_forecast erroring > 3 times."
}
]
}
}
Each time an agent calls POST /v0/event, the service:
- Persists the event,
- Counts recent matches against active rules, and
- Returns any triggered actions in the response (so your orchestrator can notify humans or inject new context into the next turn).
Python client recap
All REST capabilities are also available via the in-process Python client:
from mengram import MemoryClient, init_memory_os_schema
init_memory_os_schema() # safe to call multiple times
client = MemoryClient()
client.remember(
content="met Alice",
type="episodic",
scope="session",
entity_id="sess-42",
)
memories = client.recall(
query="Alice",
scope="session",
entity_id="sess-42",
)
rule = client.create_rule(
condition={
"event_type": "tool:error",
"window_minutes": 10,
"threshold_count": 3,
},
actions={
"actions": [
{"type": "notify", "channel": "stdout", "message": "tool is failing"}
]
},
)
client.record_event(
event_type="tool:error",
tool_name="search_tool",
scope="session",
entity_id="sess-42",
)
You can also run:
python scripts/smoke_client.py
for a quick end-to-end smoke test without starting the FastAPI server.
Custom embeddings & fake-embed smoke tests
MemoryClient accepts a custom embedding function, so you can plug in OpenAI, Bedrock, HuggingFace, or a fake vector generator for offline runs:
import numpy as np
from mengram import MemoryClient, init_memory_os_schema
init_memory_os_schema()
def fake_embed(_: str):
return np.ones(384, dtype=np.float32)
client = MemoryClient(embed_fn=fake_embed)
The scripts/smoke_client.py script supports a fake embedding mode via the MEMORY_OS_FAKE_EMBED=1 environment variable, which avoids downloading the sentence-transformers model:
MEMORY_OS_FAKE_EMBED=1 python scripts/smoke_client.py
Debugging stored memories
For quick inspection of what’s actually stored in your DB, you can use the included debug script:
python examples/debug_list_memories.py --scope user --entity-id dhruv
This will initialize the schema (if needed), connect via MemoryClient, and print out memories for the given scope/entity in a human-readable format.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mengram-0.1.0.tar.gz.
File metadata
- Download URL: mengram-0.1.0.tar.gz
- Upload date:
- Size: 49.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
810a6a597dce094ef20b209c2e47803e1c3bf3859287ec19c65d340be7002802
|
|
| MD5 |
ac758b9ef0b7ad3578d8b127776c672f
|
|
| BLAKE2b-256 |
f0abd2e0bdb62f626cc9c59e81574d089fc21ba5fdb252c740aa778cba905999
|
File details
Details for the file mengram-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mengram-0.1.0-py3-none-any.whl
- Upload date:
- Size: 44.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8fa7c9a98da00023689d609278226ad2151965205daed85f0235d71d92d3f22
|
|
| MD5 |
2f2e3ca9414f2f7df0148e98c813920d
|
|
| BLAKE2b-256 |
b651122775d1ba7fd7f015a8eb4022cb110ea1ff5c94908b18c62a9c2f86f5e2
|