Skip to main content

Involuntary, multi-dimensional memory recall for AI agents

Project description

Ember

Involuntary, multi-dimensional memory recall for AI agents.

CI PyPI License Python

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

ember_experiences-0.3.0.tar.gz (58.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ember_experiences-0.3.0-py3-none-any.whl (49.2 kB view details)

Uploaded Python 3

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

Hashes for ember_experiences-0.3.0.tar.gz
Algorithm Hash digest
SHA256 28d56456b812fac7bdeddfdeb7cc5027add19fa2d4c2b48e105acf6fa02c52b9
MD5 bd5865198feeed6aa673ebe8b244b826
BLAKE2b-256 5de15d7ecf7932e9425cc33b97ed2f80b0d896b42505ace63a68ec7fd08feafa

See more details on using hashes here.

Provenance

The following attestation bundles were made for ember_experiences-0.3.0.tar.gz:

Publisher: publish.yml on ember-experiences/ember-experiences

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ember_experiences-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for ember_experiences-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d7b5417f96884711e02c25d4ed8f1f55e748c6c8388b2ee7235daec1d0dd3cfa
MD5 9ddd3a7829332a8fc66ae4c92169db9d
BLAKE2b-256 1e4c22cee23afddc35ca13ee8a5fcb0595a2b63d77207231517eccb038b0e671

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page