Skip to content

Commit 5f4b724

Browse files
dani1005claude
andcommitted
✨ examples: add Langfuse (OpenTelemetry) integration example
Trace EverOS memory operations (add / flush+extract / search / reflection) into Langfuse as OpenTelemetry spans, with recall quality pushed as Langfuse scores. Pure OTel SDK, no Langfuse package dependency; runs against a built-in mock or a real EverOS server (EVEROS_BASE_URL). Referenced by the Langfuse docs integration cookbook. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 45656d3 commit 5f4b724

4 files changed

Lines changed: 713 additions & 0 deletions

File tree

examples/langfuse/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
spans.jsonl
2+
__pycache__/
3+
.venv/

examples/langfuse/README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# EverOS × Langfuse (OpenTelemetry)
2+
3+
Trace EverOS memory operations — writes, LLM extraction, recall with quality
4+
scores, and reflection — into [Langfuse](https://langfuse.com) as OpenTelemetry
5+
spans, so an agent's memory layer becomes visible and evaluable next to the rest
6+
of its traces.
7+
8+
This is a thin, dependency-light wrapper (pure OpenTelemetry SDK, no Langfuse
9+
package dependency). The same spans work with Langfuse Cloud, self-hosted
10+
Langfuse, or any other OTLP backend.
11+
12+
## Files
13+
14+
- `everos_langfuse.py` — the instrumentation wrapper (`init_tracing`,
15+
`InstrumentedEverOS`, `HTTPTransport`, recall-score push).
16+
- `demo.py` — a runnable end-to-end example. Ships a mock transport, so it runs
17+
with **no EverOS server required**; set `EVEROS_BASE_URL` to trace a real one.
18+
19+
## Span model
20+
21+
| EverOS operation | Langfuse observation |
22+
| --- | --- |
23+
| `POST /api/v1/memory/add` | span `everos.memory.add` |
24+
| `POST /api/v1/memory/flush` → extraction | span + generation `everos.extract` (model + tokens) |
25+
| markdown persistence | span `everos.persist.markdown` |
26+
| async index sync | span `everos.cascade.index` (separate correlated trace) |
27+
| `POST /api/v1/memory/search` | retriever `everos.memory.search` |
28+
| ↳ embedding / hybrid recall / rerank | embedding / retriever / span |
29+
| `POST /api/v1/ome/trigger` | agent `everos.ome.<strategy>` + generation |
30+
31+
`langfuse.session.id` / `langfuse.user.id` are set on every span; recall quality
32+
is pushed as Langfuse scores (`recall_top_score`, `recall_hit`).
33+
34+
## Run
35+
36+
```bash
37+
pip install opentelemetry-sdk opentelemetry-exporter-otlp requests
38+
39+
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
40+
export LANGFUSE_SECRET_KEY="sk-lf-..."
41+
export LANGFUSE_HOST="https://us.cloud.langfuse.com" # EU: https://cloud.langfuse.com
42+
43+
python demo.py
44+
```
45+
46+
- With no keys set, `demo.py` still runs against the built-in mock and writes a
47+
local `spans.jsonl` (offline inspection) — nothing is sent anywhere.
48+
- With Langfuse keys set, the same spans and recall scores flow into your
49+
Langfuse project. Open **Tracing → Traces** (filter by tag `everos` / `memory`).
50+
- To trace a real deployment, set `EVEROS_BASE_URL` to a running EverOS server
51+
(see the [EverOS quickstart](../../README.md)); the instrumentation is identical.
52+
53+
## Privacy
54+
55+
Spans carry non-sensitive metadata (latency, token counts, model names, scores)
56+
by default. Capturing raw query or memory content as span input/output is opt-in.
57+
The demo uses synthetic data, and its `public_traces` flag (safe only for
58+
synthetic data) marks the resulting traces as publicly shareable.
59+
60+
## Learn more
61+
62+
- Langfuse OpenTelemetry docs: https://langfuse.com/integrations/native/opentelemetry
63+
- Native, opt-in instrumentation inside EverOS core is planned; this wrapper is
64+
the interim path and mirrors the same span model.

examples/langfuse/demo.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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

Comments
 (0)