461 lines
15 KiB
Python
461 lines
15 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
OpenHer × EverOS Integration Demo
|
||
|
||
Demonstrates how EverOS provides long-term memory to the
|
||
AI Being persona engine. Shows session context loading, memory
|
||
storage, search, and relationship vector evolution.
|
||
|
||
Usage:
|
||
# With EverMind Cloud
|
||
export EVERMEMOS_BASE_URL=https://api.evermind.ai/v1
|
||
export EVERMEMOS_API_KEY=your_key
|
||
python demo/evermemos_demo.py
|
||
|
||
# With self-hosted EverOS
|
||
export EVERMEMOS_BASE_URL=http://localhost:1995/api/v1
|
||
python demo/evermemos_demo.py
|
||
"""
|
||
|
||
import asyncio
|
||
import os
|
||
import sys
|
||
from datetime import datetime
|
||
|
||
# ──────────────────────────────────────────────
|
||
# EverOS Client (minimal standalone version)
|
||
# ──────────────────────────────────────────────
|
||
|
||
try:
|
||
import httpx
|
||
except ImportError:
|
||
print("❌ httpx not installed. Run: pip install httpx")
|
||
sys.exit(1)
|
||
|
||
|
||
class EverOSClient:
|
||
"""Minimal EverOS 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 EverOS 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 EverOS session)
|
||
# ──────────────────────────────────────────────
|
||
|
||
|
||
def compute_relationship_vector(profile_data: dict) -> dict:
|
||
"""
|
||
Extract 4D relationship vector from EverOS 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: dict | None = None,
|
||
) -> dict:
|
||
"""
|
||
Semi-emergent relationship update (Step 2.5 of ChatAgent lifecycle).
|
||
|
||
Blends EverOS 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 × EverOS Integration Demo")
|
||
print("=" * 60)
|
||
print()
|
||
print("⚠️ EVERMEMOS_BASE_URL not set.")
|
||
print()
|
||
print("To run this demo, set up EverOS:")
|
||
print()
|
||
print(" Option A — EverMind 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/EverOS && 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 = EverOSClient(base_url, api_key)
|
||
|
||
print("=" * 60)
|
||
print("OpenHer × EverOS Integration Demo")
|
||
print("=" * 60)
|
||
print(f"\n📡 EverOS: {base_url}")
|
||
|
||
# Health check
|
||
healthy = await client.health_check()
|
||
if not healthy:
|
||
print("❌ EverOS is not reachable. Check your URL and try again.")
|
||
await client.close()
|
||
return
|
||
print("✅ EverOS 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(
|
||
"\n → After 3 turns: no longer a stranger "
|
||
f"(depth={ema['relationship_depth']:.3f})"
|
||
)
|
||
print(" → 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 EverOS connection)."""
|
||
print("📊 Simulating Relationship Vector Evolution:\n")
|
||
print(" This shows how the 4D EverOS 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" valence={ema['emotional_valence']:+.3f} {bar_v}")
|
||
print(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())
|