windhover 0.8.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {windhover-0.8.0 → windhover-0.9.0}/PKG-INFO +9 -2
- {windhover-0.8.0 → windhover-0.9.0}/README.md +8 -1
- {windhover-0.8.0 → windhover-0.9.0}/pyproject.toml +1 -1
- {windhover-0.8.0 → windhover-0.9.0}/tests/test_smoke.py +63 -0
- {windhover-0.8.0 → windhover-0.9.0}/windhover/demo_graph.py +19 -3
- {windhover-0.8.0 → windhover-0.9.0}/windhover/server.py +37 -3
- {windhover-0.8.0 → windhover-0.9.0}/windhover/static/index.html +77 -7
- {windhover-0.8.0 → windhover-0.9.0}/windhover/store.py +13 -5
- {windhover-0.8.0 → windhover-0.9.0}/windhover/tracer.py +44 -8
- {windhover-0.8.0 → windhover-0.9.0}/.github/workflows/ci.yml +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/.gitignore +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/LICENSE +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/SPEC.md +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/docs/graph.png +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/docs/logo.svg +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/docs/runs.png +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/docs/social-preview.png +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/docs/stats.png +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/docs/trace.png +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/tests/__init__.py +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/windhover/__init__.py +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/windhover/config.py +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/windhover/extract.py +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/windhover/pricing.json +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/windhover/static/icon.svg +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/windhover/static/manifest.json +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/windhover/static/vendor/cytoscape-dagre.js +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/windhover/static/vendor/cytoscape.min.js +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/windhover/static/vendor/dagre.min.js +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/windhover/static/vendor/fonts/fraunces-500.woff2 +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/windhover/static/vendor/fonts/fraunces-600-italic.woff2 +0 -0
- {windhover-0.8.0 → windhover-0.9.0}/windhover/static/vendor/fonts/fraunces-600.woff2 +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: windhover
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Self-hosted, mobile-friendly observability for LangGraph — traces, run history, cost, and a living graph view.
|
|
5
5
|
Project-URL: Repository, https://github.com/justfeltlikerunning/windhover
|
|
6
6
|
Author: justfeltlikerunning
|
|
@@ -90,7 +90,14 @@ graph.invoke(input, config={
|
|
|
90
90
|
status/tag/session filters, bookmarks, pagination, CSV/JSON export.
|
|
91
91
|
- **Sessions** — group runs into threads/batches; roll-up tokens, cost, errors.
|
|
92
92
|
- **Scores** — attach numeric evals to runs (API or UI): eval harnesses, LLM-as-judge, human review.
|
|
93
|
-
- **Live tail** — open a running run and watch its spans arrive in real time
|
|
93
|
+
- **Live tail** — open a running run and watch its spans arrive in real time; nodes can push
|
|
94
|
+
progress via LangGraph's `get_stream_writer()`.
|
|
95
|
+
- **Custom events** — `dispatch_custom_event("name", {...})` anywhere in your app lands as an
|
|
96
|
+
event marker in the trace, parented to the node that fired it.
|
|
97
|
+
- **Retries + TTFT** — tenacity retries badge the span (`↻2`); streaming LLM calls record
|
|
98
|
+
time-to-first-token; cache-read / reasoning token details show on the model line.
|
|
99
|
+
- **Memory browser** — graphs compiled with a LangGraph `Store` get a Memory view: browse
|
|
100
|
+
namespaces and search long-term memory items.
|
|
94
101
|
- **Time-travel** — checkpointed graphs get a per-thread checkpoint browser: state, writes,
|
|
95
102
|
and next-nodes at every superstep (`get_state_history`).
|
|
96
103
|
- **Run diff** — compare any two runs node-by-node: identical vs differing outputs,
|
|
@@ -71,7 +71,14 @@ graph.invoke(input, config={
|
|
|
71
71
|
status/tag/session filters, bookmarks, pagination, CSV/JSON export.
|
|
72
72
|
- **Sessions** — group runs into threads/batches; roll-up tokens, cost, errors.
|
|
73
73
|
- **Scores** — attach numeric evals to runs (API or UI): eval harnesses, LLM-as-judge, human review.
|
|
74
|
-
- **Live tail** — open a running run and watch its spans arrive in real time
|
|
74
|
+
- **Live tail** — open a running run and watch its spans arrive in real time; nodes can push
|
|
75
|
+
progress via LangGraph's `get_stream_writer()`.
|
|
76
|
+
- **Custom events** — `dispatch_custom_event("name", {...})` anywhere in your app lands as an
|
|
77
|
+
event marker in the trace, parented to the node that fired it.
|
|
78
|
+
- **Retries + TTFT** — tenacity retries badge the span (`↻2`); streaming LLM calls record
|
|
79
|
+
time-to-first-token; cache-read / reasoning token details show on the model line.
|
|
80
|
+
- **Memory browser** — graphs compiled with a LangGraph `Store` get a Memory view: browse
|
|
81
|
+
namespaces and search long-term memory items.
|
|
75
82
|
- **Time-travel** — checkpointed graphs get a per-thread checkpoint browser: state, writes,
|
|
76
83
|
and next-nodes at every superstep (`get_state_history`).
|
|
77
84
|
- **Run diff** — compare any two runs node-by-node: identical vs differing outputs,
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "windhover"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.0"
|
|
8
8
|
description = "Self-hosted, mobile-friendly observability for LangGraph — traces, run history, cost, and a living graph view."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -299,6 +299,67 @@ def test_datasets_and_scoring():
|
|
|
299
299
|
print("datasets + scoring OK")
|
|
300
300
|
|
|
301
301
|
|
|
302
|
+
def test_ttft_retry_custom_event_usage_detail():
|
|
303
|
+
p = tempfile.mktemp(suffix=".db")
|
|
304
|
+
s = Store(p)
|
|
305
|
+
tr = SpanBuilder(db_sink(s), run_name="deep")
|
|
306
|
+
tr.on_chain_start({}, {"q": 1}, run_id="root", parent_run_id=None)
|
|
307
|
+
tr.on_chat_model_start({"kwargs": {"model": "gpt-4o"}}, [[]],
|
|
308
|
+
run_id="llm1", parent_run_id="root")
|
|
309
|
+
time.sleep(0.02)
|
|
310
|
+
tr.on_llm_new_token("Hel", run_id="llm1") # -> ttft stamped
|
|
311
|
+
tr.on_llm_new_token("lo", run_id="llm1") # ignored (only first counts)
|
|
312
|
+
|
|
313
|
+
class RS: # tenacity RetryCallState stand-in
|
|
314
|
+
attempt_number = 3
|
|
315
|
+
tr.on_retry(RS(), run_id="llm1")
|
|
316
|
+
|
|
317
|
+
class Msg:
|
|
318
|
+
content = "Hello"
|
|
319
|
+
usage_metadata = {"input_tokens": 900, "output_tokens": 40,
|
|
320
|
+
"input_token_details": {"cache_read": 700},
|
|
321
|
+
"output_token_details": {"reasoning": 12}}
|
|
322
|
+
class Gen:
|
|
323
|
+
text = "Hello"; message = Msg()
|
|
324
|
+
class Resp:
|
|
325
|
+
llm_output = None; generations = [[Gen()]]
|
|
326
|
+
tr.on_llm_end(Resp(), run_id="llm1")
|
|
327
|
+
tr.on_custom_event("checkpoint-saved", {"rows": 42}, run_id="root")
|
|
328
|
+
tr.on_chain_end({}, run_id="root")
|
|
329
|
+
|
|
330
|
+
d = s.run_detail(tr.run_id)
|
|
331
|
+
llm = [x for x in d["spans"] if x["type"] == "llm"][0]
|
|
332
|
+
assert llm["ttft_ms"] is not None and llm["ttft_ms"] >= 15
|
|
333
|
+
assert llm["retries"] == 3
|
|
334
|
+
assert llm["usage_detail"] == {"input_cache_read": 700, "output_reasoning": 12}
|
|
335
|
+
assert llm["prompt_tokens"] == 900
|
|
336
|
+
ev = [x for x in d["spans"] if x["type"] == "event"]
|
|
337
|
+
assert ev and ev[0]["name"] == "checkpoint-saved" and ev[0]["output"] == {"rows": 42}
|
|
338
|
+
print("ttft/retry/custom-event/usage-detail OK")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def test_demo_memory_and_events_end_to_end():
|
|
342
|
+
"""Demo graph writes long-term memory + dispatches a custom event — both
|
|
343
|
+
must be observable."""
|
|
344
|
+
import importlib
|
|
345
|
+
import windhover.demo_graph as dg
|
|
346
|
+
importlib.reload(dg) # fresh InMemoryStore per test
|
|
347
|
+
p = tempfile.mktemp(suffix=".db")
|
|
348
|
+
s = Store(p)
|
|
349
|
+
tr = SpanBuilder(db_sink(s), run_name="demo")
|
|
350
|
+
dg.graph.invoke({"n": 7}, config={"callbacks": [tr],
|
|
351
|
+
"configurable": {"thread_id": "mem-t"}})
|
|
352
|
+
time.sleep(.1)
|
|
353
|
+
d = s.run_detail(tr.run_id)
|
|
354
|
+
ev = [x for x in d["spans"] if x["type"] == "event"]
|
|
355
|
+
assert ev and ev[0]["name"] == "summary-ready", [x["type"] for x in d["spans"]]
|
|
356
|
+
assert dg.graph.store is not None
|
|
357
|
+
items = dg.graph.store.search(("demo", "summaries"))
|
|
358
|
+
assert items and items[0].value["n"] == 21 # n=7 -> grow x3
|
|
359
|
+
assert ("demo", "summaries") in dg.graph.store.list_namespaces()
|
|
360
|
+
print("demo memory + custom event end-to-end OK")
|
|
361
|
+
|
|
362
|
+
|
|
302
363
|
if __name__ == "__main__":
|
|
303
364
|
test_cost(); print("cost OK")
|
|
304
365
|
test_store_roundtrip()
|
|
@@ -314,4 +375,6 @@ if __name__ == "__main__":
|
|
|
314
375
|
test_auth_check()
|
|
315
376
|
test_thread_capture_and_history()
|
|
316
377
|
test_datasets_and_scoring()
|
|
378
|
+
test_ttft_retry_custom_event_usage_detail()
|
|
379
|
+
test_demo_memory_and_events_end_to_end()
|
|
317
380
|
print("ALL SMOKE TESTS PASSED")
|
|
@@ -28,7 +28,22 @@ def grow(s): time.sleep(.3); return {"n": s["n"] * 3}
|
|
|
28
28
|
def parity(s): time.sleep(.4); return {"notes": ["even" if s["n"] % 2 == 0 else "odd"]}
|
|
29
29
|
def sign(s): time.sleep(.25); return {"notes": ["positive" if s["n"] > 0 else "non-positive"]}
|
|
30
30
|
def magnitude(s): time.sleep(.35); return {"notes": ["big" if abs(s["n"]) > 100 else "small"]}
|
|
31
|
-
def summarize(s):
|
|
31
|
+
def summarize(s):
|
|
32
|
+
time.sleep(.1)
|
|
33
|
+
try: # progress + custom events + long-term memory, all observable in Windhover
|
|
34
|
+
from langgraph.config import get_stream_writer, get_store
|
|
35
|
+
writer = get_stream_writer()
|
|
36
|
+
writer({"stage": "summarize", "pct": 50})
|
|
37
|
+
from langchain_core.callbacks import dispatch_custom_event
|
|
38
|
+
dispatch_custom_event("summary-ready", {"n": s["n"], "parts": len(s["notes"])})
|
|
39
|
+
store = get_store()
|
|
40
|
+
if store is not None:
|
|
41
|
+
store.put(("demo", "summaries"), f"n-{s['n']}",
|
|
42
|
+
{"n": s["n"], "notes": s["notes"]})
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
time.sleep(.1)
|
|
46
|
+
return {"notes": [f"n={s['n']}: " + ", ".join(s["notes"])]}
|
|
32
47
|
|
|
33
48
|
|
|
34
49
|
_g = StateGraph(State)
|
|
@@ -45,8 +60,9 @@ _g.add_edge("sign", "summarize")
|
|
|
45
60
|
_g.add_edge("magnitude", "summarize")
|
|
46
61
|
_g.add_edge("summarize", END)
|
|
47
62
|
|
|
48
|
-
try: # checkpointer
|
|
63
|
+
try: # checkpointer + store make Time-travel and Memory demoable
|
|
49
64
|
from langgraph.checkpoint.memory import MemorySaver
|
|
50
|
-
|
|
65
|
+
from langgraph.store.memory import InMemoryStore
|
|
66
|
+
graph = _g.compile(checkpointer=MemorySaver(), store=InMemoryStore())
|
|
51
67
|
except Exception:
|
|
52
68
|
graph = _g.compile()
|
|
@@ -186,9 +186,13 @@ async def api_run(request: Request):
|
|
|
186
186
|
def worker():
|
|
187
187
|
try:
|
|
188
188
|
interrupted = False
|
|
189
|
-
for
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
for mode, chunk in graph.stream(payload, config=config,
|
|
190
|
+
stream_mode=["updates", "custom"]):
|
|
191
|
+
if mode == "custom":
|
|
192
|
+
# get_stream_writer() output from inside a node -> live progress
|
|
193
|
+
q.put(("progress", {"data": _trunc(chunk, 600)}))
|
|
194
|
+
continue
|
|
195
|
+
for node in chunk:
|
|
192
196
|
if node == "__interrupt__":
|
|
193
197
|
interrupted = True
|
|
194
198
|
q.put(("interrupt", {"run_id": run_id}))
|
|
@@ -340,6 +344,36 @@ def api_thread_history(thread_id: str, limit: int = 80):
|
|
|
340
344
|
return JSONResponse({"thread_id": thread_id, "steps": steps})
|
|
341
345
|
|
|
342
346
|
|
|
347
|
+
@app.get("/api/memory/namespaces")
|
|
348
|
+
def api_memory_namespaces():
|
|
349
|
+
"""LangGraph long-term memory (Store) browser — namespaces."""
|
|
350
|
+
st = getattr(graph, "store", None) if graph is not None else None
|
|
351
|
+
if st is None:
|
|
352
|
+
return JSONResponse({"error": "no local graph with a store"}, 404)
|
|
353
|
+
try:
|
|
354
|
+
return JSONResponse({"namespaces": [list(ns) for ns in st.list_namespaces()]})
|
|
355
|
+
except Exception as e:
|
|
356
|
+
return JSONResponse({"error": str(e)}, 500)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@app.get("/api/memory/items")
|
|
360
|
+
def api_memory_items(namespace: str, query: str = None, limit: int = 50):
|
|
361
|
+
"""Items in one namespace (dot-separated); optional semantic/text query."""
|
|
362
|
+
st = getattr(graph, "store", None) if graph is not None else None
|
|
363
|
+
if st is None:
|
|
364
|
+
return JSONResponse({"error": "no local graph with a store"}, 404)
|
|
365
|
+
try:
|
|
366
|
+
ns = tuple(namespace.split(".")) if namespace else ()
|
|
367
|
+
items = st.search(ns, query=query, limit=max(1, min(limit, 200)))
|
|
368
|
+
return JSONResponse({"namespace": namespace, "items": [
|
|
369
|
+
{"key": i.key, "value": _trunc(i.value, 3000),
|
|
370
|
+
"created_at": str(getattr(i, "created_at", "") or "") or None,
|
|
371
|
+
"updated_at": str(getattr(i, "updated_at", "") or "") or None,
|
|
372
|
+
"score": getattr(i, "score", None)} for i in items]})
|
|
373
|
+
except Exception as e:
|
|
374
|
+
return JSONResponse({"error": str(e)}, 500)
|
|
375
|
+
|
|
376
|
+
|
|
343
377
|
@app.get("/api/datasets")
|
|
344
378
|
def api_datasets():
|
|
345
379
|
return JSONResponse([{k: d[k] for k in ("id", "name", "n_items", "created_ms")}
|
|
@@ -236,6 +236,7 @@ window.pickDiff=async(id)=>{
|
|
|
236
236
|
.chip.tool{background:var(--chip-tool);color:var(--chip-tool-t)}
|
|
237
237
|
.chip.retriever{background:var(--chip-ret);color:var(--chip-ret-t)}
|
|
238
238
|
.chip.interrupt{background:var(--warn-weak);color:var(--warn)}
|
|
239
|
+
.chip.event{background:var(--surface2);color:var(--text2);border:1px dashed var(--border2)}
|
|
239
240
|
.tr-name{font-weight:600;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
240
241
|
.tr-name.err{color:var(--err)}
|
|
241
242
|
.tr-gantt{height:6px;background:var(--surface2);border-radius:3px;position:relative;overflow:hidden}
|
|
@@ -250,6 +251,12 @@ window.pickDiff=async(id)=>{
|
|
|
250
251
|
.code .ln{color:var(--muted);text-align:right;user-select:none;position:sticky;left:-12px;background:var(--surface2);padding-left:12px}
|
|
251
252
|
.code .cl{white-space:pre}
|
|
252
253
|
.code .errline{background:var(--err-weak)}
|
|
254
|
+
.py-k{color:var(--accent-text);font-weight:600}
|
|
255
|
+
.py-s{color:var(--ok)}
|
|
256
|
+
.py-c{color:var(--muted);font-style:italic}
|
|
257
|
+
.py-n{color:var(--chip-tool-t)}
|
|
258
|
+
.py-d{color:var(--chip-llm-t)}
|
|
259
|
+
.py-f{font-weight:700}
|
|
253
260
|
.code .cl.errline{box-shadow:inset 3px 0 0 var(--err);padding-left:6px;margin-left:-6px}
|
|
254
261
|
.errhint{font:11px var(--mono);color:var(--err);margin-top:3px;max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
255
262
|
.diffgrid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
|
@@ -341,6 +348,8 @@ th{font-family:var(--mono);letter-spacing:.11em;font-size:10.5px}
|
|
|
341
348
|
.tr-bar.tool{background:var(--chip-tool-t)}
|
|
342
349
|
.tr-bar.retriever{background:var(--chip-ret-t)}
|
|
343
350
|
.tr-bar.interrupt{background:var(--warn)}
|
|
351
|
+
.tr-bar.event{background:var(--border2)}
|
|
352
|
+
.badge-retry{font-family:var(--mono);font-size:10px;color:var(--warn);background:var(--warn-weak);border-radius:5px;padding:1px 6px;margin-left:6px}
|
|
344
353
|
.charts .card:nth-child(2) .cbar{background:var(--chip-llm-t)}
|
|
345
354
|
#cy{background:radial-gradient(circle,var(--border) 1px,transparent 1.5px) 0 0/24px 24px}
|
|
346
355
|
.legend{font-family:var(--mono);font-size:10.5px;letter-spacing:.06em;text-transform:uppercase}
|
|
@@ -384,9 +393,10 @@ th{font-family:var(--mono);letter-spacing:.11em;font-size:10.5px}
|
|
|
384
393
|
<button data-v="graph" class="on">${ICON.graph} Graph</button>
|
|
385
394
|
<button data-v="runs">${ICON.list} Runs</button>
|
|
386
395
|
<button data-v="sessions">${ICON.layers} Sessions</button>
|
|
396
|
+
<button data-v="memory" id="nav-mem" style="display:none">${ICON.mem} Memory</button>
|
|
387
397
|
<button data-v="stats">${ICON.chart} Stats</button>
|
|
388
398
|
</nav>
|
|
389
|
-
<div class="side-foot"><span class="live-dot" id="live-dot" title="live topology"></span><span id="foot-note">watching</span><span class="spacer"></span><span class="mono" id="ver">v0.
|
|
399
|
+
<div class="side-foot"><span class="live-dot" id="live-dot" title="live topology"></span><span id="foot-note">watching</span><span class="spacer"></span><span class="mono" id="ver">v0.9</span></div>
|
|
390
400
|
</aside>
|
|
391
401
|
|
|
392
402
|
<main>
|
|
@@ -438,6 +448,16 @@ th{font-family:var(--mono);letter-spacing:.11em;font-size:10.5px}
|
|
|
438
448
|
<div class="card"><div id="sess-wrap"><div class="empty">Loading…</div></div></div>
|
|
439
449
|
</div>
|
|
440
450
|
</div>
|
|
451
|
+
<div id="view-memory" style="display:none">
|
|
452
|
+
<div class="page">
|
|
453
|
+
<div class="page-head"><h1>Memory</h1><span class="sub">LangGraph long-term store</span></div>
|
|
454
|
+
<div class="rtools">
|
|
455
|
+
<select id="mem-ns"></select>
|
|
456
|
+
<input type="search" id="mem-q" placeholder="Search this namespace…">
|
|
457
|
+
</div>
|
|
458
|
+
<div class="card"><div id="mem-wrap"><div class="empty">Loading…</div></div></div>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
441
461
|
<div id="view-stats" style="display:none">
|
|
442
462
|
<div class="page">
|
|
443
463
|
<div class="page-head"><h1>Stats</h1><span class="sub">all recorded runs</span></div>
|
|
@@ -452,6 +472,7 @@ th{font-family:var(--mono);letter-spacing:.11em;font-size:10.5px}
|
|
|
452
472
|
<button data-v="graph" class="on">${ICON.graph}<span>Graph</span></button>
|
|
453
473
|
<button data-v="runs">${ICON.list}<span>Runs</span></button>
|
|
454
474
|
<button data-v="sessions">${ICON.layers}<span>Sessions</span></button>
|
|
475
|
+
<button data-v="memory" class="nav-mem" style="display:none">${ICON.mem}<span>Memory</span></button>
|
|
455
476
|
<button data-v="stats">${ICON.chart}<span>Stats</span></button>
|
|
456
477
|
</div>
|
|
457
478
|
|
|
@@ -491,6 +512,7 @@ const ICON = {
|
|
|
491
512
|
list:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M8 6h12M8 12h12M8 18h12"/><circle cx="4" cy="6" r="1.1" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1.1" fill="currentColor" stroke="none"/><circle cx="4" cy="18" r="1.1" fill="currentColor" stroke="none"/></svg>',
|
|
492
513
|
chart:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M4 20V9M10 20V4M16 20v-7M21 20H3"/></svg>',
|
|
493
514
|
layers:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3 3 8l9 5 9-5-9-5z"/><path d="M3 13l9 5 9-5"/></svg>',
|
|
515
|
+
mem:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><ellipse cx="12" cy="5.5" rx="7.5" ry="2.8"/><path d="M4.5 5.5v6c0 1.6 3.4 2.8 7.5 2.8s7.5-1.2 7.5-2.8v-6"/><path d="M4.5 11.5v6c0 1.6 3.4 2.8 7.5 2.8s7.5-1.2 7.5-2.8v-6"/></svg>',
|
|
494
516
|
play:'<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M7 4.8v14.4c0 .8.9 1.3 1.6.9l11-7.2c.6-.4.6-1.4 0-1.8l-11-7.2c-.7-.4-1.6.1-1.6.9z"/></svg>',
|
|
495
517
|
moon:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M20 13.5A8 8 0 1 1 10.5 4 6.5 6.5 0 0 0 20 13.5z"/></svg>',
|
|
496
518
|
x:'<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>',
|
|
@@ -581,9 +603,23 @@ function tbLines(err,src){ const out=new Set();
|
|
|
581
603
|
if(m[1].split('/').pop()===base){ const ln=+m[2];
|
|
582
604
|
if(ln>=src.line_start&&ln<=src.line_end) out.add(ln); } }
|
|
583
605
|
return out; }
|
|
606
|
+
const PY_TOKEN=/(#.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(@\w+)|\b(def|class|return|if|elif|else|for|while|try|except|finally|raise|with|as|import|from|lambda|pass|break|continue|and|or|not|in|is|None|True|False|global|nonlocal|yield|async|await|del|assert|self)\b|(\b\d+(?:\.\d+)?\b)/g;
|
|
607
|
+
function pyHi(line){ let out='',last=0,defNext=false;
|
|
608
|
+
for(const m of line.matchAll(PY_TOKEN)){
|
|
609
|
+
out+=esc(line.slice(last,m.index)); last=m.index+m[0].length;
|
|
610
|
+
const t=esc(m[0]);
|
|
611
|
+
if(m[1]) out+=`<span class="py-c">${t}</span>`;
|
|
612
|
+
else if(m[2]) out+=`<span class="py-s">${t}</span>`;
|
|
613
|
+
else if(m[3]) out+=`<span class="py-d">${t}</span>`;
|
|
614
|
+
else if(m[4]) out+=`<span class="py-k">${t}</span>`;
|
|
615
|
+
else out+=`<span class="py-n">${t}</span>`;
|
|
616
|
+
}
|
|
617
|
+
out+=esc(line.slice(last));
|
|
618
|
+
// bold the name right after def/class
|
|
619
|
+
return out.replace(/(<span class="py-k">(?:def|class)<\/span> )([A-Za-z_]\w*)/,'$1<span class="py-f">$2</span>'); }
|
|
584
620
|
function codeBlock(src,hi){ const lines=src.code.replace(/\n$/,'').split('\n');
|
|
585
621
|
return `<div class="code">${lines.map((l,i)=>{ const ln=src.line_start+i, h=hi.has(ln);
|
|
586
|
-
return `<span class="ln ${h?'errline':''}">${ln}</span><span class="cl ${h?'errline':''}">${
|
|
622
|
+
return `<span class="ln ${h?'errline':''}">${ln}</span><span class="cl ${h?'errline':''}">${pyHi(l)||' '}</span>`;
|
|
587
623
|
}).join('')}</div>`; }
|
|
588
624
|
|
|
589
625
|
/* ---------- node pane (tap a node on the graph) ---------- */
|
|
@@ -653,9 +689,10 @@ function switchView(v){ VIEW=v;
|
|
|
653
689
|
if(location.hash!=='#'+v) history.replaceState(null,'','#'+v);
|
|
654
690
|
$$('.nav button,.bottom-nav button').forEach(b=>b.classList.toggle('on',b.dataset.v===v));
|
|
655
691
|
$('#crumb').textContent=v[0].toUpperCase()+v.slice(1);
|
|
656
|
-
for(const k of ['graph','runs','sessions','stats'])
|
|
692
|
+
for(const k of ['graph','runs','sessions','memory','stats'])
|
|
657
693
|
$('#view-'+k).style.display=v===k?'':'none';
|
|
658
|
-
if(v==='runs')loadRuns(); if(v==='sessions')loadSessions(); if(v==='
|
|
694
|
+
if(v==='runs')loadRuns(); if(v==='sessions')loadSessions(); if(v==='memory')loadMemory();
|
|
695
|
+
if(v==='stats'){loadStats();loadDatasets();}
|
|
659
696
|
if(cy&&v==='graph')setTimeout(()=>cy.resize(),40);
|
|
660
697
|
}
|
|
661
698
|
$$('.nav button,.bottom-nav button').forEach(b=>b.onclick=()=>switchView(b.dataset.v));
|
|
@@ -680,6 +717,7 @@ async function runNow(input){
|
|
|
680
717
|
for(const p of ps){ const ev=(p.match(/event: (.*)/)||[])[1],dm=(p.match(/data: (.*)/s)||[])[1]; if(!ev)continue; const d=dm?JSON.parse(dm):{};
|
|
681
718
|
if(ev==='node'){ cy.$id(prev).addClass('done').removeClass('running'); cy.$id(d.node).addClass('running');
|
|
682
719
|
cy.edges(`[source="${prev}"][target="${d.node}"]`).addClass('flow'); prev=d.node; }
|
|
720
|
+
else if(ev==='progress'){ toast('progress: '+JSON.stringify(d.data).slice(0,120)); }
|
|
683
721
|
else if(ev==='interrupt'){ toast('Run paused at an interrupt — awaiting resume'); }
|
|
684
722
|
else if(ev==='interrupted'){ cy.$id(prev).addClass('running'); toast('Run interrupted — see Runs for the payload'); }
|
|
685
723
|
else if(ev==='done'){ cy.$id(prev).addClass('done').removeClass('running'); cy.$id('__end__').addClass('done'); toast('Run complete — saved to Runs'); }
|
|
@@ -823,6 +861,7 @@ function renderSpans(list,byParent,total,depth){
|
|
|
823
861
|
const kids=byParent[s.id]||[];
|
|
824
862
|
const left=((s.offset_ms||0)/total*100).toFixed(2), width=Math.max(1.2,(s.dur_ms||0)/total*100).toFixed(2);
|
|
825
863
|
const meta=[s.dur_ms!=null?s.dur_ms+'ms':null,
|
|
864
|
+
s.ttft_ms!=null?'ttft '+s.ttft_ms+'ms':null,
|
|
826
865
|
s.type==='llm'&&(s.prompt_tokens!=null||s.completion_tokens!=null)?fmtk((s.prompt_tokens||0)+(s.completion_tokens||0))+'t':null,
|
|
827
866
|
s.cost_usd!=null?'$'+s.cost_usd.toFixed(4):null].filter(Boolean).join(' · ');
|
|
828
867
|
const did='sp'+s.id;
|
|
@@ -830,12 +869,12 @@ function renderSpans(list,byParent,total,depth){
|
|
|
830
869
|
<div class="tr-row" onclick="tog('${did}',this)">
|
|
831
870
|
<div class="tr-main"><span class="indent" style="width:${depth*18}px"></span>
|
|
832
871
|
<span class="caret">▶</span><span class="chip ${s.type}">${s.type}</span>
|
|
833
|
-
<span class="tr-name ${s.status==='error'?'err':''}">${esc(s.name||s.type)}</span
|
|
872
|
+
<span class="tr-name ${s.status==='error'?'err':''}">${esc(s.name||s.type)}</span>${s.retries?`<span class="badge-retry">↻${s.retries}</span>`:''}</div>
|
|
834
873
|
<div class="tr-gantt"><div class="tr-bar ${s.type}" style="left:${left}%;width:${width}%"></div></div>
|
|
835
874
|
<div class="tr-meta">${meta}</div>
|
|
836
875
|
</div>
|
|
837
876
|
<div class="tr-detail" id="${did}">
|
|
838
|
-
${s.model?`<div class="kv"><div class="k">model</div><pre>${esc(s.model)}${s.prompt_tokens!=null?` · ${s.prompt_tokens} in / ${s.completion_tokens||0} out`:''}</pre></div>`:''}
|
|
877
|
+
${s.model?`<div class="kv"><div class="k">model</div><pre>${esc(s.model)}${s.prompt_tokens!=null?` · ${s.prompt_tokens} in / ${s.completion_tokens||0} out`:''}${s.usage_detail?' · '+esc(Object.entries(s.usage_detail).map(([k,v])=>`${v} ${k.replace('input_','').replace('output_','')}`).join(', ')):''}</pre></div>`:''}
|
|
839
878
|
${s.error?`<div class="kv"><div class="k">error${s.type==='node'?`<button class="copy" onclick="showSpanSource('${s.id}')">view source</button>`:''}</div><pre class="err">${esc(s.error)}</pre><div id="src-${s.id}"></div></div>`:''}
|
|
840
879
|
${s.input!=null?kv('input',s.input):''}
|
|
841
880
|
${s.output!=null?kv('output',s.output):''}
|
|
@@ -989,6 +1028,37 @@ function fmtk(n){ return n<1000?String(n):(n/1000).toFixed(1)+'k'; }
|
|
|
989
1028
|
function ago(ms){ const s=(Date.now()-ms)/1000; if(s<60)return Math.round(s)+'s ago'; if(s<3600)return Math.round(s/60)+'m ago'; if(s<86400)return Math.round(s/3600)+'h ago'; return Math.round(s/86400)+'d ago'; }
|
|
990
1029
|
let tT; function toast(m){ const t=$('#toast'); t.textContent=m; t.classList.add('show'); clearTimeout(tT); tT=setTimeout(()=>t.classList.remove('show'),2600); }
|
|
991
1030
|
|
|
1031
|
+
let MEM_NS=[];
|
|
1032
|
+
async function probeMemory(){
|
|
1033
|
+
try{ const r=await fetch('/api/memory/namespaces'); if(!r.ok) return;
|
|
1034
|
+
MEM_NS=(await r.json()).namespaces||[];
|
|
1035
|
+
document.getElementById('nav-mem').style.display='';
|
|
1036
|
+
const m=document.querySelector('.bottom-nav .nav-mem'); if(m)m.style.display='';
|
|
1037
|
+
}catch(e){}
|
|
1038
|
+
}
|
|
1039
|
+
async function loadMemory(){
|
|
1040
|
+
const sel=document.getElementById('mem-ns');
|
|
1041
|
+
try{ const r=await fetch('/api/memory/namespaces'); MEM_NS=(await r.json()).namespaces||[]; }catch(e){}
|
|
1042
|
+
const cur=sel.value;
|
|
1043
|
+
sel.innerHTML=MEM_NS.map(ns=>`<option value="${esc(ns.join('.'))}">${esc(ns.join(' / '))}</option>`).join('');
|
|
1044
|
+
if(cur) sel.value=cur;
|
|
1045
|
+
const ns=sel.value;
|
|
1046
|
+
const w=document.getElementById('mem-wrap');
|
|
1047
|
+
if(!ns){ w.innerHTML='<div class="empty"><div class="t">No memories yet</div>Nodes write with <span class="mono">get_store().put(namespace, key, value)</span> — items appear here.</div>'; return; }
|
|
1048
|
+
const q=document.getElementById('mem-q').value.trim();
|
|
1049
|
+
const p=new URLSearchParams({namespace:ns}); if(q)p.set('query',q);
|
|
1050
|
+
let d={items:[]}; try{ d=await(await fetch('/api/memory/items?'+p)).json(); }catch(e){}
|
|
1051
|
+
w.innerHTML=(d.items||[]).length?`<div style="overflow:auto"><table>
|
|
1052
|
+
<thead><tr><th>Key</th><th>Value</th><th class="r">Updated</th></tr></thead>
|
|
1053
|
+
<tbody>${d.items.map(i=>`<tr>
|
|
1054
|
+
<td class="mono" style="font-weight:600;white-space:nowrap">${esc(i.key)}</td>
|
|
1055
|
+
<td><pre style="margin:0;white-space:pre-wrap;word-break:break-word;font:12px/1.5 var(--mono);max-width:640px">${esc(JSON.stringify(i.value,null,1)).slice(0,600)}</pre></td>
|
|
1056
|
+
<td class="r" style="color:var(--muted);white-space:nowrap">${i.updated_at?esc(String(i.updated_at).slice(0,19).replace('T',' ')):'—'}</td></tr>`).join('')}</tbody></table></div>`
|
|
1057
|
+
:'<div class="empty">Nothing in this namespace'+(q?' matching that query':'')+'.</div>';
|
|
1058
|
+
}
|
|
1059
|
+
document.addEventListener('change',e=>{ if(e.target&&e.target.id==='mem-ns') loadMemory(); });
|
|
1060
|
+
let memQT; document.addEventListener('input',e=>{ if(e.target&&e.target.id==='mem-q'){ clearTimeout(memQT); memQT=setTimeout(loadMemory,300); } });
|
|
1061
|
+
|
|
992
1062
|
async function loadDatasets(){
|
|
993
1063
|
const w=document.getElementById('ds-wrap'); if(!w) return;
|
|
994
1064
|
let ds=[]; try{ ds=await(await fetch('/api/datasets')).json(); }catch(e){}
|
|
@@ -1007,7 +1077,7 @@ window.runDataset=async(id,name)=>{
|
|
|
1007
1077
|
toast('Eval started: '+d.items+' items → session '+d.session);
|
|
1008
1078
|
};
|
|
1009
1079
|
window.delDataset=async(id)=>{ await fetch('/api/datasets/'+id,{method:'DELETE'}); loadDatasets(); };
|
|
1010
|
-
loadGraph(); liveTopology();
|
|
1080
|
+
loadGraph(); liveTopology(); probeMemory();
|
|
1011
1081
|
/* deep links: #runs #sessions #stats #run=<id> */
|
|
1012
1082
|
(function(){ const h=location.hash.slice(1);
|
|
1013
1083
|
if(h==='runs'||h==='sessions'||h==='stats') switchView(h);
|
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
import json, os, sqlite3, threading, time, uuid
|
|
14
14
|
from typing import Any, Optional
|
|
15
15
|
|
|
16
|
-
SCHEMA_VERSION =
|
|
16
|
+
SCHEMA_VERSION = 6
|
|
17
17
|
_lock = threading.Lock()
|
|
18
18
|
|
|
19
19
|
|
|
@@ -68,6 +68,11 @@ class Store:
|
|
|
68
68
|
c.execute("ALTER TABLE runs ADD COLUMN bookmarked INTEGER DEFAULT 0")
|
|
69
69
|
if "thread_id" not in cols:
|
|
70
70
|
c.execute("ALTER TABLE runs ADD COLUMN thread_id TEXT")
|
|
71
|
+
scols = [r[1] for r in c.execute("PRAGMA table_info(spans)").fetchall()]
|
|
72
|
+
for col, typ in (("retries", "INTEGER"), ("ttft_ms", "INTEGER"),
|
|
73
|
+
("usage_detail", "TEXT")):
|
|
74
|
+
if col not in scols:
|
|
75
|
+
c.execute(f"ALTER TABLE spans ADD COLUMN {col} {typ}")
|
|
71
76
|
try:
|
|
72
77
|
c.execute("SELECT count(*) FROM json_each('[1]')")
|
|
73
78
|
self.has_json1 = True
|
|
@@ -133,15 +138,18 @@ class Store:
|
|
|
133
138
|
with _lock, self._conn() as c:
|
|
134
139
|
c.execute("""INSERT OR REPLACE INTO spans
|
|
135
140
|
(id,run_id,parent_id,seq,type,name,status,started_ms,ended_ms,offset_ms,
|
|
136
|
-
dur_ms,input,output,model,prompt_tokens,completion_tokens,cost_usd,error
|
|
137
|
-
|
|
141
|
+
dur_ms,input,output,model,prompt_tokens,completion_tokens,cost_usd,error,
|
|
142
|
+
retries,ttft_ms,usage_detail)
|
|
143
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
138
144
|
(s["id"], s["run_id"], s.get("parent_id"), s.get("seq", 0), s["type"],
|
|
139
145
|
s.get("name"), s.get("status", "ok"), s.get("started_ms"), s.get("ended_ms"),
|
|
140
146
|
s.get("offset_ms"), s.get("dur_ms"),
|
|
141
147
|
json.dumps(s.get("input"), default=str) if s.get("input") is not None else None,
|
|
142
148
|
json.dumps(s.get("output"), default=str) if s.get("output") is not None else None,
|
|
143
149
|
s.get("model"), s.get("prompt_tokens"), s.get("completion_tokens"),
|
|
144
|
-
s.get("cost_usd"), s.get("error")
|
|
150
|
+
s.get("cost_usd"), s.get("error"),
|
|
151
|
+
s.get("retries"), s.get("ttft_ms"),
|
|
152
|
+
json.dumps(s.get("usage_detail")) if s.get("usage_detail") else None))
|
|
145
153
|
if self.has_fts:
|
|
146
154
|
c.execute("DELETE FROM span_fts WHERE span_id=?", (s["id"],))
|
|
147
155
|
c.execute("INSERT INTO span_fts(text,span_id,run_id) VALUES(?,?,?)",
|
|
@@ -316,7 +324,7 @@ class Store:
|
|
|
316
324
|
for k in ("input", "tags"):
|
|
317
325
|
d[k] = json.loads(d[k]) if d.get(k) else None
|
|
318
326
|
for s in spans:
|
|
319
|
-
for k in ("input", "output"):
|
|
327
|
+
for k in ("input", "output", "usage_detail"):
|
|
320
328
|
s[k] = json.loads(s[k]) if s.get(k) else None
|
|
321
329
|
d["spans"] = spans
|
|
322
330
|
d["scores"] = scores
|
|
@@ -77,24 +77,31 @@ def _model_name(serialized: dict, kw: dict) -> str:
|
|
|
77
77
|
return ids[-1] if ids else "llm"
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def _usage(response: Any) -> tuple[Optional[int], Optional[int]]:
|
|
80
|
+
def _usage(response: Any) -> tuple[Optional[int], Optional[int], Optional[dict]]:
|
|
81
|
+
"""(prompt, completion, detail) — detail carries cache_read/cache_creation/
|
|
82
|
+
reasoning token counts when the provider reports them."""
|
|
81
83
|
# OpenAI-style aggregate
|
|
82
84
|
out = getattr(response, "llm_output", None) or {}
|
|
83
85
|
for key in ("token_usage", "usage"):
|
|
84
86
|
u = out.get(key) if isinstance(out, dict) else None
|
|
85
87
|
if u:
|
|
86
88
|
return (u.get("prompt_tokens") or u.get("input_tokens"),
|
|
87
|
-
u.get("completion_tokens") or u.get("output_tokens"))
|
|
89
|
+
u.get("completion_tokens") or u.get("output_tokens"), None)
|
|
88
90
|
# per-generation usage_metadata (chat models)
|
|
89
91
|
try:
|
|
90
92
|
for gens in response.generations:
|
|
91
93
|
for g in gens:
|
|
92
94
|
um = getattr(getattr(g, "message", None), "usage_metadata", None)
|
|
93
95
|
if um:
|
|
94
|
-
|
|
96
|
+
detail = {}
|
|
97
|
+
for side in ("input_token_details", "output_token_details"):
|
|
98
|
+
for k, v in (um.get(side) or {}).items():
|
|
99
|
+
if v:
|
|
100
|
+
detail[f"{side.split('_')[0]}_{k}"] = v
|
|
101
|
+
return um.get("input_tokens"), um.get("output_tokens"), detail or None
|
|
95
102
|
except Exception:
|
|
96
103
|
pass
|
|
97
|
-
return None, None
|
|
104
|
+
return None, None, None
|
|
98
105
|
|
|
99
106
|
|
|
100
107
|
def _gen_text(response: Any) -> str:
|
|
@@ -142,7 +149,7 @@ class SpanBuilder(BaseCallbackHandler):
|
|
|
142
149
|
return int((t - (self._t0 or t)) * 1000)
|
|
143
150
|
|
|
144
151
|
def _finish_span(self, info: dict, *, output=None, status="ok", error=None,
|
|
145
|
-
model=None, pt=None, ct=None):
|
|
152
|
+
model=None, pt=None, ct=None, usage_detail=None):
|
|
146
153
|
now = time.time()
|
|
147
154
|
sid = info["span_id"]
|
|
148
155
|
self._emit({"kind": "span", "id": sid, "run_id": self.run_id,
|
|
@@ -153,7 +160,10 @@ class SpanBuilder(BaseCallbackHandler):
|
|
|
153
160
|
"dur_ms": int((now - info["t"]) * 1000),
|
|
154
161
|
"input": info.get("input"), "output": output, "model": model,
|
|
155
162
|
"prompt_tokens": pt, "completion_tokens": ct,
|
|
156
|
-
"cost_usd": cost_of(model, pt, ct), "error": error
|
|
163
|
+
"cost_usd": cost_of(model, pt, ct), "error": error,
|
|
164
|
+
"retries": info.get("retries"),
|
|
165
|
+
"ttft_ms": int(info["ttft"] * 1000) if info.get("ttft") is not None else None,
|
|
166
|
+
"usage_detail": usage_detail})
|
|
157
167
|
self._seq += 1
|
|
158
168
|
|
|
159
169
|
# -- chains / nodes --
|
|
@@ -235,15 +245,41 @@ class SpanBuilder(BaseCallbackHandler):
|
|
|
235
245
|
info = self._open.pop(run_id, None)
|
|
236
246
|
if not info:
|
|
237
247
|
return
|
|
238
|
-
pt, ct = _usage(response)
|
|
248
|
+
pt, ct, detail = _usage(response)
|
|
239
249
|
self._finish_span(info, output=_gen_text(response)[:4000],
|
|
240
|
-
model=info["name"], pt=pt, ct=ct)
|
|
250
|
+
model=info["name"], pt=pt, ct=ct, usage_detail=detail)
|
|
241
251
|
|
|
242
252
|
def on_llm_error(self, error, *, run_id=None, **kw):
|
|
243
253
|
info = self._open.pop(run_id, None)
|
|
244
254
|
if info:
|
|
245
255
|
self._finish_span(info, status="error", error=_err_text(error), model=info["name"])
|
|
246
256
|
|
|
257
|
+
def on_llm_new_token(self, token, *, run_id=None, **kw):
|
|
258
|
+
# streaming: first token stamps time-to-first-token
|
|
259
|
+
info = self._open.get(run_id)
|
|
260
|
+
if info is not None and "ttft" not in info:
|
|
261
|
+
info["ttft"] = time.time() - info["t"]
|
|
262
|
+
|
|
263
|
+
# -- retries (tenacity, e.g. rate limits) --
|
|
264
|
+
def on_retry(self, retry_state, *, run_id=None, **kw):
|
|
265
|
+
info = self._open.get(run_id)
|
|
266
|
+
if info is not None:
|
|
267
|
+
info["retries"] = getattr(retry_state, "attempt_number", None) or \
|
|
268
|
+
(info.get("retries") or 0) + 1
|
|
269
|
+
|
|
270
|
+
# -- custom events (langchain dispatch_custom_event) --
|
|
271
|
+
def on_custom_event(self, name, data, *, run_id=None, **kw):
|
|
272
|
+
now = time.time()
|
|
273
|
+
self._emit({"kind": "span", "id": uuid.uuid4().hex[:12], "run_id": self.run_id,
|
|
274
|
+
"parent_id": self._span_of.get(run_id),
|
|
275
|
+
"seq": self._seq, "type": "event", "name": str(name), "status": "ok",
|
|
276
|
+
"started_ms": int(now * 1000), "ended_ms": int(now * 1000),
|
|
277
|
+
"offset_ms": self._rel(now), "dur_ms": 0,
|
|
278
|
+
"input": None, "output": _trunc(data), "model": None,
|
|
279
|
+
"prompt_tokens": None, "completion_tokens": None,
|
|
280
|
+
"cost_usd": None, "error": None})
|
|
281
|
+
self._seq += 1
|
|
282
|
+
|
|
247
283
|
# -- retrievers (LangChain RAG) --
|
|
248
284
|
def on_retriever_start(self, serialized, query, *, run_id=None, parent_run_id=None, **kw):
|
|
249
285
|
sid = uuid.uuid4().hex[:12]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|