Files
EverOS/use-cases/openher/demo/evermemos_demo.py
Elliot Chen 518b8eca85 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.
2026-06-06 07:33:17 +08:00

363 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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())