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 GPS proximity, location name or type Haversine distance + known places + 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.

# Text-only (original API)
ember.register_places({
    'levittown': 'Levittown, PA',
})

# With GPS coordinates — enables proximity scoring
ember.register_places({
    'el porto': {'name': 'El Porto, CA', 'lat': 33.895, 'lon': -118.421},
    'home': {'name': 'Home', 'lat': 33.920, 'lon': -118.376},
})

ember.register_people({
    'dad': 'dad',
    'mom': 'mom',
})

GPS / Spatial Proximity

Ember supports GPS coordinate-based spatial scoring. Pass coordinates at index time and current location at check time:

# Index a memory with GPS coordinates
ember.index(
    "Dawn patrol at El Porto, cold wax and salt air...",
    emotions=["alive", "free"],
    sensory={"olfactory": ["salt", "wax"], "tactile": ["cold"]},
    location="El Porto, CA",
    latitude=33.895,       # GPS latitude
    longitude=-118.421,    # GPS longitude
    season="summer",
    time_of_day="morning",
)

# Check with current GPS location
results = ember.check(
    "The waves are great this morning",
    context={
        "location": {"lat": 33.896, "lon": -118.422},  # user's GPS
    },
)

Proximity is scored in distance bands:

Distance Score Meaning
< 1 km 1.0 Same neighborhood
< 5 km 0.7 Same area
< 20 km 0.4 Same city
< 100 km 0.2 Same region
>= 100 km 0.0 Too far

When registered places have coordinates and the user has GPS, Ember auto-resolves nearby places — even if the user doesn't mention them by name. Being physically near a place is enough to activate the spatial dimension.

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.1.tar.gz (63.8 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.1-py3-none-any.whl (51.3 kB view details)

Uploaded Python 3

File details

Details for the file ember_experiences-0.3.1.tar.gz.

File metadata

  • Download URL: ember_experiences-0.3.1.tar.gz
  • Upload date:
  • Size: 63.8 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.1.tar.gz
Algorithm Hash digest
SHA256 6ed4978c913c29db23ee9c157f296afadfef4b9f9fe595f09f143c58c3b8c4ea
MD5 7320691168d2e93d5d2f9a1d438b9e25
BLAKE2b-256 b81be9f971b92df141d50481d02b6e71a802901417a81fe5ee1d5d1ae408f943

See more details on using hashes here.

Provenance

The following attestation bundles were made for ember_experiences-0.3.1.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.1-py3-none-any.whl.

File metadata

File hashes

Hashes for ember_experiences-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 08fb29e575f53475ec8030c35f8fee7292a88b64ba979142fde053ee7f0a7017
MD5 186444a06f17d3bc80393d5d1d4f7e3c
BLAKE2b-256 aeaaaab600de242e0fbd8d7be294a9ef3c79826a03ac1a750c0f34f687fab029

See more details on using hashes here.

Provenance

The following attestation bundles were made for ember_experiences-0.3.1-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