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.
Files changed (21) hide show
  1. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/CHANGELOG.md +7 -0
  2. langfuse_haystack-2.3.0/PKG-INFO +45 -0
  3. langfuse_haystack-2.3.0/README.md +21 -0
  4. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/src/haystack_integrations/tracing/langfuse/tracer.py +19 -8
  5. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/tests/test_tracer.py +100 -75
  6. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/tests/test_tracing.py +65 -0
  7. langfuse_haystack-2.2.1/PKG-INFO +0 -154
  8. langfuse_haystack-2.2.1/README.md +0 -130
  9. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/.gitignore +0 -0
  10. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/LICENSE.txt +0 -0
  11. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/example/basic_rag.py +0 -0
  12. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/example/chat.py +0 -0
  13. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/example/requirements.txt +0 -0
  14. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/pydoc/config.yml +0 -0
  15. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/pyproject.toml +0 -0
  16. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/src/haystack_integrations/components/connectors/__init__.py +0 -0
  17. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
  18. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +0 -0
  19. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
  20. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/tests/__init__.py +0 -0
  21. {langfuse_haystack-2.2.1 → langfuse_haystack-2.3.0}/tests/test_langfuse_connector.py +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/langfuse-v2.2.1] - 2025-08-07
4
+
5
+ ### 🚀 Features
6
+
7
+ - Add AmazonBedrockChatGenerator to supported chat generators in Langfuse (#2164)
8
+
9
+
3
10
  ## [integrations/langfuse-v2.2.0] - 2025-07-03
4
11
 
5
12
  ### 🚀 Features
@@ -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
+ [![PyPI - Version](https://img.shields.io/pypi/v/langfuse-haystack.svg)](https://pypi.org/project/langfuse-haystack)
28
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/langfuse-haystack.svg)](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
+ [![PyPI - Version](https://img.shields.io/pypi/v/langfuse-haystack.svg)](https://pypi.org/project/langfuse-haystack)
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/langfuse-haystack.svg)](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
- # Context var used to keep track of tracing related info.
65
- # This mainly useful for parents spans.
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
- self._context.append(span)
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
- # CRITICAL: Always pop context to prevent corruption
418
- # This is especially important for nested pipeline scenarios
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
- return self._context[-1] if self._context else None
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, DefaultSpanHandler, LangfuseSpan, LangfuseTracer,
19
- SpanContext)
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 = "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
- # assert correct operation name passed to the span
33
- assert name == "operation_name"
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
- return self
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
- return MockSpan()
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
- assert tracer._context == []
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
- assert len(tracer._context) == 1, "The trace span should have been added to the the root context span"
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
- assert (
266
- len(tracer._context) == 0
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 'name' in call[1]:
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]['name']
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
- assert tracer._context == []
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('haystack')]
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 test_context_cleanup_after_nested_failures(self):
413
+
414
+ def test_async_concurrency_span_isolation(self):
408
415
  """
409
- Test that tracer context is properly cleaned up even when nested operations fail.
416
+ Test that concurrent async traces maintain isolated span contexts.
410
417
 
411
- This test addresses a critical bug where failing nested operations (like inner pipelines)
412
- could corrupt the tracing context, leaving stale spans that affect subsequent operations.
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
- @component
421
- class FailingParser:
422
- @component.output_types(result=str)
423
- def run(self, data: str):
424
- # This will fail with ValueError when data is not valid JSON
425
- parsed = json.loads(data)
426
- return {"result": parsed["key"]}
427
-
428
- @component
429
- class ComponentWithNestedPipeline:
430
- def __init__(self):
431
- # This simulates IntentClassifier's internal pipeline
432
- self.internal_pipeline = Pipeline()
433
- self.internal_pipeline.add_component("parser", FailingParser())
434
-
435
- @component.output_types(result=str)
436
- def run(self, input_data: str):
437
- # Run nested pipeline - this is where corruption occurs
438
- result = self.internal_pipeline.run({"parser": {"data": input_data}})
439
- return {"result": result["parser"]["result"]}
440
-
441
- tracer = LangfuseConnector("test")
442
-
443
- main_pipeline = Pipeline()
444
- main_pipeline.add_component("nested_component", ComponentWithNestedPipeline())
445
- main_pipeline.add_component("tracer", tracer)
446
-
447
- # Test 1: First run will fail and should clean up context
448
- try:
449
- main_pipeline.run({"nested_component": {"input_data": "invalid json"}})
450
- except Exception:
451
- pass # Expected to fail
452
-
453
- # Critical assertion: context should be empty after failed operation
454
- assert len(tracer.tracer._context) == 0
455
-
456
- # Test 2: Second run should work normally with clean context
457
- main_pipeline.run({"nested_component": {"input_data": '{"key": "valid"}'}})
458
-
459
- # Critical assertion: context should be empty after successful operation
460
- assert len(tracer.tracer._context) == 0
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
@@ -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
- [![PyPI - Version](https://img.shields.io/pypi/v/langfuse-haystack.svg)](https://pypi.org/project/langfuse-haystack)
28
- [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/langfuse-haystack.svg)](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
- [![PyPI - Version](https://img.shields.io/pypi/v/langfuse-haystack.svg)](https://pypi.org/project/langfuse-haystack)
4
- [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/langfuse-haystack.svg)](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.