langfuse-haystack 2.2.1__tar.gz → 2.3.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.
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/CHANGELOG.md +7 -0
- langfuse_haystack-2.3.0/PKG-INFO +45 -0
- langfuse_haystack-2.3.0/README.md +21 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/src/haystack_integrations/tracing/langfuse/tracer.py +19 -8
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/tests/test_tracer.py +100 -75
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/tests/test_tracing.py +65 -0
- langfuse_haystack-2.2.1/PKG-INFO +0 -154
- langfuse_haystack-2.2.1/README.md +0 -130
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/.gitignore +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/LICENSE.txt +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/example/basic_rag.py +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/example/chat.py +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/example/requirements.txt +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/pydoc/config.yml +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/pyproject.toml +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/src/haystack_integrations/components/connectors/__init__.py +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/tests/__init__.py +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/tests/test_langfuse_connector.py +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langfuse-haystack
|
|
3
|
+
Version: 2.3.0
|
|
4
|
+
Summary: Langfuse integration for Haystack
|
|
5
|
+
Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse#readme
|
|
6
|
+
Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
|
|
7
|
+
Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse
|
|
8
|
+
Author-email: deepset GmbH <info@deepset.ai>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE.txt
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: haystack-ai>=2.15.1
|
|
22
|
+
Requires-Dist: langfuse<3.0.0,>=2.9.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# langfuse-haystack
|
|
26
|
+
|
|
27
|
+
[](https://pypi.org/project/langfuse-haystack)
|
|
28
|
+
[](https://pypi.org/project/langfuse-haystack)
|
|
29
|
+
|
|
30
|
+
- [Integration page](https://haystack.deepset.ai/integrations/langfuse)
|
|
31
|
+
- [Changelog](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/langfuse/CHANGELOG.md)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Contributing
|
|
36
|
+
|
|
37
|
+
Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
|
|
38
|
+
|
|
39
|
+
To run integration tests locally, you need to export the following environment variables:
|
|
40
|
+
|
|
41
|
+
- `LANGFUSE_SECRET_KEY`
|
|
42
|
+
- `LANGFUSE_PUBLIC_KEY`
|
|
43
|
+
- `OPENAI_API_KEY`
|
|
44
|
+
- `ANTHROPIC_API_KEY`
|
|
45
|
+
- `COHERE_API_KEY`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# langfuse-haystack
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/langfuse-haystack)
|
|
4
|
+
[](https://pypi.org/project/langfuse-haystack)
|
|
5
|
+
|
|
6
|
+
- [Integration page](https://haystack.deepset.ai/integrations/langfuse)
|
|
7
|
+
- [Changelog](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/langfuse/CHANGELOG.md)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Contributing
|
|
12
|
+
|
|
13
|
+
Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
|
|
14
|
+
|
|
15
|
+
To run integration tests locally, you need to export the following environment variables:
|
|
16
|
+
|
|
17
|
+
- `LANGFUSE_SECRET_KEY`
|
|
18
|
+
- `LANGFUSE_PUBLIC_KEY`
|
|
19
|
+
- `OPENAI_API_KEY`
|
|
20
|
+
- `ANTHROPIC_API_KEY`
|
|
21
|
+
- `COHERE_API_KEY`.
|
|
@@ -61,10 +61,14 @@ _COMPONENT_TYPE_KEY = "haystack.component.type"
|
|
|
61
61
|
_COMPONENT_OUTPUT_KEY = "haystack.component.output"
|
|
62
62
|
_COMPONENT_INPUT_KEY = "haystack.component.input"
|
|
63
63
|
|
|
64
|
-
#
|
|
65
|
-
#
|
|
64
|
+
# External session metadata for trace correlation (Haystack system)
|
|
65
|
+
# Stores trace_id, user_id, session_id, tags, version for root trace creation
|
|
66
66
|
tracing_context_var: ContextVar[Dict[Any, Any]] = ContextVar("tracing_context")
|
|
67
67
|
|
|
68
|
+
# Internal span execution hierarchy for our tracer
|
|
69
|
+
# Manages parent-child relationships and prevents cross-request span interleaving
|
|
70
|
+
span_stack_var: ContextVar[Optional[List["LangfuseSpan"]]] = ContextVar("span_stack", default=None)
|
|
71
|
+
|
|
68
72
|
|
|
69
73
|
class LangfuseSpan(Span):
|
|
70
74
|
"""
|
|
@@ -265,6 +269,7 @@ class DefaultSpanHandler(SpanHandler):
|
|
|
265
269
|
)
|
|
266
270
|
raise RuntimeError(message)
|
|
267
271
|
|
|
272
|
+
# Get external tracing context for root trace creation (correlation metadata)
|
|
268
273
|
tracing_ctx = tracing_context_var.get({})
|
|
269
274
|
if not context.parent_span:
|
|
270
275
|
# Create a new trace when there's no parent span
|
|
@@ -360,6 +365,7 @@ class LangfuseTracer(Tracer):
|
|
|
360
365
|
"before importing Haystack."
|
|
361
366
|
)
|
|
362
367
|
self._tracer = tracer
|
|
368
|
+
# Keep _context as deprecated shim to avoid AttributeError if anyone uses it
|
|
363
369
|
self._context: List[LangfuseSpan] = []
|
|
364
370
|
self._name = name
|
|
365
371
|
self._public = public
|
|
@@ -391,7 +397,12 @@ class LangfuseTracer(Tracer):
|
|
|
391
397
|
# Create span using the handler
|
|
392
398
|
span = self._span_handler.create_span(span_context)
|
|
393
399
|
|
|
394
|
-
|
|
400
|
+
# Build new span hierarchy: copy existing stack, add new span, save for restoration
|
|
401
|
+
prev_stack = span_stack_var.get()
|
|
402
|
+
new_stack = (prev_stack or []).copy()
|
|
403
|
+
new_stack.append(span)
|
|
404
|
+
token = span_stack_var.set(new_stack)
|
|
405
|
+
|
|
395
406
|
span.set_tags(tags)
|
|
396
407
|
|
|
397
408
|
try:
|
|
@@ -414,10 +425,8 @@ class LangfuseTracer(Tracer):
|
|
|
414
425
|
cleanup_error=cleanup_error,
|
|
415
426
|
)
|
|
416
427
|
finally:
|
|
417
|
-
#
|
|
418
|
-
|
|
419
|
-
if self._context and self._context[-1] == span:
|
|
420
|
-
self._context.pop()
|
|
428
|
+
# Restore previous span stack using saved token - ensures proper cleanup
|
|
429
|
+
span_stack_var.reset(token)
|
|
421
430
|
|
|
422
431
|
if self.enforce_flush:
|
|
423
432
|
self.flush()
|
|
@@ -431,7 +440,9 @@ class LangfuseTracer(Tracer):
|
|
|
431
440
|
|
|
432
441
|
:return: The current span if available, else None.
|
|
433
442
|
"""
|
|
434
|
-
|
|
443
|
+
# Get top of span stack (most recent span) from context-local storage
|
|
444
|
+
stack = span_stack_var.get()
|
|
445
|
+
return stack[-1] if stack else None
|
|
435
446
|
|
|
436
447
|
def get_trace_url(self) -> str:
|
|
437
448
|
"""
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import datetime
|
|
6
|
-
import json
|
|
7
7
|
import logging
|
|
8
8
|
import sys
|
|
9
|
+
import json
|
|
9
10
|
from typing import Optional
|
|
10
11
|
from unittest.mock import MagicMock, Mock, patch
|
|
11
12
|
|
|
@@ -13,40 +14,45 @@ import pytest
|
|
|
13
14
|
from haystack import Pipeline, component
|
|
14
15
|
from haystack.dataclasses import ChatMessage, ToolCall
|
|
15
16
|
|
|
16
|
-
from haystack_integrations.components.connectors.langfuse import LangfuseConnector
|
|
17
17
|
from haystack_integrations.tracing.langfuse.tracer import (
|
|
18
|
-
_COMPONENT_OUTPUT_KEY,
|
|
19
|
-
|
|
18
|
+
_COMPONENT_OUTPUT_KEY,
|
|
19
|
+
DefaultSpanHandler,
|
|
20
|
+
LangfuseSpan,
|
|
21
|
+
LangfuseTracer,
|
|
22
|
+
SpanContext,
|
|
23
|
+
)
|
|
24
|
+
from haystack_integrations.components.connectors.langfuse import LangfuseConnector
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
class MockSpan:
|
|
23
|
-
def __init__(self):
|
|
28
|
+
def __init__(self, name="mock_span"):
|
|
24
29
|
self._data = {}
|
|
25
30
|
self._span = self
|
|
26
|
-
self.operation_name =
|
|
31
|
+
self.operation_name = name
|
|
32
|
+
self._name = name
|
|
27
33
|
|
|
28
34
|
def raw_span(self):
|
|
29
35
|
return self
|
|
30
36
|
|
|
31
37
|
def span(self, name=None):
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
return self
|
|
38
|
+
# Return a new mock span for child spans
|
|
39
|
+
return MockSpan(name=name or "child_span")
|
|
35
40
|
|
|
36
41
|
def update(self, **kwargs):
|
|
37
42
|
self._data.update(kwargs)
|
|
38
43
|
|
|
39
44
|
def generation(self, name=None):
|
|
40
|
-
|
|
45
|
+
# Return a new mock span for generation spans
|
|
46
|
+
return MockSpan(name=name or "generation_span")
|
|
41
47
|
|
|
42
48
|
def end(self):
|
|
43
49
|
pass
|
|
44
50
|
|
|
45
51
|
|
|
46
52
|
class MockTracer:
|
|
47
|
-
|
|
48
53
|
def trace(self, name, **kwargs):
|
|
49
|
-
|
|
54
|
+
# Return a unique mock span for each trace call
|
|
55
|
+
return MockSpan(name=name)
|
|
50
56
|
|
|
51
57
|
def flush(self):
|
|
52
58
|
pass
|
|
@@ -62,7 +68,6 @@ class CustomSpanHandler(DefaultSpanHandler):
|
|
|
62
68
|
|
|
63
69
|
|
|
64
70
|
class TestLangfuseSpan:
|
|
65
|
-
|
|
66
71
|
# LangfuseSpan can be initialized with a span object
|
|
67
72
|
def test_initialized_with_span_object(self):
|
|
68
73
|
mock_span = Mock()
|
|
@@ -235,7 +240,8 @@ class TestLangfuseTracer:
|
|
|
235
240
|
langfuse_instance = Mock()
|
|
236
241
|
tracer = LangfuseTracer(tracer=langfuse_instance, name="Haystack", public=True)
|
|
237
242
|
assert tracer._tracer == langfuse_instance
|
|
238
|
-
|
|
243
|
+
# Check behavioral state instead of internal _context list
|
|
244
|
+
assert tracer.current_span() is None
|
|
239
245
|
assert tracer._name == "Haystack"
|
|
240
246
|
assert tracer._public
|
|
241
247
|
|
|
@@ -258,13 +264,14 @@ class TestLangfuseTracer:
|
|
|
258
264
|
|
|
259
265
|
# check that the trace method is called on the tracer instance with the provided operation name and tags
|
|
260
266
|
with tracer.trace("operation_name", tags={"tag1": "value1", "tag2": "value2"}) as span:
|
|
261
|
-
|
|
267
|
+
# Check that there is a current active span during tracing
|
|
268
|
+
assert tracer.current_span() is not None, "There should be an active span during tracing"
|
|
269
|
+
assert tracer.current_span() == span, "The current span should be the active span"
|
|
262
270
|
assert span.raw_span().operation_name == "operation_name"
|
|
263
271
|
assert span.raw_span().metadata == {"tag1": "value1", "tag2": "value2"}
|
|
264
272
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
), "The trace span should have been popped, and the root span is closed as well"
|
|
273
|
+
# Check that the span is cleaned up after tracing
|
|
274
|
+
assert tracer.current_span() is None, "There should be no active span after tracing completes"
|
|
268
275
|
|
|
269
276
|
# check that update method is called on the span instance with the provided key value pairs
|
|
270
277
|
def test_update_span_with_pipeline_input_output_data(self):
|
|
@@ -327,12 +334,12 @@ class TestLangfuseTracer:
|
|
|
327
334
|
assert mock_span.update.call_count >= 1
|
|
328
335
|
name_update_call = None
|
|
329
336
|
for call in mock_span.update.call_args_list:
|
|
330
|
-
if
|
|
337
|
+
if "name" in call[1]:
|
|
331
338
|
name_update_call = call
|
|
332
339
|
break
|
|
333
340
|
|
|
334
341
|
assert name_update_call is not None, "No call to update the span name was made"
|
|
335
|
-
updated_name = name_update_call[1][
|
|
342
|
+
updated_name = name_update_call[1]["name"]
|
|
336
343
|
|
|
337
344
|
# verify the format of the updated span name to be: `original_component_name - [list_of_tool_names]`
|
|
338
345
|
assert updated_name != "tool_invoker", f"Expected 'tool_invoker` to be upddated with tool names"
|
|
@@ -372,8 +379,7 @@ class TestLangfuseTracer:
|
|
|
372
379
|
monkeypatch.setenv("HAYSTACK_LANGFUSE_ENFORCE_FLUSH", "false")
|
|
373
380
|
tracer_mock = Mock()
|
|
374
381
|
|
|
375
|
-
from haystack_integrations.tracing.langfuse.tracer import
|
|
376
|
-
LangfuseTracer
|
|
382
|
+
from haystack_integrations.tracing.langfuse.tracer import LangfuseTracer
|
|
377
383
|
|
|
378
384
|
tracer = LangfuseTracer(tracer=tracer_mock, name="Haystack", public=False)
|
|
379
385
|
with tracer.trace(operation_name="operation_name", tags={"haystack.pipeline.input_data": "hello"}) as span:
|
|
@@ -388,11 +394,12 @@ class TestLangfuseTracer:
|
|
|
388
394
|
with tracer.trace(operation_name="operation_name", tags={"haystack.pipeline.input_data": "hello"}) as span:
|
|
389
395
|
pass
|
|
390
396
|
|
|
391
|
-
|
|
397
|
+
# Check behavioral state instead of internal _context list
|
|
398
|
+
assert tracer.current_span() is None
|
|
392
399
|
|
|
393
400
|
def test_init_with_tracing_disabled(self, monkeypatch, caplog):
|
|
394
401
|
# Clear haystack modules because ProxyTracer is initialized whenever haystack is imported
|
|
395
|
-
modules_to_clear = [name for name in sys.modules if name.startswith(
|
|
402
|
+
modules_to_clear = [name for name in sys.modules if name.startswith("haystack")]
|
|
396
403
|
for name in modules_to_clear:
|
|
397
404
|
sys.modules.pop(name, None)
|
|
398
405
|
|
|
@@ -403,58 +410,76 @@ class TestLangfuseTracer:
|
|
|
403
410
|
|
|
404
411
|
LangfuseTracer(tracer=MockTracer(), name="Haystack", public=False)
|
|
405
412
|
assert "tracing is disabled" in caplog.text
|
|
406
|
-
|
|
407
|
-
def
|
|
413
|
+
|
|
414
|
+
def test_async_concurrency_span_isolation(self):
|
|
408
415
|
"""
|
|
409
|
-
Test that
|
|
416
|
+
Test that concurrent async traces maintain isolated span contexts.
|
|
410
417
|
|
|
411
|
-
This test
|
|
412
|
-
|
|
413
|
-
The fix ensures proper cleanup through try/finally blocks.
|
|
414
|
-
|
|
415
|
-
Before the fix: context would retain spans after failures (length > 0)
|
|
416
|
-
After the fix: context is always cleaned up (length == 0)
|
|
418
|
+
This test verifies that the context-local span stack prevents cross-request
|
|
419
|
+
span interleaving in concurrent environments like FastAPI servers.
|
|
417
420
|
"""
|
|
421
|
+
tracer = LangfuseTracer(tracer=MockTracer(), name="Haystack", public=False)
|
|
418
422
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
#
|
|
460
|
-
|
|
423
|
+
# Track spans from each task for verification
|
|
424
|
+
task1_spans = []
|
|
425
|
+
task2_spans = []
|
|
426
|
+
|
|
427
|
+
async def trace_task(task_id: str, spans_list: list):
|
|
428
|
+
"""Simulate a request with nested tracing operations"""
|
|
429
|
+
with tracer.trace(f"outer_operation_{task_id}") as outer_span:
|
|
430
|
+
spans_list.append(("outer", outer_span, tracer.current_span()))
|
|
431
|
+
|
|
432
|
+
# Simulate some async work
|
|
433
|
+
await asyncio.sleep(0.01)
|
|
434
|
+
|
|
435
|
+
with tracer.trace(f"inner_operation_{task_id}") as inner_span:
|
|
436
|
+
spans_list.append(("inner", inner_span, tracer.current_span()))
|
|
437
|
+
|
|
438
|
+
# Simulate more async work
|
|
439
|
+
await asyncio.sleep(0.01)
|
|
440
|
+
|
|
441
|
+
# Verify nested relationship within this task
|
|
442
|
+
assert tracer.current_span() == inner_span
|
|
443
|
+
|
|
444
|
+
# After inner span, outer should be current again
|
|
445
|
+
spans_list.append(("after_inner", None, tracer.current_span()))
|
|
446
|
+
assert tracer.current_span() == outer_span
|
|
447
|
+
|
|
448
|
+
# After all spans, should be None
|
|
449
|
+
spans_list.append(("after_outer", None, tracer.current_span()))
|
|
450
|
+
assert tracer.current_span() is None
|
|
451
|
+
|
|
452
|
+
async def run_concurrent_traces():
|
|
453
|
+
"""Run two concurrent tracing tasks"""
|
|
454
|
+
await asyncio.gather(trace_task("task1", task1_spans), trace_task("task2", task2_spans))
|
|
455
|
+
|
|
456
|
+
# Run the concurrent test
|
|
457
|
+
asyncio.run(run_concurrent_traces())
|
|
458
|
+
|
|
459
|
+
# Verify both tasks completed successfully
|
|
460
|
+
assert len(task1_spans) == 4
|
|
461
|
+
assert len(task2_spans) == 4
|
|
462
|
+
|
|
463
|
+
# Verify each task had proper span isolation
|
|
464
|
+
# Task 1 spans should be different from Task 2 spans
|
|
465
|
+
task1_outer = task1_spans[0][1] # outer span from task1
|
|
466
|
+
task2_outer = task2_spans[0][1] # outer span from task2
|
|
467
|
+
assert task1_outer != task2_outer
|
|
468
|
+
|
|
469
|
+
task1_inner = task1_spans[1][1] # inner span from task1
|
|
470
|
+
task2_inner = task2_spans[1][1] # inner span from task2
|
|
471
|
+
assert task1_inner != task2_inner
|
|
472
|
+
|
|
473
|
+
# Verify proper nesting within each task
|
|
474
|
+
# Task 1: outer -> inner -> outer -> None
|
|
475
|
+
assert task1_spans[0][2] == task1_outer # current_span during outer
|
|
476
|
+
assert task1_spans[1][2] == task1_inner # current_span during inner
|
|
477
|
+
assert task1_spans[2][2] == task1_outer # current_span after inner
|
|
478
|
+
assert task1_spans[3][2] is None # current_span after outer
|
|
479
|
+
|
|
480
|
+
# Task 2: outer -> inner -> outer -> None
|
|
481
|
+
assert task2_spans[0][2] == task2_outer # current_span during outer
|
|
482
|
+
assert task2_spans[1][2] == task2_inner # current_span during inner
|
|
483
|
+
assert task2_spans[2][2] == task2_outer # current_span after inner
|
|
484
|
+
assert task2_spans[3][2] is None # current_span after outer
|
|
485
|
+
|
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
import time
|
|
7
7
|
from typing import Any, Dict, List
|
|
8
8
|
from urllib.parse import urlparse
|
|
9
|
+
import json
|
|
9
10
|
|
|
10
11
|
import pytest
|
|
11
12
|
import requests
|
|
@@ -189,3 +190,67 @@ def test_tracing_with_sub_pipelines():
|
|
|
189
190
|
component_names = [key for obs in haystack_pipeline_run_observations for key in obs["input"].keys()]
|
|
190
191
|
assert "prompt_builder" in component_names
|
|
191
192
|
assert "llm" in component_names
|
|
193
|
+
|
|
194
|
+
@pytest.mark.skipif(
|
|
195
|
+
not all(
|
|
196
|
+
[
|
|
197
|
+
os.environ.get("LANGFUSE_SECRET_KEY"),
|
|
198
|
+
os.environ.get("LANGFUSE_PUBLIC_KEY"),
|
|
199
|
+
]
|
|
200
|
+
),
|
|
201
|
+
reason="Missing required environment variables: LANGFUSE_SECRET_KEY and LANGFUSE_PUBLIC_KEY",
|
|
202
|
+
)
|
|
203
|
+
@pytest.mark.integration
|
|
204
|
+
def test_context_cleanup_after_nested_failures():
|
|
205
|
+
"""
|
|
206
|
+
Test that tracer context is properly cleaned up even when nested operations fail.
|
|
207
|
+
|
|
208
|
+
This test addresses a critical bug where failing nested operations (like inner pipelines)
|
|
209
|
+
could corrupt the tracing context, leaving stale spans that affect subsequent operations.
|
|
210
|
+
The fix ensures proper cleanup through try/finally blocks.
|
|
211
|
+
|
|
212
|
+
Before the fix: context would retain spans after failures (length > 0)
|
|
213
|
+
After the fix: context is always cleaned up (length == 0)
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
@component
|
|
217
|
+
class FailingParser:
|
|
218
|
+
@component.output_types(result=str)
|
|
219
|
+
def run(self, data: str):
|
|
220
|
+
# This will fail with ValueError when data is not valid JSON
|
|
221
|
+
parsed = json.loads(data)
|
|
222
|
+
return {"result": parsed["key"]}
|
|
223
|
+
|
|
224
|
+
@component
|
|
225
|
+
class ComponentWithNestedPipeline:
|
|
226
|
+
def __init__(self):
|
|
227
|
+
# This simulates IntentClassifier's internal pipeline
|
|
228
|
+
self.internal_pipeline = Pipeline()
|
|
229
|
+
self.internal_pipeline.add_component("parser", FailingParser())
|
|
230
|
+
|
|
231
|
+
@component.output_types(result=str)
|
|
232
|
+
def run(self, input_data: str):
|
|
233
|
+
# Run nested pipeline - this is where corruption occurs
|
|
234
|
+
result = self.internal_pipeline.run({"parser": {"data": input_data}})
|
|
235
|
+
return {"result": result["parser"]["result"]}
|
|
236
|
+
|
|
237
|
+
tracer = LangfuseConnector("test")
|
|
238
|
+
|
|
239
|
+
main_pipeline = Pipeline()
|
|
240
|
+
main_pipeline.add_component("nested_component", ComponentWithNestedPipeline())
|
|
241
|
+
main_pipeline.add_component("tracer", tracer)
|
|
242
|
+
|
|
243
|
+
# Test 1: First run will fail and should clean up context
|
|
244
|
+
try:
|
|
245
|
+
main_pipeline.run({"nested_component": {"input_data": "invalid json"}})
|
|
246
|
+
except Exception:
|
|
247
|
+
pass # Expected to fail
|
|
248
|
+
|
|
249
|
+
# Critical assertion: context should be empty after failed operation
|
|
250
|
+
assert len(tracer.tracer._context) == 0
|
|
251
|
+
|
|
252
|
+
# Test 2: Second run should work normally with clean context
|
|
253
|
+
main_pipeline.run({"nested_component": {"input_data": '{"key": "valid"}'}})
|
|
254
|
+
|
|
255
|
+
# Critical assertion: context should be empty after successful operation
|
|
256
|
+
assert len(tracer.tracer._context) == 0
|
langfuse_haystack-2.2.1/PKG-INFO
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: langfuse-haystack
|
|
3
|
-
Version: 2.2.1
|
|
4
|
-
Summary: Langfuse integration for Haystack
|
|
5
|
-
Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse#readme
|
|
6
|
-
Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
|
|
7
|
-
Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse
|
|
8
|
-
Author-email: deepset GmbH <info@deepset.ai>
|
|
9
|
-
License-Expression: Apache-2.0
|
|
10
|
-
License-File: LICENSE.txt
|
|
11
|
-
Classifier: Development Status :: 4 - Beta
|
|
12
|
-
Classifier: Programming Language :: Python
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
-
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
|
-
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
|
-
Requires-Python: >=3.9
|
|
21
|
-
Requires-Dist: haystack-ai>=2.15.1
|
|
22
|
-
Requires-Dist: langfuse<3.0.0,>=2.9.0
|
|
23
|
-
Description-Content-Type: text/markdown
|
|
24
|
-
|
|
25
|
-
# langfuse-haystack
|
|
26
|
-
|
|
27
|
-
[](https://pypi.org/project/langfuse-haystack)
|
|
28
|
-
[](https://pypi.org/project/langfuse-haystack)
|
|
29
|
-
|
|
30
|
-
langfuse-haystack integrates tracing capabilities into [Haystack](https://github.com/deepset-ai/haystack) (2.x) pipelines using [Langfuse](https://langfuse.com/).
|
|
31
|
-
This package enhances the visibility of pipeline runs by capturing comprehensive details of the execution traces, including API calls, context data, prompts, and more.
|
|
32
|
-
Whether you're monitoring model performance, pinpointing areas for improvement, or creating datasets for fine-tuning and testing from your pipeline executions, langfuse-haystack is the right tool for you.
|
|
33
|
-
|
|
34
|
-
## Features
|
|
35
|
-
|
|
36
|
-
- Easy integration with Haystack pipelines
|
|
37
|
-
- Capture the full context of the execution
|
|
38
|
-
- Track model usage and cost
|
|
39
|
-
- Collect user feedback
|
|
40
|
-
- Identify low-quality outputs
|
|
41
|
-
- Build fine-tuning and testing datasets
|
|
42
|
-
|
|
43
|
-
## Installation
|
|
44
|
-
|
|
45
|
-
To install langfuse-haystack, run the following command:
|
|
46
|
-
|
|
47
|
-
```sh
|
|
48
|
-
pip install langfuse-haystack
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Usage
|
|
52
|
-
|
|
53
|
-
To enable tracing in your Haystack pipeline, add the `LangfuseConnector` to your pipeline.
|
|
54
|
-
You also need to set the `LANGFUSE_SECRET_KEY` and `LANGFUSE_PUBLIC_KEY` environment variables in order to connect to Langfuse account.
|
|
55
|
-
You can get these keys by signing up for an account on the Langfuse website.
|
|
56
|
-
|
|
57
|
-
⚠️ **Important:** To ensure proper tracing, always set environment variables before importing any Haystack components.
|
|
58
|
-
This is crucial because Haystack initializes its internal tracing components during import.
|
|
59
|
-
|
|
60
|
-
Here's the correct way to set up your script:
|
|
61
|
-
|
|
62
|
-
```python
|
|
63
|
-
import os
|
|
64
|
-
|
|
65
|
-
# Set environment variables first
|
|
66
|
-
os.environ["LANGFUSE_SECRET_KEY"] = "" # Your Langfuse secret key
|
|
67
|
-
os.environ["LANGFUSE_PUBLIC_KEY"] = "" # Your Langfuse public key
|
|
68
|
-
os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
|
|
69
|
-
|
|
70
|
-
# Then import Haystack components
|
|
71
|
-
from haystack.components.builders import ChatPromptBuilder
|
|
72
|
-
from haystack.components.generators.chat import OpenAIChatGenerator
|
|
73
|
-
from haystack.dataclasses import ChatMessage
|
|
74
|
-
from haystack import Pipeline
|
|
75
|
-
|
|
76
|
-
from haystack_integrations.components.connectors.langfuse import LangfuseConnector
|
|
77
|
-
|
|
78
|
-
# Rest of your code...
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Alternatively, an even better practice is to set these environment variables in your shell before running the script.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
Here's a full example:
|
|
85
|
-
|
|
86
|
-
```python
|
|
87
|
-
import os
|
|
88
|
-
|
|
89
|
-
os.environ["LANGFUSE_SECRET_KEY"] = "" # Your Langfuse secret key
|
|
90
|
-
os.environ["LANGFUSE_PUBLIC_KEY"] = "" # Your Langfuse public key
|
|
91
|
-
os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
|
|
92
|
-
|
|
93
|
-
from haystack.components.builders import ChatPromptBuilder
|
|
94
|
-
from haystack.components.generators.chat import OpenAIChatGenerator
|
|
95
|
-
from haystack.dataclasses import ChatMessage
|
|
96
|
-
from haystack import Pipeline
|
|
97
|
-
|
|
98
|
-
from haystack_integrations.components.connectors.langfuse import LangfuseConnector
|
|
99
|
-
|
|
100
|
-
pipe = Pipeline()
|
|
101
|
-
pipe.add_component("tracer", LangfuseConnector("Chat example"))
|
|
102
|
-
pipe.add_component("prompt_builder", ChatPromptBuilder())
|
|
103
|
-
pipe.add_component("llm", OpenAIChatGenerator(model="gpt-3.5-turbo"))
|
|
104
|
-
|
|
105
|
-
pipe.connect("prompt_builder.prompt", "llm.messages")
|
|
106
|
-
|
|
107
|
-
messages = [
|
|
108
|
-
ChatMessage.from_system("Always respond in German even if some input data is in other languages."),
|
|
109
|
-
ChatMessage.from_user("Tell me about {{location}}"),
|
|
110
|
-
]
|
|
111
|
-
|
|
112
|
-
response = pipe.run(
|
|
113
|
-
data={"prompt_builder": {"template_variables": {"location": "Berlin"}, "template": messages}}
|
|
114
|
-
)
|
|
115
|
-
print(response["llm"]["replies"][0])
|
|
116
|
-
print(response["tracer"]["trace_url"])
|
|
117
|
-
print(response["tracer"]["trace_id"])
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
In this example, we add the `LangfuseConnector` to the pipeline with the name "tracer".
|
|
121
|
-
Each run of the pipeline produces one trace viewable on the Langfuse website with a specific URL.
|
|
122
|
-
The trace captures the entire execution context, including the prompts, completions, and metadata.
|
|
123
|
-
|
|
124
|
-
## Trace Visualization
|
|
125
|
-
|
|
126
|
-
Langfuse provides a user-friendly interface to visualize and analyze the traces generated by your Haystack pipeline.
|
|
127
|
-
Login into your Langfuse account and navigate to the trace URL to view the trace details.
|
|
128
|
-
|
|
129
|
-
## Contributing
|
|
130
|
-
|
|
131
|
-
`hatch` is the best way to interact with this project. To install it, run:
|
|
132
|
-
```sh
|
|
133
|
-
pip install hatch
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
With `hatch` installed, run all the tests:
|
|
137
|
-
```
|
|
138
|
-
hatch run test:all
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
To format your code and perform linting using Ruff (with automatic fixes), run:
|
|
142
|
-
```
|
|
143
|
-
hatch run fmt
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
To check for static type errors, run:
|
|
147
|
-
|
|
148
|
-
```console
|
|
149
|
-
$ hatch run test:types
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
## License
|
|
153
|
-
|
|
154
|
-
`langfuse-haystack` is distributed under the terms of the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) license.
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
# langfuse-haystack
|
|
2
|
-
|
|
3
|
-
[](https://pypi.org/project/langfuse-haystack)
|
|
4
|
-
[](https://pypi.org/project/langfuse-haystack)
|
|
5
|
-
|
|
6
|
-
langfuse-haystack integrates tracing capabilities into [Haystack](https://github.com/deepset-ai/haystack) (2.x) pipelines using [Langfuse](https://langfuse.com/).
|
|
7
|
-
This package enhances the visibility of pipeline runs by capturing comprehensive details of the execution traces, including API calls, context data, prompts, and more.
|
|
8
|
-
Whether you're monitoring model performance, pinpointing areas for improvement, or creating datasets for fine-tuning and testing from your pipeline executions, langfuse-haystack is the right tool for you.
|
|
9
|
-
|
|
10
|
-
## Features
|
|
11
|
-
|
|
12
|
-
- Easy integration with Haystack pipelines
|
|
13
|
-
- Capture the full context of the execution
|
|
14
|
-
- Track model usage and cost
|
|
15
|
-
- Collect user feedback
|
|
16
|
-
- Identify low-quality outputs
|
|
17
|
-
- Build fine-tuning and testing datasets
|
|
18
|
-
|
|
19
|
-
## Installation
|
|
20
|
-
|
|
21
|
-
To install langfuse-haystack, run the following command:
|
|
22
|
-
|
|
23
|
-
```sh
|
|
24
|
-
pip install langfuse-haystack
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Usage
|
|
28
|
-
|
|
29
|
-
To enable tracing in your Haystack pipeline, add the `LangfuseConnector` to your pipeline.
|
|
30
|
-
You also need to set the `LANGFUSE_SECRET_KEY` and `LANGFUSE_PUBLIC_KEY` environment variables in order to connect to Langfuse account.
|
|
31
|
-
You can get these keys by signing up for an account on the Langfuse website.
|
|
32
|
-
|
|
33
|
-
⚠️ **Important:** To ensure proper tracing, always set environment variables before importing any Haystack components.
|
|
34
|
-
This is crucial because Haystack initializes its internal tracing components during import.
|
|
35
|
-
|
|
36
|
-
Here's the correct way to set up your script:
|
|
37
|
-
|
|
38
|
-
```python
|
|
39
|
-
import os
|
|
40
|
-
|
|
41
|
-
# Set environment variables first
|
|
42
|
-
os.environ["LANGFUSE_SECRET_KEY"] = "" # Your Langfuse secret key
|
|
43
|
-
os.environ["LANGFUSE_PUBLIC_KEY"] = "" # Your Langfuse public key
|
|
44
|
-
os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
|
|
45
|
-
|
|
46
|
-
# Then import Haystack components
|
|
47
|
-
from haystack.components.builders import ChatPromptBuilder
|
|
48
|
-
from haystack.components.generators.chat import OpenAIChatGenerator
|
|
49
|
-
from haystack.dataclasses import ChatMessage
|
|
50
|
-
from haystack import Pipeline
|
|
51
|
-
|
|
52
|
-
from haystack_integrations.components.connectors.langfuse import LangfuseConnector
|
|
53
|
-
|
|
54
|
-
# Rest of your code...
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Alternatively, an even better practice is to set these environment variables in your shell before running the script.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
Here's a full example:
|
|
61
|
-
|
|
62
|
-
```python
|
|
63
|
-
import os
|
|
64
|
-
|
|
65
|
-
os.environ["LANGFUSE_SECRET_KEY"] = "" # Your Langfuse secret key
|
|
66
|
-
os.environ["LANGFUSE_PUBLIC_KEY"] = "" # Your Langfuse public key
|
|
67
|
-
os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
|
|
68
|
-
|
|
69
|
-
from haystack.components.builders import ChatPromptBuilder
|
|
70
|
-
from haystack.components.generators.chat import OpenAIChatGenerator
|
|
71
|
-
from haystack.dataclasses import ChatMessage
|
|
72
|
-
from haystack import Pipeline
|
|
73
|
-
|
|
74
|
-
from haystack_integrations.components.connectors.langfuse import LangfuseConnector
|
|
75
|
-
|
|
76
|
-
pipe = Pipeline()
|
|
77
|
-
pipe.add_component("tracer", LangfuseConnector("Chat example"))
|
|
78
|
-
pipe.add_component("prompt_builder", ChatPromptBuilder())
|
|
79
|
-
pipe.add_component("llm", OpenAIChatGenerator(model="gpt-3.5-turbo"))
|
|
80
|
-
|
|
81
|
-
pipe.connect("prompt_builder.prompt", "llm.messages")
|
|
82
|
-
|
|
83
|
-
messages = [
|
|
84
|
-
ChatMessage.from_system("Always respond in German even if some input data is in other languages."),
|
|
85
|
-
ChatMessage.from_user("Tell me about {{location}}"),
|
|
86
|
-
]
|
|
87
|
-
|
|
88
|
-
response = pipe.run(
|
|
89
|
-
data={"prompt_builder": {"template_variables": {"location": "Berlin"}, "template": messages}}
|
|
90
|
-
)
|
|
91
|
-
print(response["llm"]["replies"][0])
|
|
92
|
-
print(response["tracer"]["trace_url"])
|
|
93
|
-
print(response["tracer"]["trace_id"])
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
In this example, we add the `LangfuseConnector` to the pipeline with the name "tracer".
|
|
97
|
-
Each run of the pipeline produces one trace viewable on the Langfuse website with a specific URL.
|
|
98
|
-
The trace captures the entire execution context, including the prompts, completions, and metadata.
|
|
99
|
-
|
|
100
|
-
## Trace Visualization
|
|
101
|
-
|
|
102
|
-
Langfuse provides a user-friendly interface to visualize and analyze the traces generated by your Haystack pipeline.
|
|
103
|
-
Login into your Langfuse account and navigate to the trace URL to view the trace details.
|
|
104
|
-
|
|
105
|
-
## Contributing
|
|
106
|
-
|
|
107
|
-
`hatch` is the best way to interact with this project. To install it, run:
|
|
108
|
-
```sh
|
|
109
|
-
pip install hatch
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
With `hatch` installed, run all the tests:
|
|
113
|
-
```
|
|
114
|
-
hatch run test:all
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
To format your code and perform linting using Ruff (with automatic fixes), run:
|
|
118
|
-
```
|
|
119
|
-
hatch run fmt
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
To check for static type errors, run:
|
|
123
|
-
|
|
124
|
-
```console
|
|
125
|
-
$ hatch run test:types
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
## License
|
|
129
|
-
|
|
130
|
-
`langfuse-haystack` is distributed under the terms of the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) license.
|
|
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
|