Metadata-Version: 2.4
Name: ember-experiences
Version: 0.3.1
Summary: Involuntary, multi-dimensional memory recall for AI agents
Author: Robert Praul
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/ember-experiences/ember-experiences
Project-URL: Documentation, https://github.com/ember-experiences/ember-experiences#readme
Project-URL: Repository, https://github.com/ember-experiences/ember-experiences
Keywords: ai,memory,agents,recall,embeddings
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: sentence-transformers>=2.2.0
Provides-Extra: sqlite
Requires-Dist: sqlite-vss>=0.1.2; extra == "sqlite"
Provides-Extra: openai
Requires-Dist: openai>=1.0.0; extra == "openai"
Provides-Extra: postgres
Requires-Dist: psycopg2-binary>=2.9.0; extra == "postgres"
Requires-Dist: pgvector>=0.2.0; extra == "postgres"
Provides-Extra: presets
Requires-Dist: pyyaml>=6.0; extra == "presets"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pyyaml>=6.0; extra == "dev"
Provides-Extra: all
Requires-Dist: sqlite-vss>=0.1.2; extra == "all"
Requires-Dist: openai>=1.0.0; extra == "all"
Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
Requires-Dist: pgvector>=0.2.0; extra == "all"
Requires-Dist: pyyaml>=6.0; extra == "all"
Dynamic: license-file

# Ember

**Involuntary, multi-dimensional memory recall for AI agents.**

[![CI](https://github.com/ember-experiences/ember-experiences/actions/workflows/ci.yml/badge.svg)](https://github.com/ember-experiences/ember-experiences/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/ember-experiences)](https://pypi.org/project/ember-experiences/)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
[![Python](https://img.shields.io/pypi/pyversions/ember-experiences)](https://pypi.org/project/ember-experiences/)

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

```bash
pip install ember-experiences
```

```python
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.

```python
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.

```python
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.

```python
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.

```python
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.

```python
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.

```python
# 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:

```python
# 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 |

```python
# 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 |

```python
# 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:

```yaml
# 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
```

```python
ember.load_preset_file("my-preset.yaml")
```

## Configuration

All config values are tunable via `EmberConfig`:

```python
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.

```python
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:**

```yaml
# 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.

```python
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:

```python
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.

```python
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:

```python
ember.register_extractor("food_detector", extract_foods)
```

## Custom Backend Interface

### Storage Backend (4 methods)

```python
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)

```python
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

```bash
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
