|
| 1 | +"""End-to-end demo: EverOS memory operations traced into Langfuse. |
| 2 | +
|
| 3 | +Replays one realistic memory lifecycle — ingest -> extraction -> recall (with |
| 4 | +an updated fact winning over a stale one) -> agent-skill recall -> reflection — |
| 5 | +through the instrumentation in everos_langfuse.py. |
| 6 | +
|
| 7 | +Two modes, same code path: |
| 8 | + * offline (default) — spans land in ./spans.jsonl for offline inspection |
| 9 | + * live — set LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / |
| 10 | + LANGFUSE_HOST and the exact same spans + recall |
| 11 | + scores also flow into your Langfuse project. |
| 12 | +
|
| 13 | +The MockEverOSTransport returns responses in the exact envelope/shape of the |
| 14 | +EverOS HTTP API v1 (see EverOS docs/api.md); swap in HTTPTransport to run |
| 15 | +against a real `pip install everos` server — the instrumentation is identical. |
| 16 | +""" |
| 17 | + |
| 18 | +from __future__ import annotations |
| 19 | + |
| 20 | +import time |
| 21 | +import uuid |
| 22 | + |
| 23 | +from everos_langfuse import HTTPTransport, InstrumentedEverOS, force_flush, init_tracing |
| 24 | + |
| 25 | +TS = int(time.time() * 1000) |
| 26 | +DAY = "20260702" |
| 27 | + |
| 28 | + |
| 29 | +def _envelope(data: dict, detail: dict | None = None) -> dict: |
| 30 | + resp = {"request_id": uuid.uuid4().hex, "data": data} |
| 31 | + if detail: |
| 32 | + resp["_detail"] = detail # server-side facts the spans describe |
| 33 | + return resp |
| 34 | + |
| 35 | + |
| 36 | +class MockEverOSTransport: |
| 37 | + """Faithful mock of the EverOS HTTP API v1 (response envelope + field |
| 38 | + shapes from docs/api.md), so the demo runs without provider keys.""" |
| 39 | + |
| 40 | + def __init__(self): |
| 41 | + self.buffer: list[dict] = [] |
| 42 | + |
| 43 | + def __call__(self, path: str, payload: dict) -> dict: |
| 44 | + if path == "/api/v1/memory/add": |
| 45 | + self.buffer.extend(payload["messages"]) |
| 46 | + time.sleep(0.012) |
| 47 | + return _envelope({"message_count": len(payload["messages"]), |
| 48 | + "status": "accumulated"}) |
| 49 | + |
| 50 | + if path == "/api/v1/memory/flush": |
| 51 | + buffered, self.buffer = self.buffer, [] |
| 52 | + time.sleep(0.01) |
| 53 | + return _envelope( |
| 54 | + {"status": "extracted"}, |
| 55 | + detail={ |
| 56 | + "model": "gpt-4.1-mini", |
| 57 | + "buffered_messages": [m["content"] for m in buffered], |
| 58 | + "memory_cell": { |
| 59 | + "episode_id": "alice_ep_%s_001" % DAY, |
| 60 | + "subject": "Alice's routines and recent move", |
| 61 | + "summary": ("Alice climbs in Yosemite every spring, bikes to " |
| 62 | + "work, and recently moved from SOMA to Oakland; " |
| 63 | + "her go-to coffee used to be Blue Bottle in SOMA."), |
| 64 | + "atomic_facts": [ |
| 65 | + "Alice climbs in Yosemite every spring.", |
| 66 | + "Alice bikes to work most days.", |
| 67 | + "Alice moved from SOMA to Oakland in June 2026.", |
| 68 | + "Alice's favorite coffee shop was Blue Bottle in SOMA.", |
| 69 | + ], |
| 70 | + }, |
| 71 | + "usage": {"input": 642, "output": 187}, |
| 72 | + "md_files": ["memory/alice/episodic/2026-07-02-alice-routines.md"], |
| 73 | + "rows_indexed": 5, |
| 74 | + "index_lag_ms": 512, |
| 75 | + "extract_s": 0.42, |
| 76 | + }, |
| 77 | + ) |
| 78 | + |
| 79 | + if path == "/api/v1/memory/search": |
| 80 | + q = payload["query"].lower() |
| 81 | + if "live" in q: # conflict-resolution showcase: fresh fact outranks stale |
| 82 | + ranked = [ |
| 83 | + {"id": "alice_af_%s_003" % DAY, |
| 84 | + "content": "Alice moved from SOMA to Oakland in June 2026.", |
| 85 | + "score": 0.81}, |
| 86 | + {"id": "alice_af_%s_004" % DAY, |
| 87 | + "content": "Alice's favorite coffee shop was Blue Bottle in SOMA.", |
| 88 | + "score": 0.34}, |
| 89 | + ] |
| 90 | + elif "sport" in q or "outdoor" in q: |
| 91 | + ranked = [ |
| 92 | + {"id": "alice_af_%s_001" % DAY, |
| 93 | + "content": "Alice climbs in Yosemite every spring.", "score": 0.86}, |
| 94 | + {"id": "alice_af_%s_002" % DAY, |
| 95 | + "content": "Alice bikes to work most days.", "score": 0.72}, |
| 96 | + ] |
| 97 | + elif payload.get("agent_id"): # agent track: cases + skills |
| 98 | + ranked = [ |
| 99 | + {"id": "raven_case_%s_007" % DAY, |
| 100 | + "content": "Case: flaky LanceDB test fixed by pinning fsync " |
| 101 | + "before rename and retrying open with backoff.", |
| 102 | + "score": 0.74}, |
| 103 | + {"id": "raven_skill_retry_backoff", |
| 104 | + "content": "Skill: wrap flaky IO in retry-with-backoff; verify " |
| 105 | + "with 3 consecutive green runs.", |
| 106 | + "score": 0.69}, |
| 107 | + ] |
| 108 | + else: # deliberate miss: query about something never stored |
| 109 | + ranked = [ |
| 110 | + {"id": "alice_af_%s_002" % DAY, |
| 111 | + "content": "Alice bikes to work most days.", "score": 0.31}, |
| 112 | + ] |
| 113 | + |
| 114 | + time.sleep(0.01) |
| 115 | + if payload.get("agent_id"): |
| 116 | + data = {"episodes": [], "profiles": [], |
| 117 | + "agent_cases": [r for r in ranked if "case" in r["id"]], |
| 118 | + "agent_skills": [r for r in ranked if "skill" in r["id"]], |
| 119 | + "unprocessed_messages": []} |
| 120 | + else: |
| 121 | + data = {"episodes": [{ |
| 122 | + "id": "alice_ep_%s_001" % DAY, |
| 123 | + "user_id": payload.get("user_id"), |
| 124 | + "session_id": "sess-cafe-chat-001", |
| 125 | + "summary": "Alice's routines and recent move", |
| 126 | + "score": ranked[0]["score"], |
| 127 | + "atomic_facts": ranked, |
| 128 | + }], |
| 129 | + "profiles": [], "agent_cases": [], "agent_skills": [], |
| 130 | + "unprocessed_messages": []} |
| 131 | + return _envelope(data, detail={ |
| 132 | + "embed_model": "Qwen/Qwen3-Embedding-4B", "embed_tokens": 11, |
| 133 | + "rerank_model": "Qwen/Qwen3-Reranker-4B", |
| 134 | + "candidates": 24, "ranked": ranked, |
| 135 | + "embed_s": 0.028, "recall_s": 0.019, "rerank_s": 0.047, |
| 136 | + }) |
| 137 | + |
| 138 | + if path == "/api/v1/ome/trigger": |
| 139 | + time.sleep(0.01) |
| 140 | + return _envelope( |
| 141 | + {"status": "ok", "name": payload["name"]}, |
| 142 | + detail={ |
| 143 | + "model": "gpt-4.1-mini", |
| 144 | + "episodes_in": ["alice_ep_%s_001" % DAY], |
| 145 | + "consolidated": { |
| 146 | + "profile_update": "home_location: SOMA -> Oakland (2026-06)", |
| 147 | + "episodes_merged": 1, |
| 148 | + }, |
| 149 | + "usage": {"input": 918, "output": 141}, |
| 150 | + "reflect_s": 0.31, |
| 151 | + }, |
| 152 | + ) |
| 153 | + |
| 154 | + raise ValueError(f"unknown path {path}") |
| 155 | + |
| 156 | + |
| 157 | +def main() -> None: |
| 158 | + live = init_tracing(service_name="everos", spans_jsonl="spans.jsonl") |
| 159 | + print(f"[demo] tracing initialised — live Langfuse export: {live}") |
| 160 | + |
| 161 | + import os |
| 162 | + if os.getenv("EVEROS_BASE_URL"): |
| 163 | + transport = HTTPTransport(os.environ["EVEROS_BASE_URL"]) |
| 164 | + print(f"[demo] using real EverOS server at {os.environ['EVEROS_BASE_URL']}") |
| 165 | + else: |
| 166 | + transport = MockEverOSTransport() |
| 167 | + print("[demo] using MockEverOSTransport (EverOS HTTP API v1 shapes)") |
| 168 | + |
| 169 | + # public_traces=True: demo data is synthetic (fictional "Alice"), so the |
| 170 | + # resulting traces are safe to share as public Langfuse trace URLs. |
| 171 | + ev = InstrumentedEverOS(transport, public_traces=True) |
| 172 | + session, user = "sess-cafe-chat-001", "alice" |
| 173 | + |
| 174 | + # -- 1. write path: ingest a conversation ------------------------------ |
| 175 | + ev.add(session, [ |
| 176 | + {"sender_id": user, "role": "user", "timestamp": TS, |
| 177 | + "content": "I love climbing in Yosemite every spring."}, |
| 178 | + {"sender_id": user, "role": "user", "timestamp": TS + 10, |
| 179 | + "content": "My favorite coffee shop is Blue Bottle in SOMA."}, |
| 180 | + {"sender_id": user, "role": "user", "timestamp": TS + 20, |
| 181 | + "content": "I bike to work most days."}, |
| 182 | + ], user_id=user) |
| 183 | + ev.add(session, [ |
| 184 | + {"sender_id": user, "role": "user", "timestamp": TS + 30, |
| 185 | + "content": "Oh — actually I moved from SOMA to Oakland last month."}, |
| 186 | + ], user_id=user) |
| 187 | + |
| 188 | + # -- 2. boundary/flush: LLM extraction -> markdown -> index ------------ |
| 189 | + ev.flush(session, user_id=user) |
| 190 | + |
| 191 | + # -- 3. read path: recall with quality scores --------------------------- |
| 192 | + r1 = ev.search("What outdoor sports does Alice do?", user_id=user, |
| 193 | + session_id=session) |
| 194 | + r2 = ev.search("Where does Alice live now?", user_id=user, session_id=session) |
| 195 | + r3 = ev.search("What are Alice's favorite books?", user_id=user, |
| 196 | + session_id=session) # deliberate low-quality recall |
| 197 | + # agent-memory track (cases / skills) — the Raven angle |
| 198 | + r4 = ev.search("How did we fix the flaky LanceDB test last time?", |
| 199 | + agent_id="raven-dev-agent", session_id="raven-run-042") |
| 200 | + |
| 201 | + # -- 4. self-evolution: offline reflection ------------------------------ |
| 202 | + ev.trigger_ome("reflect_episodes", user_id=user, session_id=session) |
| 203 | + |
| 204 | + force_flush() |
| 205 | + time.sleep(0.5) |
| 206 | + |
| 207 | + print("\n[demo] traces emitted:") |
| 208 | + for label, r in [("recall: sports", r1), ("recall: moved city", r2), |
| 209 | + ("recall: miss (books)", r3), ("recall: agent skill", r4)]: |
| 210 | + print(f" - {label:24s} trace_id={r['_trace_id']} " |
| 211 | + f"scores_pushed={r['_scores_pushed']}") |
| 212 | + print("\n[demo] spans also written to spans.jsonl (offline copy)") |
| 213 | + if live: |
| 214 | + print("[demo] open your Langfuse project -> Traces; " |
| 215 | + "scores 'recall_top_score' / 'recall_hit' attached to searches.") |
| 216 | + |
| 217 | + |
| 218 | +if __name__ == "__main__": |
| 219 | + main() |
0 commit comments