by-framework-langgraph 0.0.3.dev1__tar.gz → 0.0.3.dev3__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.
- {by_framework_langgraph-0.0.3.dev1 → by_framework_langgraph-0.0.3.dev3}/.gitignore +0 -2
- {by_framework_langgraph-0.0.3.dev1 → by_framework_langgraph-0.0.3.dev3}/PKG-INFO +1 -1
- {by_framework_langgraph-0.0.3.dev1 → by_framework_langgraph-0.0.3.dev3}/pyproject.toml +1 -1
- {by_framework_langgraph-0.0.3.dev1 → by_framework_langgraph-0.0.3.dev3}/src/by_framework_langgraph/adapter.py +57 -57
- {by_framework_langgraph-0.0.3.dev1 → by_framework_langgraph-0.0.3.dev3}/tests/test_adapter.py +89 -61
- {by_framework_langgraph-0.0.3.dev1 → by_framework_langgraph-0.0.3.dev3}/README.md +0 -0
- {by_framework_langgraph-0.0.3.dev1 → by_framework_langgraph-0.0.3.dev3}/src/by_framework_langgraph/__init__.py +0 -0
- {by_framework_langgraph-0.0.3.dev1 → by_framework_langgraph-0.0.3.dev3}/src/by_framework_langgraph/_utils.py +0 -0
- {by_framework_langgraph-0.0.3.dev1 → by_framework_langgraph-0.0.3.dev3}/src/by_framework_langgraph/tools.py +0 -0
- {by_framework_langgraph-0.0.3.dev1 → by_framework_langgraph-0.0.3.dev3}/src/by_framework_langgraph/worker.py +0 -0
- {by_framework_langgraph-0.0.3.dev1 → by_framework_langgraph-0.0.3.dev3}/tests/test_tools.py +0 -0
- {by_framework_langgraph-0.0.3.dev1 → by_framework_langgraph-0.0.3.dev3}/tests/test_utils.py +0 -0
|
@@ -525,6 +525,7 @@ class LangGraphAdapter:
|
|
|
525
525
|
self._context, "parent_message_id", ""
|
|
526
526
|
),
|
|
527
527
|
"by_framework_agent_id": getattr(self._context, "current_agent_id", ""),
|
|
528
|
+
"worker_id": getattr(self._context, "worker_id", ""),
|
|
528
529
|
"langgraph_thread_id": self._thread_id,
|
|
529
530
|
}
|
|
530
531
|
return {
|
|
@@ -538,10 +539,31 @@ class LangGraphAdapter:
|
|
|
538
539
|
|
|
539
540
|
with (
|
|
540
541
|
self._phoenix_context_manager(),
|
|
542
|
+
self._langfuse_attribute_propagation_context_manager(),
|
|
541
543
|
self._langfuse_callback_manager(callbacks),
|
|
542
544
|
):
|
|
543
545
|
yield callbacks
|
|
544
546
|
|
|
547
|
+
@contextmanager
|
|
548
|
+
def _langfuse_attribute_propagation_context_manager(self) -> Iterator[None]:
|
|
549
|
+
"""Propagate stable framework metadata to Langfuse child observations."""
|
|
550
|
+
worker_id = str(getattr(self._context, "worker_id", "") or "")
|
|
551
|
+
if not worker_id:
|
|
552
|
+
yield
|
|
553
|
+
return
|
|
554
|
+
|
|
555
|
+
try:
|
|
556
|
+
propagate_attributes = getattr(
|
|
557
|
+
import_module("langfuse"),
|
|
558
|
+
"propagate_attributes",
|
|
559
|
+
)
|
|
560
|
+
except (ImportError, AttributeError):
|
|
561
|
+
yield
|
|
562
|
+
return
|
|
563
|
+
|
|
564
|
+
with propagate_attributes(metadata={"worker_id": worker_id}):
|
|
565
|
+
yield
|
|
566
|
+
|
|
545
567
|
@contextmanager
|
|
546
568
|
def _phoenix_context_manager(self) -> Iterator[None]:
|
|
547
569
|
"""Prepare OpenTelemetry context for Phoenix tracing."""
|
|
@@ -590,69 +612,47 @@ class LangGraphAdapter:
|
|
|
590
612
|
# Always inject token accumulator — works regardless of Langfuse config.
|
|
591
613
|
callbacks.append(_TokenAccumulatingCallbackHandler(self._context))
|
|
592
614
|
|
|
593
|
-
# Prefer AgentContext's callback factory so trace and parent ids align.
|
|
594
|
-
# Filter out auto-generated MagicMock attributes when tests use a mock
|
|
595
|
-
# context — real callback objects always come from a non-test module.
|
|
596
|
-
langfuse_callback_value = getattr(self._context, "langfuse_callback", None)
|
|
597
|
-
is_real_callback = langfuse_callback_value is not None and type(
|
|
598
|
-
langfuse_callback_value
|
|
599
|
-
).__module__ not in ("unittest.mock",)
|
|
600
|
-
|
|
601
|
-
if is_real_callback:
|
|
602
|
-
handler = (
|
|
603
|
-
langfuse_callback_value()
|
|
604
|
-
if callable(langfuse_callback_value)
|
|
605
|
-
else langfuse_callback_value
|
|
606
|
-
)
|
|
607
|
-
if handler is not None:
|
|
608
|
-
callbacks.append(handler)
|
|
609
|
-
yield
|
|
610
|
-
return
|
|
611
|
-
|
|
612
|
-
# Fallback to local import if context method is missing
|
|
613
|
-
# pylint: disable=import-outside-toplevel
|
|
614
615
|
try:
|
|
615
|
-
|
|
616
|
-
"by_framework_trace_langfuse"
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
raise ImportError("Langfuse not configured")
|
|
620
|
-
|
|
621
|
-
callback_handler = import_module("langfuse.langchain").CallbackHandler
|
|
622
|
-
get_client = import_module("langfuse").get_client
|
|
616
|
+
build_langchain_callback = getattr(
|
|
617
|
+
import_module("by_framework_trace_langfuse"),
|
|
618
|
+
"build_langchain_callback",
|
|
619
|
+
)
|
|
623
620
|
except (ImportError, AttributeError):
|
|
624
621
|
yield
|
|
625
622
|
return
|
|
626
623
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
624
|
+
get_parent_observation_id = getattr(
|
|
625
|
+
self._context,
|
|
626
|
+
"get_trace_parent_observation_id",
|
|
627
|
+
None,
|
|
628
|
+
)
|
|
629
|
+
parent_observation_id = (
|
|
630
|
+
str(get_parent_observation_id() or "")
|
|
631
|
+
if callable(get_parent_observation_id)
|
|
632
|
+
else ""
|
|
633
|
+
)
|
|
634
|
+
if not parent_observation_id:
|
|
635
|
+
framework_observation = getattr(
|
|
636
|
+
self._context, LANGFUSE_OBSERVATION_ATTR, None
|
|
637
|
+
)
|
|
638
|
+
parent_observation_id = getattr(framework_observation, "id", "") or ""
|
|
639
|
+
if not parent_observation_id:
|
|
640
|
+
execution_id = getattr(self._context, "execution_id", "")
|
|
641
|
+
message_id = getattr(self._context, "message_id", "")
|
|
642
|
+
raw_parent_id = (
|
|
643
|
+
f"{execution_id}:worker.execute"
|
|
644
|
+
if execution_id
|
|
645
|
+
else f"{message_id}:worker.execute"
|
|
646
|
+
)
|
|
647
|
+
parent_observation_id = f"{str_to_uint64(raw_parent_id):016x}"
|
|
633
648
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
},
|
|
642
|
-
metadata=self._default_metadata(),
|
|
643
|
-
):
|
|
644
|
-
# Prevent the generated OTel span from being promoted to a trace root.
|
|
645
|
-
# The native LangfusePlugin sets the same attribute on its own path
|
|
646
|
-
# (via _SdkLangfuseTracer); this covers the LangGraph fallback path.
|
|
647
|
-
try:
|
|
648
|
-
from opentelemetry import trace
|
|
649
|
-
|
|
650
|
-
current_span = trace.get_current_span()
|
|
651
|
-
if current_span and hasattr(current_span, "set_attribute"):
|
|
652
|
-
current_span.set_attribute("langfuse.internal.as_root", False)
|
|
653
|
-
except Exception: # pylint: disable=broad-exception-caught
|
|
654
|
-
pass
|
|
655
|
-
yield
|
|
649
|
+
handler = build_langchain_callback(
|
|
650
|
+
trace_id=getattr(self._context, "trace_id", ""),
|
|
651
|
+
parent_observation_id=parent_observation_id,
|
|
652
|
+
)
|
|
653
|
+
if handler is not None:
|
|
654
|
+
callbacks.append(handler)
|
|
655
|
+
yield
|
|
656
656
|
|
|
657
657
|
@staticmethod
|
|
658
658
|
def _default_input_mapper(content: str) -> dict:
|
{by_framework_langgraph-0.0.3.dev1 → by_framework_langgraph-0.0.3.dev3}/tests/test_adapter.py
RENAMED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Tests for adapter and worker modules."""
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
-
from contextlib import contextmanager
|
|
5
4
|
from types import SimpleNamespace
|
|
6
5
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
7
6
|
|
|
@@ -24,6 +23,7 @@ def _make_mock_context(session_id: str = "test-session"):
|
|
|
24
23
|
ctx.message_id = "msg-ctx"
|
|
25
24
|
ctx.parent_message_id = "parent-ctx"
|
|
26
25
|
ctx.current_agent_id = "planner"
|
|
26
|
+
ctx.worker_id = "worker-langgraph-1"
|
|
27
27
|
ctx.redis = AsyncMock()
|
|
28
28
|
ctx.emit_chunk = AsyncMock()
|
|
29
29
|
ctx.ask_user = AsyncMock()
|
|
@@ -40,7 +40,7 @@ def _make_mock_context(session_id: str = "test-session"):
|
|
|
40
40
|
metadata={"source": "test"},
|
|
41
41
|
)
|
|
42
42
|
)
|
|
43
|
-
ctx.
|
|
43
|
+
ctx.get_trace_parent_observation_id.return_value = "obs-framework"
|
|
44
44
|
return ctx
|
|
45
45
|
|
|
46
46
|
|
|
@@ -187,7 +187,7 @@ class TestAdapterRun:
|
|
|
187
187
|
async def test_includes_langfuse_callbacks_and_parent_trace_context(
|
|
188
188
|
self, monkeypatch
|
|
189
189
|
):
|
|
190
|
-
"""Verify adapter
|
|
190
|
+
"""Verify adapter gets Langfuse callback from the trace provider package."""
|
|
191
191
|
ctx = _make_mock_context()
|
|
192
192
|
graph = MagicMock()
|
|
193
193
|
graph.ainvoke = AsyncMock(
|
|
@@ -197,45 +197,18 @@ class TestAdapterRun:
|
|
|
197
197
|
snapshot.next = ()
|
|
198
198
|
graph.get_state.return_value = snapshot
|
|
199
199
|
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
callback_handler = object()
|
|
201
|
+
callback_calls: list[dict[str, str]] = []
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
yield SimpleNamespace(id="obs-langgraph")
|
|
207
|
-
|
|
208
|
-
class FakeCallbackHandler: # pylint: disable=too-few-public-methods
|
|
209
|
-
"""Minimal callback handler stub for adapter config assertions."""
|
|
210
|
-
|
|
211
|
-
def __init__(self):
|
|
212
|
-
callback_instances.append(self)
|
|
213
|
-
|
|
214
|
-
fake_langfuse_client = SimpleNamespace(
|
|
215
|
-
start_as_current_observation=fake_observation_scope
|
|
216
|
-
)
|
|
203
|
+
def fake_build_langchain_callback(**kwargs):
|
|
204
|
+
callback_calls.append(kwargs)
|
|
205
|
+
return callback_handler
|
|
217
206
|
|
|
218
|
-
monkeypatch.setitem(
|
|
219
|
-
sys.modules,
|
|
220
|
-
"langfuse",
|
|
221
|
-
SimpleNamespace(get_client=MagicMock(return_value=fake_langfuse_client)),
|
|
222
|
-
)
|
|
223
|
-
monkeypatch.setitem(
|
|
224
|
-
sys.modules,
|
|
225
|
-
"langfuse.langchain",
|
|
226
|
-
SimpleNamespace(CallbackHandler=FakeCallbackHandler),
|
|
227
|
-
)
|
|
228
207
|
monkeypatch.setitem(
|
|
229
208
|
sys.modules,
|
|
230
209
|
"by_framework_trace_langfuse",
|
|
231
|
-
SimpleNamespace(
|
|
210
|
+
SimpleNamespace(build_langchain_callback=fake_build_langchain_callback),
|
|
232
211
|
)
|
|
233
|
-
sys.modules[
|
|
234
|
-
"by_framework_trace_langfuse"
|
|
235
|
-
].LangfuseConfig.from_env.return_value = object()
|
|
236
|
-
monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-test")
|
|
237
|
-
monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-test")
|
|
238
|
-
monkeypatch.setenv("LANGFUSE_BASE_URL", "http://localhost:3000")
|
|
239
212
|
|
|
240
213
|
adapter = LangGraphAdapter(graph, ctx, stream=False)
|
|
241
214
|
|
|
@@ -253,27 +226,86 @@ class TestAdapterRun:
|
|
|
253
226
|
assert config["metadata"]["langfuse_session_id"] == "test-session"
|
|
254
227
|
assert config["metadata"]["langfuse_user_id"] == "user-1"
|
|
255
228
|
assert config["metadata"]["by_framework_message_id"] == "msg-ctx"
|
|
229
|
+
assert config["metadata"]["worker_id"] == "worker-langgraph-1"
|
|
256
230
|
assert config["metadata"]["langgraph_thread_id"] == "test-session"
|
|
257
231
|
# Callbacks list includes the Langfuse handler(s) plus the token accumulator.
|
|
258
|
-
assert
|
|
232
|
+
assert callback_handler in config["callbacks"]
|
|
259
233
|
assert any(
|
|
260
234
|
type(cb).__name__ == "_TokenAccumulatingCallbackHandler"
|
|
261
235
|
for cb in config["callbacks"]
|
|
262
236
|
)
|
|
263
|
-
assert
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
237
|
+
assert callback_calls == [
|
|
238
|
+
{
|
|
239
|
+
"trace_id": "trace-ctx",
|
|
240
|
+
"parent_observation_id": "obs-framework",
|
|
241
|
+
}
|
|
242
|
+
]
|
|
269
243
|
|
|
270
244
|
@pytest.mark.asyncio
|
|
271
|
-
async def
|
|
272
|
-
|
|
273
|
-
|
|
245
|
+
async def test_propagates_worker_id_to_langfuse_child_observations(
|
|
246
|
+
self, monkeypatch
|
|
247
|
+
):
|
|
248
|
+
"""Langfuse attribute propagation covers nested LangGraph observations."""
|
|
249
|
+
ctx = _make_mock_context()
|
|
250
|
+
graph = MagicMock()
|
|
251
|
+
snapshot = MagicMock()
|
|
252
|
+
snapshot.next = ()
|
|
253
|
+
graph.get_state.return_value = snapshot
|
|
254
|
+
|
|
255
|
+
propagation_events: list[tuple[str, dict[str, str]]] = []
|
|
256
|
+
propagation_active = False
|
|
257
|
+
|
|
258
|
+
class FakePropagation:
|
|
259
|
+
|
|
260
|
+
def __init__(self, metadata):
|
|
261
|
+
self.metadata = metadata
|
|
262
|
+
|
|
263
|
+
def __enter__(self):
|
|
264
|
+
nonlocal propagation_active
|
|
265
|
+
propagation_active = True
|
|
266
|
+
propagation_events.append(("enter", self.metadata))
|
|
267
|
+
|
|
268
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
269
|
+
nonlocal propagation_active
|
|
270
|
+
propagation_events.append(("exit", self.metadata))
|
|
271
|
+
propagation_active = False
|
|
272
|
+
|
|
273
|
+
def fake_propagate_attributes(**kwargs):
|
|
274
|
+
return FakePropagation(metadata=kwargs["metadata"])
|
|
275
|
+
|
|
276
|
+
async def fake_ainvoke(input_data, *, config):
|
|
277
|
+
del input_data
|
|
278
|
+
assert config["metadata"]["worker_id"] == "worker-langgraph-1"
|
|
279
|
+
assert propagation_active is True
|
|
280
|
+
return {"messages": [MagicMock(content="hello")]}
|
|
281
|
+
|
|
282
|
+
graph.ainvoke = fake_ainvoke
|
|
283
|
+
monkeypatch.setitem(
|
|
284
|
+
sys.modules,
|
|
285
|
+
"langfuse",
|
|
286
|
+
SimpleNamespace(propagate_attributes=fake_propagate_attributes),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
adapter = LangGraphAdapter(graph, ctx, stream=False)
|
|
290
|
+
|
|
291
|
+
result = await adapter.run(
|
|
292
|
+
AskAgentCommand(header=_make_header(), content="write a poem")
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
assert result == "hello"
|
|
296
|
+
assert propagation_events == [
|
|
297
|
+
("enter", {"worker_id": "worker-langgraph-1"}),
|
|
298
|
+
("exit", {"worker_id": "worker-langgraph-1"}),
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
@pytest.mark.asyncio
|
|
302
|
+
async def test_skips_langfuse_callback_when_provider_package_missing(
|
|
303
|
+
self, monkeypatch
|
|
304
|
+
):
|
|
305
|
+
"""LangGraph stays provider-agnostic when trace-langfuse is not installed."""
|
|
274
306
|
|
|
275
307
|
# pylint: disable=too-few-public-methods,missing-class-docstring,missing-function-docstring
|
|
276
|
-
class
|
|
308
|
+
class ContextWithoutProvider:
|
|
277
309
|
session_id = "test-session"
|
|
278
310
|
trace_id = "trace-ctx"
|
|
279
311
|
message_id = "msg-ctx"
|
|
@@ -291,11 +323,15 @@ class TestAdapterRun:
|
|
|
291
323
|
def __init__(self):
|
|
292
324
|
self.emit_chunk = AsyncMock()
|
|
293
325
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
326
|
+
def fake_import_module(name):
|
|
327
|
+
if name == "by_framework_trace_langfuse":
|
|
328
|
+
raise ImportError(name)
|
|
329
|
+
raise AssertionError(f"unexpected import: {name}")
|
|
297
330
|
|
|
298
|
-
|
|
331
|
+
monkeypatch.setattr(
|
|
332
|
+
"by_framework_langgraph.adapter.import_module", fake_import_module
|
|
333
|
+
)
|
|
334
|
+
ctx = ContextWithoutProvider()
|
|
299
335
|
graph = MagicMock()
|
|
300
336
|
graph.ainvoke = AsyncMock(
|
|
301
337
|
return_value={"messages": [MagicMock(content="hello")]}
|
|
@@ -310,7 +346,6 @@ class TestAdapterRun:
|
|
|
310
346
|
assert result == "hello"
|
|
311
347
|
_, kwargs = graph.ainvoke.call_args
|
|
312
348
|
callbacks = kwargs["config"]["callbacks"]
|
|
313
|
-
assert handler in callbacks
|
|
314
349
|
assert any(
|
|
315
350
|
type(cb).__name__ == "_TokenAccumulatingCallbackHandler" for cb in callbacks
|
|
316
351
|
)
|
|
@@ -335,11 +370,8 @@ class TestAdapterRun:
|
|
|
335
370
|
monkeypatch.setitem(
|
|
336
371
|
sys.modules,
|
|
337
372
|
"by_framework_trace_langfuse",
|
|
338
|
-
SimpleNamespace(
|
|
373
|
+
SimpleNamespace(build_langchain_callback=MagicMock(return_value=None)),
|
|
339
374
|
)
|
|
340
|
-
sys.modules[
|
|
341
|
-
"by_framework_trace_langfuse"
|
|
342
|
-
].LangfuseConfig.from_env.return_value = None
|
|
343
375
|
|
|
344
376
|
adapter = LangGraphAdapter(graph, ctx, stream=False)
|
|
345
377
|
|
|
@@ -382,11 +414,8 @@ class TestAdapterRun:
|
|
|
382
414
|
monkeypatch.setitem(
|
|
383
415
|
sys.modules,
|
|
384
416
|
"by_framework_trace_langfuse",
|
|
385
|
-
SimpleNamespace(
|
|
417
|
+
SimpleNamespace(build_langchain_callback=MagicMock(return_value=None)),
|
|
386
418
|
)
|
|
387
|
-
sys.modules[
|
|
388
|
-
"by_framework_trace_langfuse"
|
|
389
|
-
].LangfuseConfig.from_env.return_value = None
|
|
390
419
|
|
|
391
420
|
adapter = LangGraphAdapter(graph, ctx, stream=False)
|
|
392
421
|
|
|
@@ -530,7 +559,6 @@ class TestTokenAccumulatingCallbackHandler:
|
|
|
530
559
|
def test_callback_injected_in_tracing_scope(self):
|
|
531
560
|
"""_langfuse_callback_manager injects _TokenAccumulatingCallbackHandler."""
|
|
532
561
|
ctx = MagicMock()
|
|
533
|
-
ctx.langfuse_callback = None
|
|
534
562
|
graph = MagicMock()
|
|
535
563
|
adapter = LangGraphAdapter(graph=graph, context=ctx)
|
|
536
564
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|