Involuntary, multi-dimensional memory recall for AI agents
Project description
Ember
Involuntary, multi-dimensional memory recall for AI agents.
Memory is not search. Every AI memory system today retrieves on demand — user asks, system fetches. Ember is the opposite. Memory that happens to you. No intent required.
Multi-dimensional ignition means a single keyword never triggers recall. It requires convergence across semantic, emotional, sensory, temporal, spatial, relational, and musical dimensions — the way human memory actually works.
Quickstart
pip install ember-experiences
from ember import Ember
ember = Ember()
# Index a memory
ember.index(
"Summers growing up in Philadelphia running around until the street lights come on. "
"Chasing fireflies, thunder rumbling, BBQ smoke in the air.",
emotions=["nostalgic", "warm", "alive"],
sensory={"visual": ["fireflies"], "olfactory": ["bbq", "fireworks"], "auditory": ["thunder"]},
location="Levittown, PA",
season="summer",
era="childhood",
importance=0.85,
)
# Check for ignition (run on every message)
results = ember.check("Lightning bugs on the porch, someone grilling down the street...")
for r in results:
print(f"{r.recall_intensity.upper()} — score {r.ignition_score:.2f}, {r.dimensions_fired} dims fired")
Zero config. No API keys. No database server. SQLite + MiniLM out of the box.
Architecture
┌──────────────────────────────────────────┐
│ Incoming Message │
└────────────────┬─────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ Signal Constellation │
│ Extract 7 dimensions from message: │
│ semantic · emotional · sensory │
│ temporal · spatial · relational │
│ musical │
└────────────────┬─────────────────────────┘
│
┌────────────────▼─────────────────────────┐
│ Vector Search │
│ Find candidate embers by embedding │
│ similarity (cosine distance) │
└────────────────┬─────────────────────────┘
│
┌────────────────▼─────────────────────────┐
│ 6 Ignition Gates │
│ │
│ 1. Refractory period (24h cooldown) │
│ 2. Thematic refractory (4h dampen) │
│ 3. Semantic floor (min relevance) │
│ 4. Min dimensions fired (convergence) │
│ 5. Weighted composite + bonuses │
│ 6. Final threshold check │
└────────────────┬─────────────────────────┘
│
┌────────────────▼─────────────────────────┐
│ IgnitionResult │
│ score · intensity · dimensions_fired │
│ dimension_scores · original_text │
└──────────────────────────────────────────┘
7 Dimensions
Every message is decomposed into a signal constellation — a multi-dimensional fingerprint:
| Dimension | What it measures | How |
|---|---|---|
| Semantic | Meaning similarity | Cosine distance (MiniLM/OpenAI embeddings) |
| Emotional | Valence, arousal, label overlap | Lexicon-based extraction |
| Sensory | Visual, auditory, olfactory, tactile, gustatory overlap | Dictionary + pattern matching |
| Temporal | Season, time of day, era alignment | Keyword extraction |
| Spatial | Location name or type match | Known places + type keywords |
| Relational | Shared people | ID overlap |
| Musical | Track, artist, or URI match | Exact + fuzzy matching |
3 Intensity Tiers
| Tier | Score Range | Description |
|---|---|---|
| Faint | 0.28 - 0.49 | A whisper. Background coloring. |
| Warm | 0.50 - 0.67 | Clear recall. Specific details surface. |
| Vivid | 0.68+ | Full sensory immersion. The memory takes over. |
API Reference
Ember(config=None, backend=None, embedding=None)
Create an Ember instance. All parameters optional — defaults to InMemoryBackend + MiniLM.
from ember import Ember, EmberConfig
from ember.backends.sqlite import SQLiteBackend
# Zero-config
ember = Ember()
# With SQLite persistence
ember = Ember(backend=SQLiteBackend())
# With custom config
config = EmberConfig(min_dimensions_fired=2, base_ignition_threshold=0.20)
ember = Ember(config=config)
# With OpenAI embeddings + PostgreSQL
from ember.embeddings.openai import OpenAIBackend
from ember.backends.postgres import PostgresBackend
ember = Ember(
embedding=OpenAIBackend(model='text-embedding-3-small'),
backend=PostgresBackend(dsn='postgresql://user:pass@host/ember'),
)
ember.index(text, **kwargs)
Index a memory as an ember.
ember.index(
"The smell of pine and campfire smoke at 6am...",
emotions=["peaceful", "free"],
sensory={"olfactory": ["pine", "campfire"], "visual": ["mist", "sunrise"]},
location="Yosemite",
location_type="nature",
season="fall",
time_of_day="morning",
era="college",
importance=0.7,
music_track="Into the Mystic",
music_artist="Van Morrison",
)
ember.check(message, context=None)
Check if any stored embers ignite for the given message.
results = ember.check(
"Early morning in the mountains, you can smell the fire pit from last night",
context={"turn_count": 5, "people_ids": []}
)
for r in results:
print(r.recall_intensity) # 'faint', 'warm', or 'vivid'
print(r.ignition_score) # 0.0 - 1.0
print(r.dimensions_fired) # how many of 7 dimensions activated
print(r.dimension_scores) # per-dimension breakdown
print(r.original_text) # the memory that ignited
ember.load_preset(name)
Load a bundled ignition preset. Presets tune thresholds and weights for different use cases.
ember = Ember()
ember.load_preset("creative-writing") # wider net, sensory-heavy
ember.load_preset("therapy-journal") # gentle, emotional focus
ember.load_preset("family-archive") # relational + temporal bias
ember.load_preset("companion") # balanced for AI companions
ember.load_preset_file(path)
Load a custom preset from a YAML file.
ember.load_preset_file("./my-custom-preset.yaml")
ember.register_places(places) / ember.register_people(people)
Register known places and people for spatial and relational scoring.
ember.register_places({
'levittown': 'Levittown, PA',
'el porto': 'El Porto, CA',
})
ember.register_people({
'dad': 'dad',
'mom': 'mom',
})
Backends
Storage
| Backend | Install | Use case |
|---|---|---|
InMemoryBackend |
Built-in | Testing, prototyping |
SQLiteBackend |
pip install ember-experiences[sqlite] |
Production default, zero-config |
PostgresBackend |
pip install ember-experiences[postgres] |
Production at scale |
# SQLite (zero-config, file-based)
from ember.backends.sqlite import SQLiteBackend
backend = SQLiteBackend() # ~/.ember/ember.db
backend = SQLiteBackend(db_path="/path/to/my.db")
# PostgreSQL + pgvector (production-grade)
from ember.backends.postgres import PostgresBackend
backend = PostgresBackend(dsn="postgresql://user:pass@localhost/ember")
backend = PostgresBackend(
host="localhost", port=5432, dbname="ember",
user="ember", password="secret",
table_prefix="myapp_", # optional namespace
)
Embeddings
| Backend | Install | Use case |
|---|---|---|
MiniLMBackend |
Built-in (via sentence-transformers) | Default, local, free, ~12ms |
OpenAIBackend |
pip install ember-experiences[openai] |
Cloud deployments, higher quality |
# MiniLM (default — local, CPU, free)
from ember.embeddings.minilm import MiniLMBackend
embedding = MiniLMBackend() # 384 dimensions
# OpenAI (cloud — requires API key)
from ember.embeddings.openai import OpenAIBackend
embedding = OpenAIBackend() # reads OPENAI_API_KEY from env
embedding = OpenAIBackend(model='text-embedding-3-small') # 1536 dims
embedding = OpenAIBackend(model='text-embedding-3-large') # 3072 dims
embedding = OpenAIBackend(model='text-embedding-ada-002') # 1536 dims (legacy)
Presets
Presets are YAML files that tune Ember's thresholds and weights for specific use cases.
Bundled Presets
| Preset | Focus | Key traits |
|---|---|---|
default |
Balanced | Standard thresholds, 3 dims minimum |
creative-writing |
Sensory, vivid | Lower thresholds, 2 dims minimum, 5 max ignitions |
therapy-journal |
Emotional, gentle | Emotional weight 0.30, 48h refractory, 2 max ignitions |
family-archive |
Relational, temporal | Relational weight 0.20, 12h refractory |
companion |
Emotional, intimate | State modifiers tuned for intimate conversations |
Custom Presets
Create a YAML file with any EmberConfig fields you want to override:
# my-preset.yaml
base_ignition_threshold: 0.20
min_dimensions_fired: 2
thresholds:
sensory: 0.05
emotional: 0.12
weights:
semantic: 0.20
emotional: 0.25
sensory: 0.20
relational: 0.08
temporal: 0.10
spatial: 0.05
music: 0.12
intensity:
faint_floor: 0.20
vivid_floor: 0.60
ember.load_preset_file("my-preset.yaml")
Configuration
All config values are tunable via EmberConfig:
from ember import EmberConfig
from ember.config import ThresholdConfig, WeightConfig
config = EmberConfig(
thresholds=ThresholdConfig(sensory=0.15, emotional=0.25),
weights=WeightConfig(sensory=0.20, emotional=0.20, semantic=0.22,
relational=0.07, temporal=0.09, spatial=0.05, music=0.17),
min_dimensions_fired=2,
base_ignition_threshold=0.25,
)
# Or load from YAML / preset
config = EmberConfig.from_preset("creative-writing")
config = EmberConfig.from_yaml("path/to/config.yaml")
Community
Ember's community layer lets you extend the engine with custom dimensions, custom extractors, and authored experience packs.
Experience Packs
Experience packs are authored memory constellations — someone's lived moments encoded as multi-dimensional data. Load them into your Ember instance and your agent gains the ability to recognize those sensory constellations.
from ember import Ember
ember = Ember()
# Load a bundled pack
count = ember.load_experience("levittown") # Summer in Levittown — fireflies, thunder, BBQ
count = ember.load_experience("el-porto") # Dawn patrol surf sessions
count = ember.load_experience("tokyo-after-midnight") # Neon, ramen, vending machines
# Load a custom pack from a YAML file
count = ember.load_experience("./my-experience.yaml")
Bundled packs:
| Pack | Theme | Embers |
|---|---|---|
levittown |
East Coast summer childhood — fireflies, Kool-Aid, BBQ, street lights | 3 |
el-porto |
California dawn patrol — cold wax, salt air, sunrise sets | 3 |
tokyo-after-midnight |
Neon rain, ramen at 2AM, vending machine glow | 3 |
Creating your own:
# my-experience.yaml
name: "Ba Ngoai's Kitchen"
author: "Your Name"
description: "Fish sauce, lemongrass, cleaver on wood, steam rising"
version: "1.0"
tags: ["family", "food", "vietnam"]
embers:
- text: "The sound of the cleaver on the cutting board..."
emotions: ["nostalgic", "warm", "grateful"]
sensory:
auditory: ["cleaver", "sizzle"]
olfactory: ["fish sauce", "lemongrass"]
location: "Grandmother's kitchen"
location_type: "home"
era: "childhood"
importance: 0.9
vocabulary:
olfactory: ["fish sauce", "lemongrass", "star anise"]
synonyms:
"nuoc mam": "fish sauce"
Custom Dimensions
Add scoring dimensions beyond the built-in 7. Custom dimensions participate in the full ignition pipeline — they're scored, counted toward the fired dimension threshold, and weighted in the composite score.
from ember.community import register_dimension
@register_dimension("culinary", weight=0.10, threshold=0.15)
def score_culinary(constellation, ember):
"""Score culinary overlap between message and memory."""
c_foods = set(constellation.custom_data.get("foods", []))
e_foods = set(ember.get("custom_data", {}).get("foods", []))
if not c_foods or not e_foods:
return 0.0
return len(c_foods & e_foods) / max(len(c_foods), len(e_foods))
Or register on an instance:
ember.register_dimension("culinary", score_culinary, weight=0.10, threshold=0.15)
Custom Extractors
Extractors decompose messages into signals. Custom extractors populate constellation.custom_data, which custom dimensions can read.
from ember.community import register_extractor
@register_extractor("food_detector")
def extract_foods(text, context):
"""Detect food references in text."""
foods = []
for food in ["ramen", "pizza", "sushi", "pho", "tacos"]:
if food in text.lower():
foods.append(food)
return {"foods": foods} if foods else {}
Or register on an instance:
ember.register_extractor("food_detector", extract_foods)
Custom Backend Interface
Storage Backend (4 methods)
from ember.backends.base import StorageBackend
class MyBackend(StorageBackend):
def vector_search(self, embedding: list[float], threshold: float, limit: int) -> list[dict]: ...
def get_recent_ignitions(self, hours: int) -> list[dict]: ...
def store_ember(self, row: dict) -> dict: ...
def record_ignition(self, ignition: dict) -> None: ...
Embedding Backend (3 methods)
from ember.embeddings.base import EmbeddingBackend
class MyEmbedding(EmbeddingBackend):
def embed(self, text: str) -> list[float]: ...
def embed_batch(self, texts: list[str]) -> list[list[float]]: ...
def dimensions(self) -> int: ...
Development
git clone https://github.com/ember-experiences/ember-experiences.git
cd ember-experiences
python -m venv venv && source venv/bin/activate
pip install -e ".[dev]"
pytest tests/ -v
License
Apache 2.0
Project details
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 ember_experiences-0.3.0.tar.gz.
File metadata
- Download URL: ember_experiences-0.3.0.tar.gz
- Upload date:
- Size: 58.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
28d56456b812fac7bdeddfdeb7cc5027add19fa2d4c2b48e105acf6fa02c52b9
|
|
| MD5 |
bd5865198feeed6aa673ebe8b244b826
|
|
| BLAKE2b-256 |
5de15d7ecf7932e9425cc33b97ed2f80b0d896b42505ace63a68ec7fd08feafa
|
Provenance
The following attestation bundles were made for ember_experiences-0.3.0.tar.gz:
Publisher:
publish.yml on ember-experiences/ember-experiences
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ember_experiences-0.3.0.tar.gz -
Subject digest:
28d56456b812fac7bdeddfdeb7cc5027add19fa2d4c2b48e105acf6fa02c52b9 - Sigstore transparency entry: 1013117211
- Sigstore integration time:
-
Permalink:
ember-experiences/ember-experiences@216a2f42ff34bdcced5d35fd82532804f3369d6f -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/ember-experiences
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@216a2f42ff34bdcced5d35fd82532804f3369d6f -
Trigger Event:
release
-
Statement type:
File details
Details for the file ember_experiences-0.3.0-py3-none-any.whl.
File metadata
- Download URL: ember_experiences-0.3.0-py3-none-any.whl
- Upload date:
- Size: 49.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d7b5417f96884711e02c25d4ed8f1f55e748c6c8388b2ee7235daec1d0dd3cfa
|
|
| MD5 |
9ddd3a7829332a8fc66ae4c92169db9d
|
|
| BLAKE2b-256 |
1e4c22cee23afddc35ca13ee8a5fcb0595a2b63d77207231517eccb038b0e671
|
Provenance
The following attestation bundles were made for ember_experiences-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on ember-experiences/ember-experiences
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ember_experiences-0.3.0-py3-none-any.whl -
Subject digest:
d7b5417f96884711e02c25d4ed8f1f55e748c6c8388b2ee7235daec1d0dd3cfa - Sigstore transparency entry: 1013117229
- Sigstore integration time:
-
Permalink:
ember-experiences/ember-experiences@216a2f42ff34bdcced5d35fd82532804f3369d6f -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/ember-experiences
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@216a2f42ff34bdcced5d35fd82532804f3369d6f -
Trigger Event:
release
-
Statement type: