chore: initialize EverOS 1.0.0
md-first memory extraction framework for AI agents. Markdown is the single source of truth; SQLite holds state and LanceDB provides the rebuildable vector + BM25 + scalar index. The codebase follows a single-direction DDD layering (entrypoints -> service -> memory -> infra, with component / core / config cross-cutting) enforced by import-linter. Engineering surface: - Coding conventions in .claude/rules/ (path-scoped) and workflows in .claude/skills/ (/commit, /new-branch, /pr). - GitHub Actions CI runs make lint + test + integration; pre-commit mirrors the gates locally (ruff, hygiene hooks, gitlint commit-msg). - Commit messages follow Conventional Commits, enforced by gitlint. - make lint also enforces datetime two-zone discipline and OpenAPI drift.
This commit is contained in:
28
use-cases/openher/.env.example
Normal file
28
use-cases/openher/.env.example
Normal file
@ -0,0 +1,28 @@
|
||||
# OpenHer × EverCore Use Case
|
||||
# Copy this file to .env and fill in your values
|
||||
|
||||
# ─── LLM Provider (pick one) ───
|
||||
DEFAULT_PROVIDER=gemini
|
||||
DEFAULT_MODEL=gemini-3.1-flash-lite-preview
|
||||
|
||||
# Gemini
|
||||
GEMINI_API_KEY=your_gemini_api_key_here
|
||||
|
||||
# Claude (alternative)
|
||||
# ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||
|
||||
# Qwen (alternative)
|
||||
# DASHSCOPE_API_KEY=your_dashscope_api_key_here
|
||||
|
||||
# OpenAI (alternative)
|
||||
# OPENAI_API_KEY=your_openai_api_key_here
|
||||
|
||||
# ─── EverCore Long-Term Memory ───
|
||||
|
||||
# Option A: EverCore Cloud
|
||||
EVERMEMOS_BASE_URL=https://api.evermind.ai/v1
|
||||
EVERMEMOS_API_KEY=your_evermemos_api_key_here
|
||||
|
||||
# Option B: Self-Hosted EverCore
|
||||
# cd vendor/EverCore && docker compose up -d && uv run python src/run.py
|
||||
# EVERMEMOS_BASE_URL=http://localhost:1995/api/v1
|
||||
285
use-cases/openher/README.md
Normal file
285
use-cases/openher/README.md
Normal file
@ -0,0 +1,285 @@
|
||||
# OpenHer — Teaching AI to Remember Who You Are
|
||||
|
||||
Built on [EverCore](https://github.com/EverMind-AI/EverOS/tree/main/methods/EverCore) — Open-source AI memory infrastructure
|
||||
|
||||
**OpenHer** doesn't build chatbots. It doesn't build AI assistants. It builds **AI Beings** — entities with personality, emotion, and memory that *feel*, *remember*, and *grow* through every interaction.
|
||||
|
||||
**EverCore** is her long-term memory — the part that lets her carry your story across sessions, remember who you are, what you've talked about, and how your relationship has evolved.
|
||||
|
||||
Full Project: [github.com/kellyvv/OpenHer](https://github.com/kellyvv/OpenHer)
|
||||
|
||||
---
|
||||
|
||||
## Why Does She Need Memory?
|
||||
|
||||
Without memory, every conversation starts from zero. She doesn't know your name. She doesn't remember that three weeks ago you mentioned you drink your coffee black. She doesn't know you once had a fight and made up.
|
||||
|
||||
With EverCore:
|
||||
|
||||
**She remembers what you said.**
|
||||
Three weeks ago you casually mentioned no sugar in your coffee. Today she says: "Americano, no sugar, right?"
|
||||
|
||||
**She gets to know you.**
|
||||
The more you talk, the better she understands you. The her after one month is not the same her as day one.
|
||||
|
||||
**She has foresight.**
|
||||
Last time you mentioned work stress. This time she asks: "How's that project going?"
|
||||
|
||||
> *She doesn't "look up" your information — she naturally recalls it.*
|
||||
|
||||
---
|
||||
|
||||
## Memory Architecture
|
||||
|
||||
OpenHer's memory has three layers. EverCore powers the deepest one:
|
||||
|
||||
| Layer | What it does | Analogy |
|
||||
|:------|:-------------|:--------|
|
||||
| **Style Memory** | Her behavioral habits — tone, expression patterns | Muscle memory |
|
||||
| **Local Facts** | Your preferences, personal info | Short-term memory |
|
||||
| **Long-Term Memory** | What happened between you, her understanding of you, her hunches | **Episodic memory (EverCore)** |
|
||||
|
||||
---
|
||||
|
||||
## How Memory Feeds Into Personality
|
||||
|
||||
OpenHer's core is a living neural network (25D input, 24D hidden, 8D behavioral signals). EverCore provides 4 key dimensions that let her tell the difference between a stranger and an old friend:
|
||||
|
||||
```
|
||||
Relationship Depth 0 ─────────────────── 1
|
||||
Stranger Old friend
|
||||
|
||||
Emotional Valence -1 ─────────────────── 1
|
||||
Rocky history Warm history
|
||||
|
||||
Trust Level 0 ─────────────────── 1
|
||||
First meeting Deep trust
|
||||
|
||||
Pending Foresight 0 ─────────────────── 1
|
||||
Nothing unresolved Something on her mind
|
||||
```
|
||||
|
||||
New users start at all zeros — a stranger. As conversations accumulate, these values grow naturally. The same conversation context produces completely different behavioral signals for strangers vs. old friends:
|
||||
|
||||
- With an old friend: warmer, more initiative, more willing to be vulnerable
|
||||
- With a stranger: more reserved, more polite, keeps distance
|
||||
|
||||
This isn't a rule written in a prompt — it's emergent behavior computed by the neural network from the relationship vector.
|
||||
|
||||
---
|
||||
|
||||
## How She "Remembers"
|
||||
|
||||
Memory retrieval is async and two-stage — she never freezes up trying to recall:
|
||||
|
||||
```
|
||||
Turn 1: You say "I love hiking"
|
||||
\-- After you finish, background search for related memories fires
|
||||
|
||||
Turn 2: You say "What about this weekend?"
|
||||
\-- Last turn's search results come back
|
||||
Found: "User mentioned liking weekend hikes 3 weeks ago"
|
||||
Naturally woven in: "The mountains should be nice this weekend"
|
||||
\-- Simultaneously searching for "weekend plans" memories
|
||||
|
||||
Turn 3: ...continues...
|
||||
```
|
||||
|
||||
If the search takes too long (>500ms), she doesn't stall — she keeps talking from what she already knows, like a person who can't quite place something but doesn't stop mid-sentence.
|
||||
|
||||
---
|
||||
|
||||
## What Happens Each Turn
|
||||
|
||||
```
|
||||
User sends a message
|
||||
|
|
||||
v
|
||||
Load memory -- First turn: load "who you are", "what we talked about",
|
||||
| "what's on her mind" from EverCore
|
||||
v
|
||||
Perceive -- LLM evaluates the current moment: your emotion, topic
|
||||
| intimacy, conflict level... (8 dimensions)
|
||||
| + relationship dimensions from EverCore (4 dimensions) = 12D
|
||||
v
|
||||
Relationship evolves -- Blend EverCore history with this turn's changes
|
||||
| Smoothed so a single remark can't flip the relationship
|
||||
v
|
||||
Neural network -- 25D input (drives + context + relationship + internal state)
|
||||
| 24D hidden layer, 8D behavioral signals
|
||||
| Decides how direct, warm, stubborn, curious she is right now
|
||||
v
|
||||
Recall -- Collect relevant memories found by last turn's search
|
||||
| Blend into the response prompt
|
||||
v
|
||||
Respond -- Internal monologue first, then choose what to say and how
|
||||
|
|
||||
v
|
||||
Remember this turn -- Store the conversation in EverCore (async, non-blocking)
|
||||
|
|
||||
v
|
||||
Prepare for next -- Search for memories related to what you just said
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
- **Emergent Personality** — Not written in a prompt. Emerges from random neural networks, 5D drives, and Hebbian learning
|
||||
- **Emotional Thermodynamics** — Drives metabolize over real time. She gets lonely when you're away, irritated when ignored
|
||||
- **Feel First** — Every response starts with an internal monologue before choosing words
|
||||
- **Cross-Session Memory** — EverCore stores your shared story across every conversation
|
||||
- **Relationship Evolution** — The relationship vector deepens naturally with each turn
|
||||
- **Proactive Messages** — She reaches out not on a timer, but because her connection hunger is rising
|
||||
- **Modal Expression** — She chooses text, voice, or photos based on what the moment calls for
|
||||
- **10 Pre-built Personas** — Each with unique MBTI, drive baselines, and neural network seeds
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|:------|:-----------|
|
||||
| Runtime | Python 3.11+, FastAPI, WebSocket, asyncio |
|
||||
| LLM | Gemini, Claude, Qwen3, GPT-5.4-mini, MiniMax, Moonshot, StepFun, Ollama |
|
||||
| Memory | **EverCore** (self-hosted / cloud) + SQLite local state |
|
||||
| Desktop | SwiftUI (macOS native) |
|
||||
| Voice | DashScope, OpenAI, MiniMax |
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.11+
|
||||
- Any supported LLM provider API key
|
||||
- EverCore (self-hosted or cloud)
|
||||
|
||||
### 1. Clone & Install
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kellyvv/OpenHer.git
|
||||
cd OpenHer
|
||||
bash setup.sh
|
||||
```
|
||||
|
||||
### 2. Configure
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
```bash
|
||||
# LLM (pick one)
|
||||
DEFAULT_PROVIDER=gemini
|
||||
DEFAULT_MODEL=gemini-3.1-flash-lite-preview
|
||||
GEMINI_API_KEY=your_key
|
||||
|
||||
# EverCore — Cloud
|
||||
EVERMEMOS_BASE_URL=https://api.evermind.ai/v1
|
||||
EVERMEMOS_API_KEY=your_key
|
||||
|
||||
# EverCore — Self-hosted
|
||||
# cd vendor/EverCore && docker compose up -d && uv run python src/run.py
|
||||
# EVERMEMOS_BASE_URL=http://localhost:1995/api/v1
|
||||
```
|
||||
|
||||
### 3. Start
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
# GenomeEngine loaded, 10 personas available
|
||||
```
|
||||
|
||||
### 4. Try the Demo
|
||||
|
||||
```bash
|
||||
python demo/evermemos_demo.py
|
||||
# Runs in simulation mode even without EverCore
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
OpenHer/
|
||||
├── agent/
|
||||
│ ├── chat_agent.py # Main agent, full lifecycle
|
||||
│ ├── evermemos_mixin.py # EverCore integration (load/store/search/EMA)
|
||||
│ └── prompt_builder.py # Memory injection into Actor prompt
|
||||
├── engine/
|
||||
│ └── genome/
|
||||
│ ├── genome_engine.py # Neural network + 12D context (incl. 4D EverCore)
|
||||
│ ├── critic.py # LLM perception: 8D context + relationship deltas
|
||||
│ ├── drive_metabolism.py # Emotional thermodynamics
|
||||
│ └── style_memory.py # KNN behavioral memory + Hawking radiation decay
|
||||
├── memory/
|
||||
│ ├── memory_store.py # SQLite FTS5 local memory
|
||||
│ └── types.py # Memory & SessionContext types
|
||||
├── persona/
|
||||
│ └── personas/ # 10 pre-built personas (SOUL.md + seeds)
|
||||
├── vendor/
|
||||
│ └── EverCore/ # Self-hosted EverCore
|
||||
└── main.py # FastAPI server
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Code at a Glance
|
||||
|
||||
### EverCore Mixin
|
||||
|
||||
The core integration is a mixin class handling four async operations:
|
||||
|
||||
```python
|
||||
class EverMemosMixin:
|
||||
async def _evermemos_gather(self):
|
||||
"""Load session context (first turn): who you are,
|
||||
what we talked about, what's on her mind"""
|
||||
|
||||
def _apply_relationship_ema(self, prior, delta, depth):
|
||||
"""Relationship evolution: blend history with this turn's changes"""
|
||||
|
||||
def _evermemos_store_bg(self, user_message, reply):
|
||||
"""Remember this turn (async background, never blocks)"""
|
||||
|
||||
def _evermemos_search_bg(self, user_message):
|
||||
"""Search related memories (preparing for next turn)"""
|
||||
```
|
||||
|
||||
### SessionContext — Everything She Knows About You
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class SessionContext:
|
||||
user_profile: str = "" # Who you are
|
||||
episode_summary: str = "" # What happened between you
|
||||
foresight_text: str = "" # What's on her mind
|
||||
relationship_depth: float = 0.0 # Stranger to old friend
|
||||
emotional_valence: float = 0.0 # Rocky history to warm history
|
||||
trust_level: float = 0.0 # First meeting to deep trust
|
||||
has_history: bool = False # Has she met you before?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Without Memory vs. With Memory
|
||||
|
||||
| | Without EverCore | With EverCore |
|
||||
|:--|:--|:--|
|
||||
| First meeting | "Hi! I'm Luna" | "Hi! I'm Luna" |
|
||||
| Second meeting | "Hi! I'm Luna" | "Hey Alex! How's that project going?" |
|
||||
| You say you're tired | "Get some rest!" | "Working late again? You said that last time too... want me to order you an Americano? No sugar." |
|
||||
|
||||
> *Three weeks ago you casually mentioned no sugar in your coffee. Today: "Americano, no sugar, right?"*
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
|
||||
- Full Project: [github.com/kellyvv/OpenHer](https://github.com/kellyvv/OpenHer)
|
||||
- EverCore: [evermind.ai](https://evermind.ai)
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
362
use-cases/openher/demo/evermemos_demo.py
Normal file
362
use-cases/openher/demo/evermemos_demo.py
Normal file
@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OpenHer × EverCore Integration Demo
|
||||
|
||||
Demonstrates how EverCore provides long-term memory to the
|
||||
AI Being persona engine. Shows session context loading, memory
|
||||
storage, search, and relationship vector evolution.
|
||||
|
||||
Usage:
|
||||
# With EverCore Cloud
|
||||
export EVERMEMOS_BASE_URL=https://api.evermind.ai/v1
|
||||
export EVERMEMOS_API_KEY=your_key
|
||||
python demo/evermemos_demo.py
|
||||
|
||||
# With self-hosted EverCore
|
||||
export EVERMEMOS_BASE_URL=http://localhost:1995/api/v1
|
||||
python demo/evermemos_demo.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# EverCore Client (minimal standalone version)
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
try:
|
||||
import httpx
|
||||
except ImportError:
|
||||
print("❌ httpx not installed. Run: pip install httpx")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class EverCoreClient:
|
||||
"""Minimal EverCore client for demo purposes."""
|
||||
|
||||
def __init__(self, base_url: str, api_key: str = ""):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.api_key = api_key
|
||||
self._client = httpx.AsyncClient(timeout=10.0)
|
||||
self.available = bool(base_url)
|
||||
|
||||
async def _headers(self) -> dict:
|
||||
h = {"Content-Type": "application/json"}
|
||||
if self.api_key:
|
||||
h["Authorization"] = f"Bearer {self.api_key}"
|
||||
return h
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""Check if EverCore is reachable."""
|
||||
try:
|
||||
# Try the health endpoint (remove /api/v1 suffix)
|
||||
health_url = self.base_url.replace("/api/v1", "") + "/health"
|
||||
resp = await self._client.get(health_url, headers=await self._headers())
|
||||
return resp.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def store_turn(
|
||||
self,
|
||||
user_id: str,
|
||||
persona_id: str,
|
||||
persona_name: str,
|
||||
user_name: str,
|
||||
group_id: str,
|
||||
user_message: str,
|
||||
agent_reply: str,
|
||||
) -> dict:
|
||||
"""Store a conversation turn as memory."""
|
||||
now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S+00:00")
|
||||
messages = [
|
||||
{
|
||||
"message_id": f"msg_{hash(user_message) & 0xFFFF:04x}_u",
|
||||
"create_time": now,
|
||||
"sender": user_id,
|
||||
"sender_name": user_name,
|
||||
"content": user_message,
|
||||
},
|
||||
{
|
||||
"message_id": f"msg_{hash(agent_reply) & 0xFFFF:04x}_a",
|
||||
"create_time": now,
|
||||
"sender": persona_id,
|
||||
"sender_name": persona_name,
|
||||
"content": agent_reply,
|
||||
},
|
||||
]
|
||||
resp = await self._client.post(
|
||||
f"{self.base_url}/memories",
|
||||
json={"messages": messages, "group_id": group_id},
|
||||
headers=await self._headers(),
|
||||
)
|
||||
return resp.json() if resp.status_code == 200 else {"error": resp.text}
|
||||
|
||||
async def search(
|
||||
self,
|
||||
query: str,
|
||||
user_id: str,
|
||||
group_id: str,
|
||||
top_k: int = 5,
|
||||
) -> dict:
|
||||
"""Search for relevant memories."""
|
||||
resp = await self._client.get(
|
||||
f"{self.base_url}/memories/search",
|
||||
params={
|
||||
"query": query,
|
||||
"user_id": user_id,
|
||||
"group_id": group_id,
|
||||
"top_k": top_k,
|
||||
"retrieve_method": "hybrid",
|
||||
},
|
||||
headers=await self._headers(),
|
||||
)
|
||||
return resp.json() if resp.status_code == 200 else {"error": resp.text}
|
||||
|
||||
async def get_user_profile(self, user_id: str) -> dict:
|
||||
"""Get user profile (accumulated from conversations)."""
|
||||
resp = await self._client.get(
|
||||
f"{self.base_url}/users/{user_id}/profile",
|
||||
headers=await self._headers(),
|
||||
)
|
||||
return resp.json() if resp.status_code == 200 else {}
|
||||
|
||||
async def close(self):
|
||||
await self._client.aclose()
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Relationship Vector (from EverCore session)
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
def compute_relationship_vector(profile_data: dict) -> dict:
|
||||
"""
|
||||
Extract 4D relationship vector from EverCore profile data.
|
||||
|
||||
These 4 dimensions expand the persona engine's neural network
|
||||
from 8D to 12D input, allowing it to differentiate behavior
|
||||
between strangers and old friends.
|
||||
"""
|
||||
return {
|
||||
"relationship_depth": min(1.0, profile_data.get("interaction_count", 0) / 50),
|
||||
"emotional_valence": profile_data.get("sentiment_avg", 0.0),
|
||||
"trust_level": min(1.0, profile_data.get("trust_score", 0.0)),
|
||||
"pending_foresight": 1.0 if profile_data.get("foresight") else 0.0,
|
||||
}
|
||||
|
||||
|
||||
def apply_relationship_ema(
|
||||
prior: dict,
|
||||
delta: dict,
|
||||
conversation_depth: float,
|
||||
prev_ema: Optional[dict] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Semi-emergent relationship update (Step 2.5 of ChatAgent lifecycle).
|
||||
|
||||
Blends EverCore prior with LLM-judged delta through EMA:
|
||||
- alpha modulated by conversation depth (deeper = trust LLM more)
|
||||
- Clips to valid ranges
|
||||
- Preserves momentum through prev_ema
|
||||
"""
|
||||
if prev_ema is None:
|
||||
prev_ema = dict(prior)
|
||||
|
||||
alpha = max(0.15, min(0.65, 0.15 + 0.5 * conversation_depth))
|
||||
|
||||
ema = {}
|
||||
for k in prior:
|
||||
lo = -1.0 if k == "emotional_valence" else 0.0
|
||||
posterior = max(lo, min(1.0, prior[k] + delta.get(k, 0.0)))
|
||||
prev = prev_ema.get(k, prior[k])
|
||||
ema[k] = round(alpha * posterior + (1 - alpha) * prev, 4)
|
||||
|
||||
return ema
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Demo
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
async def main():
|
||||
base_url = os.getenv("EVERMEMOS_BASE_URL", "")
|
||||
api_key = os.getenv("EVERMEMOS_API_KEY", "")
|
||||
|
||||
if not base_url:
|
||||
print("=" * 60)
|
||||
print("OpenHer × EverCore Integration Demo")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("⚠️ EVERMEMOS_BASE_URL not set.")
|
||||
print()
|
||||
print("To run this demo, set up EverCore:")
|
||||
print()
|
||||
print(" Option A — Cloud:")
|
||||
print(" export EVERMEMOS_BASE_URL=https://api.evermind.ai/v1")
|
||||
print(" export EVERMEMOS_API_KEY=your_key")
|
||||
print()
|
||||
print(" Option B — Self-hosted:")
|
||||
print(" cd vendor/EverCore && docker compose up -d")
|
||||
print(" uv run python src/run.py")
|
||||
print(" export EVERMEMOS_BASE_URL=http://localhost:1995/api/v1")
|
||||
print()
|
||||
print("Get your API key: https://console.evermind.ai/")
|
||||
print()
|
||||
print("Running in simulation mode...\n")
|
||||
await demo_simulation()
|
||||
return
|
||||
|
||||
client = EverCoreClient(base_url, api_key)
|
||||
|
||||
print("=" * 60)
|
||||
print("OpenHer × EverCore Integration Demo")
|
||||
print("=" * 60)
|
||||
print(f"\n📡 EverCore: {base_url}")
|
||||
|
||||
# Health check
|
||||
healthy = await client.health_check()
|
||||
if not healthy:
|
||||
print("❌ EverCore is not reachable. Check your URL and try again.")
|
||||
await client.close()
|
||||
return
|
||||
print("✅ EverCore is healthy\n")
|
||||
|
||||
# ── Demo conversation ──
|
||||
user_id = "demo_user"
|
||||
persona_id = "luna"
|
||||
persona_name = "Luna (陆暖)"
|
||||
user_name = "Demo User"
|
||||
group_id = f"{persona_id}__{user_id}"
|
||||
|
||||
conversations = [
|
||||
("My name is Alex, I'm a software engineer", "Nice to meet you Alex! What kind of software do you work on?"),
|
||||
("I love hiking in the mountains on weekends", "That sounds wonderful! There's something about being up high that makes everything else feel small."),
|
||||
("I drink my coffee black, no sugar", "Noted! A purist. I respect that."),
|
||||
]
|
||||
|
||||
print("📝 Storing conversation memories...\n")
|
||||
for user_msg, agent_reply in conversations:
|
||||
result = await client.store_turn(
|
||||
user_id=user_id,
|
||||
persona_id=persona_id,
|
||||
persona_name=persona_name,
|
||||
user_name=user_name,
|
||||
group_id=group_id,
|
||||
user_message=user_msg,
|
||||
agent_reply=agent_reply,
|
||||
)
|
||||
status = "✅" if "error" not in result else "❌"
|
||||
print(f" {status} User: \"{user_msg[:50]}...\"")
|
||||
|
||||
# Wait for indexing
|
||||
print("\n⏳ Waiting for memory indexing (3s)...")
|
||||
await asyncio.sleep(3)
|
||||
|
||||
# Search
|
||||
print("\n🔍 Searching for relevant memories...\n")
|
||||
queries = [
|
||||
"What does Alex like to do on weekends?",
|
||||
"How does Alex take their coffee?",
|
||||
"What is Alex's occupation?",
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
result = await client.search(
|
||||
query=query,
|
||||
user_id=user_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
memories = result.get("result", {}).get("memories", [])
|
||||
print(f" Q: \"{query}\"")
|
||||
if memories:
|
||||
for mem in memories[:2]:
|
||||
content = str(mem)[:100]
|
||||
print(f" → {content}")
|
||||
else:
|
||||
print(" → (no results yet — indexing may still be in progress)")
|
||||
print()
|
||||
|
||||
# Relationship vector
|
||||
print("📊 Relationship Vector Evolution:\n")
|
||||
prior = {"relationship_depth": 0.0, "emotional_valence": 0.0, "trust_level": 0.0, "pending_foresight": 0.0}
|
||||
deltas = [
|
||||
{"relationship_depth": 0.1, "emotional_valence": 0.2, "trust_level": 0.05},
|
||||
{"relationship_depth": 0.05, "emotional_valence": 0.1, "trust_level": 0.1},
|
||||
{"relationship_depth": 0.08, "emotional_valence": 0.15, "trust_level": 0.12},
|
||||
]
|
||||
|
||||
ema = None
|
||||
for i, delta in enumerate(deltas):
|
||||
ema = apply_relationship_ema(prior, delta, conversation_depth=0.2 * (i + 1), prev_ema=ema)
|
||||
print(f" Turn {i+1}: depth={ema['relationship_depth']:.3f} "
|
||||
f"valence={ema['emotional_valence']:.3f} "
|
||||
f"trust={ema['trust_level']:.3f}")
|
||||
prior = ema
|
||||
|
||||
print(f"\n → After 3 turns: no longer a stranger (depth={ema['relationship_depth']:.3f})")
|
||||
print(f" → Neural network now produces warmer, more familiar behavioral signals\n")
|
||||
|
||||
await client.close()
|
||||
print("✅ Demo complete!")
|
||||
|
||||
|
||||
async def demo_simulation():
|
||||
"""Run demo in simulation mode (no EverCore connection)."""
|
||||
print("📊 Simulating Relationship Vector Evolution:\n")
|
||||
print(" This shows how the 4D EverCore relationship vector")
|
||||
print(" deepens over multiple conversation turns.\n")
|
||||
|
||||
prior = {"relationship_depth": 0.0, "emotional_valence": 0.0, "trust_level": 0.0, "pending_foresight": 0.0}
|
||||
|
||||
# Simulate 10 turns of conversation
|
||||
simulated_deltas = [
|
||||
(0.3, {"relationship_depth": 0.10, "emotional_valence": 0.15, "trust_level": 0.05}),
|
||||
(0.4, {"relationship_depth": 0.08, "emotional_valence": 0.10, "trust_level": 0.08}),
|
||||
(0.5, {"relationship_depth": 0.05, "emotional_valence": 0.20, "trust_level": 0.12}),
|
||||
(0.6, {"relationship_depth": 0.06, "emotional_valence": -0.10, "trust_level": 0.03}),
|
||||
(0.7, {"relationship_depth": 0.04, "emotional_valence": 0.08, "trust_level": 0.10}),
|
||||
(0.7, {"relationship_depth": 0.03, "emotional_valence": 0.12, "trust_level": 0.08}),
|
||||
(0.8, {"relationship_depth": 0.02, "emotional_valence": 0.05, "trust_level": 0.06}),
|
||||
(0.8, {"relationship_depth": 0.03, "emotional_valence": 0.10, "trust_level": 0.05}),
|
||||
(0.9, {"relationship_depth": 0.01, "emotional_valence": 0.08, "trust_level": 0.04}),
|
||||
(0.9, {"relationship_depth": 0.02, "emotional_valence": 0.06, "trust_level": 0.03}),
|
||||
]
|
||||
|
||||
ema = None
|
||||
for i, (depth, delta) in enumerate(simulated_deltas, 1):
|
||||
alpha = max(0.15, min(0.65, 0.15 + 0.5 * depth))
|
||||
ema = apply_relationship_ema(prior, delta, conversation_depth=depth, prev_ema=ema)
|
||||
bar_d = "█" * int(ema["relationship_depth"] * 20)
|
||||
bar_v = "█" * int(max(0, ema["emotional_valence"]) * 20)
|
||||
bar_t = "█" * int(ema["trust_level"] * 20)
|
||||
print(f" Turn {i:2d} (α={alpha:.2f}): "
|
||||
f"depth={ema['relationship_depth']:.3f} {bar_d}")
|
||||
print(f" "
|
||||
f"valence={ema['emotional_valence']:+.3f} {bar_v}")
|
||||
print(f" "
|
||||
f"trust={ema['trust_level']:.3f} {bar_t}")
|
||||
print()
|
||||
prior = ema
|
||||
|
||||
print(" ──────────────────────────────────")
|
||||
print(f" Final state: depth={ema['relationship_depth']:.3f}, "
|
||||
f"valence={ema['emotional_valence']:+.3f}, "
|
||||
f"trust={ema['trust_level']:.3f}")
|
||||
print()
|
||||
print(" Turn 4 shows a negative emotional event (valence delta = -0.10),")
|
||||
print(" but the EMA smoothing prevents overreaction — the relationship")
|
||||
print(" continues to deepen because trust was already building.")
|
||||
print()
|
||||
print(" This vector feeds into the 25D neural network input,")
|
||||
print(" producing different behavioral signals for strangers vs. friends:")
|
||||
print(" - Higher warmth, vulnerability, and initiative for trusted users")
|
||||
print(" - More guarded, formal signals for new users")
|
||||
print()
|
||||
print("✅ Simulation complete!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
66
use-cases/openher/integration/context_features.py
Normal file
66
use-cases/openher/integration/context_features.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""
|
||||
Neural network context features — showing how EverCore expands
|
||||
the persona engine's perception from 8D to 12D.
|
||||
|
||||
The 4 additional relationship dimensions from EverCore allow the
|
||||
neural network to produce different behavioral signals depending
|
||||
on the history between user and persona.
|
||||
|
||||
Full source: https://github.com/kellyvv/OpenHer/blob/main/engine/genome/genome_engine.py
|
||||
"""
|
||||
|
||||
# ══════════════════════════════════════════════
|
||||
# 5D Drive System (internal motivation)
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
DRIVES = ['connection', 'novelty', 'expression', 'safety', 'play']
|
||||
|
||||
# ══════════════════════════════════════════════
|
||||
# 8D Behavioral Signals (neural network output)
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
SIGNALS = [
|
||||
'directness', # 0=indirect hints → 1=straight talk
|
||||
'vulnerability', # 0=guarded → 1=emotionally open
|
||||
'playfulness', # 0=serious → 1=playful/teasing
|
||||
'initiative', # 0=reactive → 1=proactive leading
|
||||
'depth', # 0=small talk → 1=deep conversation
|
||||
'warmth', # 0=cold/distant → 1=warm/caring
|
||||
'defiance', # 0=compliant → 1=rebellious/stubborn
|
||||
'curiosity', # 0=indifferent → 1=intensely curious
|
||||
]
|
||||
|
||||
# ══════════════════════════════════════════════
|
||||
# 12D Context Features (neural network input)
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
CONTEXT_FEATURES = [
|
||||
# ── 8D from Critic LLM (per-turn perception) ──
|
||||
'user_emotion', # -1=negative → 1=positive
|
||||
'topic_intimacy', # 0=professional → 1=intimate
|
||||
'time_of_day', # 0=morning → 1=late night
|
||||
'conversation_depth', # 0=just started → 1=deep conversation
|
||||
'user_engagement', # 0=dismissive → 1=invested
|
||||
'conflict_level', # 0=harmonious → 1=conflict
|
||||
'novelty_level', # 0=routine topic → 1=novel topic
|
||||
'user_vulnerability', # 0=guarded → 1=open
|
||||
|
||||
# ── 4D from EverCore (cross-session relationship) ──
|
||||
'relationship_depth', # 0=stranger → 1=old friend
|
||||
'emotional_valence', # -1=negative history → 1=positive history
|
||||
'trust_level', # 0=no trust → 1=deep trust
|
||||
'pending_foresight', # 0=nothing pending → 1=unresolved concern
|
||||
]
|
||||
|
||||
# Neural network dimensions
|
||||
N_DRIVES = len(DRIVES) # 5
|
||||
N_CONTEXT = len(CONTEXT_FEATURES) # 12 (8 + 4 from EverCore)
|
||||
N_SIGNALS = len(SIGNALS) # 8
|
||||
RECURRENT_SIZE = 8 # Internal "mood" state
|
||||
INPUT_SIZE = N_DRIVES + N_CONTEXT + RECURRENT_SIZE # 5 + 12 + 8 = 25
|
||||
HIDDEN_SIZE = 24
|
||||
|
||||
# Architecture: 25D input → 24D hidden (tanh) → 8D output (sigmoid)
|
||||
# The 4 EverCore dimensions mean the same neural network produces
|
||||
# DIFFERENT behavioral signals for strangers vs. old friends,
|
||||
# even with identical conversation context.
|
||||
193
use-cases/openher/integration/evermemos_mixin.py
Normal file
193
use-cases/openher/integration/evermemos_mixin.py
Normal file
@ -0,0 +1,193 @@
|
||||
"""
|
||||
EverMemosMixin — EverCore integration for ChatAgent.
|
||||
|
||||
This mixin handles all async memory operations in the ChatAgent lifecycle:
|
||||
Step 0: Session context loading (first turn)
|
||||
Step 2.5: Relationship EMA (blend EverCore prior + LLM delta)
|
||||
Step 8.5: Collect async search results
|
||||
Step 11: Fire-and-forget turn storage
|
||||
Step 12: Async prefetch for next turn
|
||||
|
||||
The mixin pattern keeps EverCore concerns cleanly separated from the
|
||||
core persona engine (drives, metabolism, neural network, style memory).
|
||||
|
||||
Full source: https://github.com/kellyvv/OpenHer/blob/main/agent/evermemos_mixin.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
class EverMemosMixin:
|
||||
"""EverCore async memory integration methods."""
|
||||
|
||||
async def _evermemos_gather(self) -> dict:
|
||||
"""
|
||||
Step 0: Load EverCore session context (first turn only).
|
||||
Subsequent turns reuse cached _session_ctx.
|
||||
Returns relationship_4d dict for GenomeEngine context.
|
||||
"""
|
||||
empty_4d = {
|
||||
'relationship_depth': 0.0,
|
||||
'emotional_valence': 0.0,
|
||||
'trust_level': 0.0,
|
||||
'pending_foresight': 0.0,
|
||||
}
|
||||
|
||||
if not (self.evermemos and self.evermemos.available):
|
||||
return empty_4d
|
||||
|
||||
# Load once per session
|
||||
if self._turn_count == 1:
|
||||
self._session_ctx = await self.evermemos.load_session_context(
|
||||
user_id=self.evermemos_uid,
|
||||
persona_id=self.persona.persona_id,
|
||||
group_id=self._group_id,
|
||||
)
|
||||
if self._session_ctx.user_profile:
|
||||
self._user_profile = self._session_ctx.user_profile
|
||||
if self._session_ctx.episode_summary:
|
||||
self._episode_summary = self._session_ctx.episode_summary
|
||||
# Cache foresight text for Actor injection
|
||||
if self._session_ctx.foresight_text:
|
||||
self._foresight_text = self._session_ctx.foresight_text
|
||||
|
||||
if not self._session_ctx:
|
||||
return empty_4d
|
||||
|
||||
return self.evermemos.relationship_vector(self._session_ctx)
|
||||
|
||||
def _apply_relationship_ema(
|
||||
self,
|
||||
prior: dict,
|
||||
rel_delta: dict,
|
||||
conversation_depth: float,
|
||||
) -> dict:
|
||||
"""
|
||||
Step 2.5: Semi-emergent relationship update.
|
||||
|
||||
Pattern: posterior = clip(prior + LLM_delta) → EMA smooth
|
||||
alpha = clip(0.15 + 0.5 * depth, 0.15, 0.65)
|
||||
state_t = alpha * posterior + (1 - alpha) * state_{t-1}
|
||||
|
||||
First turn initializes EMA state from prior, then applies delta normally.
|
||||
"""
|
||||
# Map Critic output keys → context feature keys
|
||||
delta_map = {
|
||||
'relationship_depth': rel_delta.get('relationship_delta', 0.0),
|
||||
'emotional_valence': rel_delta.get('emotional_valence', 0.0),
|
||||
'trust_level': rel_delta.get('trust_delta', 0.0),
|
||||
'pending_foresight': 0.0, # No delta for foresight (data-driven only)
|
||||
}
|
||||
|
||||
# Initialize EMA on first turn
|
||||
if not self._relationship_ema:
|
||||
self._relationship_ema = dict(prior)
|
||||
|
||||
# Compute posterior = clip(prior + delta)
|
||||
posterior = {}
|
||||
for k in prior:
|
||||
lo = -1.0 if k == 'emotional_valence' else 0.0
|
||||
posterior[k] = max(lo, min(1.0, prior[k] + delta_map.get(k, 0.0)))
|
||||
|
||||
# Depth-modulated alpha: shallow → trust prior, deep → trust LLM
|
||||
alpha = max(0.15, min(0.65, 0.15 + 0.5 * conversation_depth))
|
||||
|
||||
# EMA smooth
|
||||
ema = {}
|
||||
for k in prior:
|
||||
prev = self._relationship_ema.get(k, prior[k])
|
||||
ema[k] = round(alpha * posterior[k] + (1 - alpha) * prev, 4)
|
||||
self._relationship_ema = ema
|
||||
|
||||
return ema
|
||||
|
||||
def _evermemos_store_bg(self, user_message: str, reply: str) -> None:
|
||||
"""Step 11: Fire-and-forget EverCore storage (asyncio.create_task)."""
|
||||
if not (self.evermemos and self.evermemos.available):
|
||||
return
|
||||
async def _do_store():
|
||||
try:
|
||||
await self.evermemos.store_turn(
|
||||
user_id=self.evermemos_uid,
|
||||
persona_id=self.persona.persona_id,
|
||||
persona_name=self.persona.name,
|
||||
user_name=self.user_name or "用户",
|
||||
group_id=self._group_id,
|
||||
user_message=user_message,
|
||||
agent_reply=reply,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f" [evermemos] ❌ store failed: {type(e).__name__}: {e}")
|
||||
try:
|
||||
asyncio.create_task(_do_store())
|
||||
except Exception as e:
|
||||
print(f" [evermemos] create_task error: {e}")
|
||||
|
||||
def _evermemos_search_bg(self, user_message: str) -> None:
|
||||
"""
|
||||
Step 12: Fire async RRF search for the current user_message.
|
||||
Results are collected at Step 8.5 of the NEXT turn.
|
||||
Cancels any pending search before starting a new one.
|
||||
"""
|
||||
if not (self.evermemos and self.evermemos.available):
|
||||
return
|
||||
if not self._session_ctx or not self._session_ctx.has_history:
|
||||
return
|
||||
|
||||
# Cancel any orphaned previous search task
|
||||
if self._search_task and not self._search_task.done():
|
||||
self._search_task.cancel()
|
||||
self._search_task = None
|
||||
|
||||
try:
|
||||
self._search_turn_id = self._turn_count
|
||||
self._search_task = asyncio.create_task(
|
||||
self.evermemos.search_relevant_memories(
|
||||
query=user_message,
|
||||
user_id=self.evermemos_uid,
|
||||
group_id=self._group_id,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f" [evermemos] search create_task error: {e}")
|
||||
self._search_task = None
|
||||
|
||||
async def _collect_search_results(self) -> None:
|
||||
"""
|
||||
Collect previous turn's async search results (called at Step 8.5).
|
||||
Validates turn_id to prevent concurrent mismatch.
|
||||
Waits up to 0.5s; on timeout/error falls back to empty.
|
||||
"""
|
||||
if self._search_task is None:
|
||||
return
|
||||
|
||||
# Concurrency guard: reject stale results from wrong turn
|
||||
expected_turn = self._turn_count - 1
|
||||
if self._search_turn_id != expected_turn:
|
||||
self._search_task.cancel()
|
||||
self._search_task = None
|
||||
self._relevant_facts = ""
|
||||
self._relevant_episodes = ""
|
||||
self._relevant_profile = ""
|
||||
return
|
||||
|
||||
try:
|
||||
facts, episodes, profile = await asyncio.wait_for(
|
||||
self._search_task, timeout=0.5
|
||||
)
|
||||
self._relevant_facts = facts
|
||||
self._relevant_episodes = episodes
|
||||
self._relevant_profile = profile
|
||||
except asyncio.TimeoutError:
|
||||
# Graceful degradation: use static session context
|
||||
self._relevant_facts = ""
|
||||
self._relevant_episodes = ""
|
||||
self._relevant_profile = ""
|
||||
except Exception:
|
||||
self._relevant_facts = ""
|
||||
self._relevant_episodes = ""
|
||||
self._relevant_profile = ""
|
||||
finally:
|
||||
self._search_task = None
|
||||
66
use-cases/openher/integration/memory_types.py
Normal file
66
use-cases/openher/integration/memory_types.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""
|
||||
Memory shared types for OpenHer.
|
||||
|
||||
These types bridge the two memory providers:
|
||||
- SoulMem (behavioral memory, always-on SQLite layer)
|
||||
- EverCore (declarative memory, cross-session persistence)
|
||||
|
||||
The SessionContext is the key data structure loaded from EverCore
|
||||
at session start — it provides relationship priors, user profile,
|
||||
episode summaries, and foresight data that expand the neural
|
||||
network's perception from 8D to 12D.
|
||||
|
||||
Full source: https://github.com/kellyvv/OpenHer/blob/main/memory/types.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class Memory:
|
||||
"""A single memory entry (SoulMem behavioral layer)."""
|
||||
memory_id: int = 0
|
||||
user_id: str = ""
|
||||
persona_id: str = ""
|
||||
content: str = ""
|
||||
category: str = "conversation" # conversation | fact | event | preference
|
||||
importance: float = 0.5
|
||||
source_turn: int = 0
|
||||
created_at: float = 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class SessionContext:
|
||||
"""
|
||||
EverCore session context (declarative memory).
|
||||
|
||||
Loaded once at session start, this contains everything the
|
||||
persona needs to know about the user from past sessions:
|
||||
|
||||
- user_profile: Who they are (name, preferences, occupation)
|
||||
- episode_summary: What happened between us (narrative history)
|
||||
- foresight_text: What we should pay attention to (unresolved topics)
|
||||
- relationship_*: 4D vector feeding the neural network
|
||||
- has_history: Whether there's prior interaction (gates search)
|
||||
|
||||
These values feed into the ChatAgent lifecycle at multiple steps:
|
||||
- Step 0: Session context loaded
|
||||
- Step 2: user_profile + episode_summary inject into Critic prompt
|
||||
- Step 2.5: relationship_* feed EMA computation
|
||||
- Step 5: 4D vector enters neural network as context features
|
||||
- Step 8.5: Used as fallback when async search times out
|
||||
"""
|
||||
user_id: str = ""
|
||||
persona_id: str = ""
|
||||
user_profile: str = ""
|
||||
episode_summary: str = ""
|
||||
foresight_text: str = ""
|
||||
relationship_depth: float = 0.0
|
||||
emotional_valence: float = 0.0
|
||||
trust_level: float = 0.0
|
||||
pending_foresight: float = 0.0
|
||||
has_history: bool = False
|
||||
raw_data: Optional[dict] = None
|
||||
Reference in New Issue
Block a user