Skip to content

Commit b7b28ec

Browse files
committed
docs: polish eros-engine readmes
1 parent 7f63819 commit b7b28ec

2 files changed

Lines changed: 252 additions & 166 deletions

File tree

README.md

Lines changed: 124 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,149 +1,195 @@
11
# eros-engine
22

3-
> **An AI companion that learns who you are — and feels like a real person doing it.**
3+
> **An open-source Rust engine for AI companions with memory, relationship state, and structured user insight.**
44
>
5-
> The same intimacy + memory pipeline that powers [Eros](https://eros.etherfun.xyz)'s dating product, open-sourced. Talk to a persona; the engine quietly builds a structured profile of you for matchmaking and runs a six-dimensional affinity model so the companion behaves like a person, not a chatbot.
5+
> `eros-engine` is the companion-chat core behind [Eros](https://eros.etherfun.xyz), extracted into a standalone service. It turns conversation into three durable signals: a structured user profile, two-layer long-term memory, and a six-dimensional affinity model that changes how a persona behaves over time.
66
77
[![CI](https://github.com/etherfunlab/eros-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/etherfunlab/eros-engine/actions/workflows/ci.yml)
88
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
99

1010
English · [中文](README.zh.md)
1111

12+
## Why this exists
13+
14+
Most AI character apps treat memory as a prompt append and relationship as a paragraph of instructions. That works for a demo, but it drifts in long sessions and is hard to debug.
15+
16+
`eros-engine` moves those concerns into explicit state:
17+
18+
- **Memory** lives in Postgres + pgvector, split into profile memory and relationship memory.
19+
- **Affinity** is a numeric vector updated with EMA smoothing and real-time decay.
20+
- **User insight** is a structured JSONB profile that downstream products can query.
21+
- **Persona behavior** is planned through a rules-based Persona Decision Engine (PDE), then rendered by an LLM.
22+
23+
The result is not a generic agent framework. It is a focused engine for products where a persona talks to the same user across many sessions: AI companions, journaling companions, coaching agents, language tutors, and character chat.
24+
1225
## Key features
1326

14-
- **Memory that grows with the user**Two pgvector layers (cross-session profile + per-session relationship callbacks). The persona still remembers what you said weeks ago without re-stuffing the context window every turn. "She remembers" is structural, not prompt-engineered.
27+
### Two-layer memory
1528

16-
- **Deterministic affinity, not prompt drift** — Closeness between user and persona is a six-dimensional numeric vector mutated every turn, not a paragraph of "you feel warmer toward the user now" baked into a system prompt. Numbers behave; prompt-injected feelings drift over a long session.
29+
`eros-engine` stores memory in two semantic scopes:
1730

18-
- **User profile, structured and queryable** — Each turn quietly mines facts about the user (city, MBTI signals, love values, life rhythm, …) into a JSONB profile with a weighted training level. If you're building a companion, a journaling app, a coaching agent, or anything where actually knowing the user matters, the profile is structured — not a black-box vector — so you can drive whatever logic you want from it.
31+
| Layer | Scope | Purpose |
32+
|---|---|---|
33+
| Profile memory | `user_id`, with `instance_id IS NULL` | Stable user facts shared across sessions and personas. |
34+
| Relationship memory | `user_id + persona instance` | Callbacks, shared moments, unresolved threads, and relationship-specific context. |
1935

20-
## What it does
36+
Embeddings use Voyage `voyage-3-lite` with 512-dimensional vectors. Retrieval runs through pgvector IVFFlat cosine search.
2137

22-
eros-engine is the conversational layer of a dating platform, carved out as a standalone service. Two things happen in every chat turn:
38+
### Six-dimensional affinity
2339

24-
### 1. Profile-building pipeline (`companion_insights`)
40+
Each chat session has a relationship vector:
2541

26-
Every user message is mined for facts about the user: city, occupation, interests, MBTI signals, love values, emotional needs, life rhythm, personality traits, matching preferences. These get merged into a single JSONB profile per user with a weighted **training level** that climbs as more dimensions fill in. The profile is structured the way a matchmaker would think — not a vector blob you can't introspect — so it can drive real matchmaking later.
42+
| Axis | Range | Controls |
43+
|---|---:|---|
44+
| `warmth` | -1.0 to 1.0 | Tone and address, from distant to affectionate. |
45+
| `trust` | 0.0 to 1.0 | Topic depth and willingness to disclose. |
46+
| `intrigue` | 0.0 to 1.0 | Curiosity and follow-up behavior. |
47+
| `intimacy` | 0.0 to 1.0 | Nicknames, inside jokes, and callbacks. |
48+
| `patience` | 0.0 to 1.0 | Tolerance for low-effort or repeated messages. |
49+
| `tension` | 0.0 to 1.0 | Push-pull, friction, and playful resistance. |
2750

28-
A user who chats freely for a few hours produces a richer dating profile than one who fills out a form, because they're answering the questions they didn't know were being asked.
51+
Updates are smoothed with exponential moving average (EMA), so the persona does not jump between emotional states. `intrigue`, `patience`, and `tension` also decay or recover with real time.
2952

30-
### 2. Six-dimensional affinity (the "feels like a real person" part)
53+
Relationship labels such as `stranger`, `slow_burn`, `friend`, `frenemy`, and `romantic` emerge from threshold rules. They are internal state, not user-facing badges.
3154

32-
Most chatbots are stateless. eros-engine isn't. Each chat session carries a six-axis vector that mutates with every turn:
55+
### Deterministic ghost mechanics
3356

34-
| Axis | Range | What it controls |
35-
|------|------|------|
36-
| **warmth** | −1.0 ↔ 1.0 | Tone and address — cold to affectionate |
37-
| **trust** | 0.0 ↔ 1.0 | Topic depth, willingness to disclose |
38-
| **intrigue** | 0.0 ↔ 1.0 | Curiosity, follow-up questions |
39-
| **intimacy** | 0.0 ↔ 1.0 | Inside jokes, nicknames, callbacks |
40-
| **patience** | 0.0 ↔ 1.0 | Threshold for short or low-effort messages |
41-
| **tension** | 0.0 ↔ 1.0 | Push-pull, playful friction |
57+
The same affinity vector drives a deterministic ghost decision. When patience and intrigue drop far enough, the persona can choose not to reply.
4258

43-
Updates use exponential-moving-average smoothing so the persona doesn't lurch, and three axes (intrigue, patience, tension) decay or recover with real time when no one's around. Five relationship labels — `stranger`, `slow_burn`, `friend`, `frenemy`, `romantic` — emerge from the vector by threshold rule, not by being assigned. They're not user-facing; they shape the system prompt the persona generates from.
59+
Four protection rules keep this from feeling arbitrary:
4460

45-
The vector also drives a deterministic **ghost decision** — when patience and intrigue dip past a threshold, the persona simply doesn't reply. With four protection rules layered on top (no ghosting before message 10, no two ghosts in a row, 1-hour cooldown, raised threshold after a recent ghost) it produces the texture of being slightly absent rather than always available. That single mechanic does more for the "talking to a person" feeling than any prompt-engineering trick.
61+
- no ghosting before message 10;
62+
- no two ghosts in a row;
63+
- one-hour cooldown after a ghost;
64+
- a higher threshold after a recent ghost.
4665

47-
### Plus a memory layer
66+
This is implemented as domain logic in Rust, not as a prompt suggestion.
4867

49-
Two pgvector tables hold what the persona remembers about you:
68+
### Structured user insight
5069

51-
- **Profile layer** — cross-session facts (`instance_id IS NULL`), the things any version of any persona could pull up.
52-
- **Relationship layer** — per-session callbacks ("the bookshop you were in that rainy day"), which is what makes someone feel known across weeks of conversation rather than helpfully assistant-shaped.
70+
The `companion_insights` table stores a JSONB profile per user: city, occupation, interests, MBTI signals, relationship values, emotional needs, life rhythm, personality traits, and matching preferences.
5371

54-
Embeddings are 512-dimensional via Voyage's `voyage-3-lite`. Retrieval is cosine over IVFFlat.
72+
Each field contributes to a weighted `training_level`. That makes the profile useful outside the chat loop: matchmaking, onboarding completion, coaching logic, analytics, and product gating can all query structured fields instead of parsing free text.
5573

5674
## Architecture
5775

58-
```
76+
```txt
5977
┌─────────────────────────────────────────────────────────┐
6078
│ /comp/* HTTP routes ← Supabase JWT middleware │
6179
│ │ │
6280
│ ▼ │
63-
pipeline orchestrator: pre → PDE → handler → chat → post
81+
│ pipeline orchestrator: load → PDE → handler → chat → post
6482
│ │ │
6583
│ ┌───────────────────────────────────────┴────────┐ │
66-
│ │ post-process (background, per turn) │ │
67-
│ │ • affinity persist (LLM-evaluated 6-dim Δ) │ │
68-
│ │ • memory (Voyage embed → pgvector upsert) │ │
69-
│ │ • insight (extract facts → companion_insights)
84+
│ │ post-process, spawned after reply │ │
85+
│ │ • affinity: persist 6D delta + EMA │ │
86+
│ │ • memory: Voyage embed → pgvector upsert │ │
87+
│ │ • insight: extract facts → JSONB merge │ │
7088
│ └────────────────────────────────────────────────┘ │
7189
└─────────────────────────────────────────────────────────┘
7290
```
7391

74-
Four crates under `crates/`:
92+
The workspace is split into four crates:
7593

7694
| Crate | Role |
77-
|-------|------|
78-
| `eros-engine-core` | Pure-domain logic — affinity vector math, ghost decision, persona decision engine. Zero I/O. |
79-
| `eros-engine-llm` | OpenRouter chat client + Voyage embedding client + TOML model-config loader. |
80-
| `eros-engine-store` | Postgres + pgvector persistence. All tables namespaced under the `engine` schema. |
81-
| `eros-engine-server` | Axum HTTP service + Supabase JWT middleware + pipeline wiring. |
82-
83-
Embed `core + llm + store` as a library to build your own service, or run `eros-engine-server` as a standalone HTTP API.
84-
85-
Deeper docs:
86-
- [Architecture](docs/architecture.md) — crate boundaries, pipeline phases, data flow
87-
- [Affinity model](docs/affinity-model.md) — 6 dimensions, EMA, time decay, relationship labels
88-
- [Ghost mechanics](docs/ghost-mechanics.md) — score formula + protection rules + worked examples
89-
- [Memory layers](docs/memory-layers.md) — profile vs relationship, Voyage, pgvector retrieval
90-
- [Deploying](docs/deploying.md) — Fly.io, Docker compose, bring-your-own-Postgres / IdP
91-
- [API reference](docs/api-reference.md) — every `/comp/*` endpoint
95+
|---|---|
96+
| `eros-engine-core` | Pure domain logic: affinity math, ghost decision, PDE, persona types. Zero I/O. |
97+
| `eros-engine-llm` | OpenRouter chat client, Voyage embedding client, TOML model-config loader. |
98+
| `eros-engine-store` | Postgres + pgvector persistence, with all tables under the `engine` schema. |
99+
| `eros-engine-server` | Axum HTTP service, Supabase JWT middleware, OpenAPI docs, and pipeline wiring. |
100+
101+
You can run `eros-engine-server` as an HTTP API, or embed `core + llm + store` directly in your own Rust service.
102+
103+
## Documentation
104+
105+
- [Architecture](docs/architecture.md) — crate boundaries, pipeline phases, data flow.
106+
- [Affinity model](docs/affinity-model.md) — six dimensions, EMA, time decay, relationship labels.
107+
- [Ghost mechanics](docs/ghost-mechanics.md) — score formula, protection rules, examples.
108+
- [Memory layers](docs/memory-layers.md) — profile vs relationship memory, Voyage, pgvector retrieval.
109+
- [Deploying](docs/deploying.md) — Fly.io, Docker, bring-your-own Postgres / IdP.
110+
- [API reference](docs/api-reference.md) — every `/comp/*` endpoint.
92111

93112
## Quickstart
94113

114+
Prerequisites:
115+
116+
- Rust toolchain from `rust-toolchain.toml`.
117+
- Postgres 16+ with the `pgvector` extension.
118+
- OpenRouter API key.
119+
- Voyage API key.
120+
- Supabase JWT secret, or your own `AuthValidator` implementation.
121+
95122
```bash
96123
git clone https://github.com/etherfunlab/eros-engine
97124
cd eros-engine
98-
cp .env.example .env # fill in: DATABASE_URL, OPENROUTER_API_KEY,
99-
# VOYAGE_API_KEY, SUPABASE_URL,
100-
# SUPABASE_JWT_SECRET
101-
docker compose -f docker/docker-compose.yml up
125+
cp .env.example .env
102126
```
103127

104-
Engine listens on `:8080`. OpenAPI/Scalar reference at `/docs`. The official hosted web client (Eros Chat) is closed-source — eros-engine itself runs standalone, bring your own UI.
128+
Fill in `DATABASE_URL`, `OPENROUTER_API_KEY`, `VOYAGE_API_KEY`, and `SUPABASE_JWT_SECRET`, then run:
129+
130+
```bash
131+
cargo run -p eros-engine-server -- migrate
132+
cargo run -p eros-engine-server -- seed-personas examples/personas
133+
cargo run -p eros-engine-server -- serve
134+
```
105135

106-
For self-hosters running against an existing Supabase project: tables live under the `engine` Postgres schema, so they coexist cleanly with your other tables.
136+
The server listens on `0.0.0.0:8080` by default. Scalar API docs are available at `/docs`, and the OpenAPI JSON is available at `/api-docs/openapi.json`.
137+
138+
The official Eros Chat web client is closed-source. `eros-engine` is designed to run standalone; bring your own UI or embed the crates in another service.
107139

108140
## API surface
109141

110-
Full reference at `/docs` once running. Highlights:
142+
All `/comp/*` routes require `Authorization: Bearer <Supabase JWT>` by default.
143+
144+
Highlights:
111145

112-
- `POST /comp/chat/start` — open a session against a persona
113-
- `POST /comp/chat/{session_id}/message` — synchronous chat turn
114-
- `GET /comp/chat/{session_id}/history` — paginated history
115-
- `GET /comp/user/{user_id}/profile` — current `companion_insights` + training level
116-
- `GET /comp/affinity/{session_id}` — live 6-dim vector (env-gated for OSS demo; off in prod-flavoured deploys)
146+
- `GET /comp/personas` — list active persona genomes.
147+
- `POST /comp/chat/start` — open a chat session against a persona.
148+
- `POST /comp/chat/{session_id}/message` — synchronous chat turn.
149+
- `POST /comp/chat/{session_id}/message_async` — async chat turn with pending-status polling.
150+
- `GET /comp/chat/{session_id}/pending/{message_id}` — poll async completion.
151+
- `GET /comp/chat/{session_id}/history` — paginated chat history.
152+
- `GET /comp/chat/{user_id}/sessions` — list a user's sessions.
153+
- `GET /comp/user/{user_id}/profile` — current `companion_insights` and `training_level`.
154+
- `POST /comp/chat/{session_id}/event/gift` — apply an out-of-band gift event and affinity delta.
155+
- `GET /comp/chat/{session_id}/gifts` — list gift events for a session.
156+
- `GET /comp/affinity/{session_id}` — debug-only live affinity vector, enabled by `EXPOSE_AFFINITY_DEBUG=true`.
117157

118-
Auth: Bearer Supabase JWT on every `/comp/*` route. The `AuthValidator` trait is pluggable if you bring a different IdP.
158+
The `AuthValidator` trait is pluggable if you use a different identity provider.
119159

120160
## Configuration
121161

122162
| Env var | Required | Notes |
123-
|---------|----------|-------|
124-
| `DATABASE_URL` | yes | Postgres with `pgvector` extension. Engine creates its tables in the `engine` schema. |
125-
| `OPENROUTER_API_KEY` | yes | Chat completions. Routed via `examples/model_config.toml`. |
126-
| `VOYAGE_API_KEY` | yes | Embeddings. Failure modes are loud — empty key fails server boot. |
127-
| `SUPABASE_URL` | yes | Project URL. |
128-
| `SUPABASE_JWT_SECRET` | yes | Project JWT secret. eros-engine validates every incoming token. |
163+
|---|---|---|
164+
| `DATABASE_URL` | yes | Postgres with `pgvector`; tables are created under `engine.*`. |
165+
| `OPENROUTER_API_KEY` | yes | Chat completions, routed by `examples/model_config.toml` unless overridden. |
166+
| `VOYAGE_API_KEY` | yes | Embeddings. Empty keys fail server boot. |
167+
| `SUPABASE_URL` | no | Supabase project URL. Kept in `.env.example` for client/deploy conventions; the server does not read it today. |
168+
| `SUPABASE_JWT_SECRET` | yes | JWT signing secret for default auth. |
169+
| `BIND_ADDR` | no | Defaults to `0.0.0.0:8080`. |
129170
| `EXPOSE_AFFINITY_DEBUG` | no | Set `true` to enable `/comp/affinity/{session_id}`. |
130-
| `EMA_INERTIA` | no | Default `0.8`. |
131-
| `MODEL_CONFIG_PATH` | no | Default `examples/model_config.toml`. |
171+
| `EMA_INERTIA` | no | Defaults to `0.8`. |
172+
| `MODEL_CONFIG_PATH` | no | Defaults to `examples/model_config.toml`. |
173+
| `RUST_LOG` | no | Defaults to `info`. |
174+
175+
## What is deliberately out of scope
176+
177+
This repository is the conversation, memory, and relationship-state core. It does not include:
132178

133-
## What's not here
179+
- **Matchmaking** — multi-stage filtering, soft scoring, and agent-to-agent matching simulation remain in the closed-source product.
180+
- **Full social UX** — onboarding, video, voice, billing, photos, moderation UI, and mobile clients.
181+
- **Persona provenance / marketplace logic** — commercial product code, not part of the engine.
134182

135-
This repo is the conversational + intimacy core. Things deliberately out of scope:
183+
If you are building a different product, the reusable part is the affinity + memory + insight pipeline.
136184

137-
- **Match-making algorithm** — the multi-stage filter + soft scoring + LLM agent-to-agent simulation lives in the closed-source product. eros-engine builds the *profiles* that feed it, but doesn't pair people up.
138-
- **Full social product UX** — onboarding, video, voice, billing, photos.
139-
- **Companion provenance / lineage** — proprietary.
185+
## Content note
140186

141-
If you want to build a different product on top — a journaling companion, a language tutor, a coaching agent — the affinity + memory + insight pipeline is the part you'd reuse.
187+
The example personas under `examples/personas/` are written as adult character-chat examples. They can flirt and express desire when the relationship state reaches that point, while still refusing disrespectful or boundary-crossing behavior. If your product needs a SFW default, replace those persona files before deploying.
142188

143189
## Contributing
144190

145-
Read [`CONTRIBUTING.md`](CONTRIBUTING.md). All contributors must accept the [`CLA`](CLA.md) via cla-assistant.io on first PR (one-time, covers all future PRs).
191+
Read [`CONTRIBUTING.md`](CONTRIBUTING.md). All contributors must accept the [`CLA`](CLA.md) through cla-assistant.io on their first PR.
146192

147193
## License
148194

149-
AGPL-3.0. If AGPL doesn't fit your distribution model, commercial licensing is available `henrylin@etherfun.xyz`.
195+
`eros-engine` is licensed under AGPL-3.0-only. If AGPL does not fit your distribution model, commercial licensing is available: `henrylin@etherfun.xyz`.

0 commit comments

Comments
 (0)