langfuse-haystack 2.2.1__tar.gz → 3.0.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-3.0.0}/CHANGELOG.md +22 -0
  2. langfuse_haystack-3.0.0/PKG-INFO +45 -0
  3. langfuse_haystack-3.0.0/README.md +21 -0
  4. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/pyproject.toml +1 -1
  5. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/src/haystack_integrations/tracing/langfuse/tracer.py +63 -36
  6. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/tests/test_tracer.py +237 -148
  7. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/tests/test_tracing.py +75 -11
  8. langfuse_haystack-2.2.1/PKG-INFO +0 -154
  9. langfuse_haystack-2.2.1/README.md +0 -130
  10. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/.gitignore +0 -0
  11. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/LICENSE.txt +0 -0
  12. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/example/basic_rag.py +0 -0
  13. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/example/chat.py +0 -0
  14. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/example/requirements.txt +0 -0
  15. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/pydoc/config.yml +0 -0
  16. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/src/haystack_integrations/components/connectors/__init__.py +0 -0
  17. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
  18. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +0 -0
  19. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
  20. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/tests/__init__.py +0 -0
  21. {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/tests/test_langfuse_connector.py +0 -0
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/langfuse-v2.3.0] - 2025-08-25
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - Avoid mixed Langfuse traces in async envs (#2207)
8
+
9
+ ### 🧪 Testing
10
+
11
+ - Make tests successfully run from forks (#2203)
12
+
13
+ ### 🧹 Chores
14
+
15
+ - Standardize readmes - part 2 (#2205)
16
+
17
+
18
+ ## [integrations/langfuse-v2.2.1] - 2025-08-07
19
+
20
+ ### 🚀 Features
21
+
22
+ - Add AmazonBedrockChatGenerator to supported chat generators in Langfuse (#2164)
23
+
24
+
3
25
  ## [integrations/langfuse-v2.2.0] - 2025-07-03
4
26
 
5
27
  ### 🚀 Features
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: langfuse-haystack
3
+ Version: 3.0.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<4.0.0,>=3.0.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`.
@@ -22,7 +22,7 @@ classifiers = [
22
22
  "Programming Language :: Python :: Implementation :: CPython",
23
23
  "Programming Language :: Python :: Implementation :: PyPy",
24
24
  ]
25
- dependencies = ["haystack-ai>=2.15.1", "langfuse>=2.9.0, <3.0.0"]
25
+ dependencies = ["haystack-ai>=2.15.1", "langfuse>=3.0.0, <4.0.0"]
26
26
 
27
27
  [project.urls]
28
28
  Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse#readme"
@@ -6,24 +6,21 @@ import contextlib
6
6
  import os
7
7
  from abc import ABC, abstractmethod
8
8
  from collections import Counter
9
+ from contextlib import AbstractContextManager
9
10
  from contextvars import ContextVar
10
11
  from dataclasses import dataclass
11
12
  from datetime import datetime
12
- from typing import Any, Dict, Iterator, List, Optional, Union
13
+ from typing import Any, Dict, Iterator, List, Optional
13
14
 
14
15
  from haystack import default_from_dict, default_to_dict, logging
15
16
  from haystack.dataclasses import ChatMessage
16
17
  from haystack.tracing import Span, Tracer
17
18
  from haystack.tracing import tracer as proxy_tracer
18
19
  from haystack.tracing import utils as tracing_utils
19
- from typing_extensions import TypeAlias
20
20
 
21
21
  import langfuse
22
- from langfuse.client import StatefulGenerationClient, StatefulSpanClient, StatefulTraceClient
23
-
24
- # Type alias for Langfuse stateful clients
25
- LangfuseStatefulClient: TypeAlias = Union[StatefulTraceClient, StatefulSpanClient, StatefulGenerationClient]
26
-
22
+ from langfuse import LangfuseSpan as LangfuseClientSpan
23
+ from langfuse.types import TraceMetadata
27
24
 
28
25
  logger = logging.getLogger(__name__)
29
26
 
@@ -61,25 +58,31 @@ _COMPONENT_TYPE_KEY = "haystack.component.type"
61
58
  _COMPONENT_OUTPUT_KEY = "haystack.component.output"
62
59
  _COMPONENT_INPUT_KEY = "haystack.component.input"
63
60
 
64
- # Context var used to keep track of tracing related info.
65
- # This mainly useful for parents spans.
61
+ # External session metadata for trace correlation (Haystack system)
62
+ # Stores trace_id, user_id, session_id, tags, version for root trace creation
66
63
  tracing_context_var: ContextVar[Dict[Any, Any]] = ContextVar("tracing_context")
67
64
 
65
+ # Internal span execution hierarchy for our tracer
66
+ # Manages parent-child relationships and prevents cross-request span interleaving
67
+ span_stack_var: ContextVar[Optional[List["LangfuseSpan"]]] = ContextVar("span_stack", default=None)
68
+
68
69
 
69
70
  class LangfuseSpan(Span):
70
71
  """
71
72
  Internal class representing a bridge between the Haystack span tracing API and Langfuse.
72
73
  """
73
74
 
74
- def __init__(self, span: LangfuseStatefulClient) -> None:
75
+ def __init__(self, context_manager: AbstractContextManager) -> None:
75
76
  """
76
77
  Initialize a LangfuseSpan instance.
77
78
 
78
- :param span: The span instance managed by Langfuse.
79
+ :param context_manager: The context manager from Langfuse created with
80
+ `langfuse.get_client().start_as_current_span` or
81
+ `langfuse.get_client().start_as_current_observation`.
79
82
  """
80
- self._span = span
81
- # locally cache tags
83
+ self._span = context_manager.__enter__()
82
84
  self._data: Dict[str, Any] = {}
85
+ self._context_manager = context_manager
83
86
 
84
87
  def set_tag(self, key: str, value: Any) -> None:
85
88
  """
@@ -121,7 +124,7 @@ class LangfuseSpan(Span):
121
124
 
122
125
  self._data[key] = value
123
126
 
124
- def raw_span(self) -> LangfuseStatefulClient:
127
+ def raw_span(self) -> LangfuseClientSpan:
125
128
  """
126
129
  Return the underlying span instance.
127
130
 
@@ -265,24 +268,39 @@ class DefaultSpanHandler(SpanHandler):
265
268
  )
266
269
  raise RuntimeError(message)
267
270
 
271
+ # Get external tracing context for root trace creation (correlation metadata)
268
272
  tracing_ctx = tracing_context_var.get({})
269
273
  if not context.parent_span:
270
274
  # Create a new trace when there's no parent span
271
- return LangfuseSpan(
272
- self.tracer.trace(
273
- name=context.trace_name,
274
- public=context.public,
275
- id=tracing_ctx.get("trace_id"),
276
- user_id=tracing_ctx.get("user_id"),
277
- session_id=tracing_ctx.get("session_id"),
278
- tags=tracing_ctx.get("tags"),
279
- version=tracing_ctx.get("version"),
280
- )
275
+ span_context_manager = self.tracer.start_as_current_span(
276
+ name=context.trace_name,
277
+ version=tracing_ctx.get("version"),
281
278
  )
279
+
280
+ # Create LangfuseSpan which will handle entering the context manager
281
+ span = LangfuseSpan(span_context_manager)
282
+
283
+ # Build trace metadata from context
284
+ trace_metadata: TraceMetadata = {
285
+ "name": context.trace_name,
286
+ "user_id": tracing_ctx.get("user_id"),
287
+ "session_id": tracing_ctx.get("session_id"),
288
+ "version": tracing_ctx.get("version"),
289
+ "metadata": None,
290
+ "tags": tracing_ctx.get("tags"),
291
+ "public": context.public,
292
+ }
293
+
294
+ # Filter out None values and apply trace attributes
295
+ trace_attrs = {k: v for k, v in trace_metadata.items() if v is not None}
296
+ if trace_attrs:
297
+ span._span.update_trace(**trace_attrs)
298
+
299
+ return span
282
300
  elif context.component_type in _ALL_SUPPORTED_GENERATORS:
283
- return LangfuseSpan(context.parent_span.raw_span().generation(name=context.name))
301
+ return LangfuseSpan(self.tracer.start_as_current_observation(name=context.name, as_type="generation"))
284
302
  else:
285
- return LangfuseSpan(context.parent_span.raw_span().span(name=context.name))
303
+ return LangfuseSpan(self.tracer.start_as_current_span(name=context.name))
286
304
 
287
305
  def handle(self, span: LangfuseSpan, component_type: Optional[str]) -> None:
288
306
  # If the span is at the pipeline level, we add input and output keys to the span
@@ -290,8 +308,7 @@ class DefaultSpanHandler(SpanHandler):
290
308
  if at_pipeline_level:
291
309
  coerced_input = tracing_utils.coerce_tag_value(span.get_data().get(_PIPELINE_INPUT_KEY))
292
310
  coerced_output = tracing_utils.coerce_tag_value(span.get_data().get(_PIPELINE_OUTPUT_KEY))
293
- span.raw_span().update(input=coerced_input, output=coerced_output)
294
-
311
+ span.raw_span().update_trace(input=coerced_input, output=coerced_output)
295
312
  # special case for ToolInvoker (to update the span name to be: `original_component_name - [tool_names]`)
296
313
  if component_type == "ToolInvoker":
297
314
  tool_names: List[str] = []
@@ -360,6 +377,7 @@ class LangfuseTracer(Tracer):
360
377
  "before importing Haystack."
361
378
  )
362
379
  self._tracer = tracer
380
+ # Keep _context as deprecated shim to avoid AttributeError if anyone uses it
363
381
  self._context: List[LangfuseSpan] = []
364
382
  self._name = name
365
383
  self._public = public
@@ -391,7 +409,12 @@ class LangfuseTracer(Tracer):
391
409
  # Create span using the handler
392
410
  span = self._span_handler.create_span(span_context)
393
411
 
394
- self._context.append(span)
412
+ # Build new span hierarchy: copy existing stack, add new span, save for restoration
413
+ prev_stack = span_stack_var.get()
414
+ new_stack = (prev_stack or []).copy()
415
+ new_stack.append(span)
416
+ token = span_stack_var.set(new_stack)
417
+
395
418
  span.set_tags(tags)
396
419
 
397
420
  try:
@@ -404,7 +427,11 @@ class LangfuseTracer(Tracer):
404
427
 
405
428
  # End span (may fail if span data is corrupted)
406
429
  raw_span = span.raw_span()
407
- if isinstance(raw_span, (StatefulSpanClient, StatefulGenerationClient)):
430
+ # In v3, we need to properly exit context managers
431
+ if span._context_manager is not None:
432
+ span._context_manager.__exit__(None, None, None)
433
+ elif hasattr(raw_span, "end"):
434
+ # Only call end() if it's not a context manager
408
435
  raw_span.end()
409
436
  except Exception as cleanup_error:
410
437
  # Log cleanup errors but don't let them corrupt context
@@ -414,10 +441,8 @@ class LangfuseTracer(Tracer):
414
441
  cleanup_error=cleanup_error,
415
442
  )
416
443
  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()
444
+ # Restore previous span stack using saved token - ensures proper cleanup
445
+ span_stack_var.reset(token)
421
446
 
422
447
  if self.enforce_flush:
423
448
  self.flush()
@@ -431,7 +456,9 @@ class LangfuseTracer(Tracer):
431
456
 
432
457
  :return: The current span if available, else None.
433
458
  """
434
- return self._context[-1] if self._context else None
459
+ # Get top of span stack (most recent span) from context-local storage
460
+ stack = span_stack_var.get()
461
+ return stack[-1] if stack else None
435
462
 
436
463
  def get_trace_url(self) -> str:
437
464
  """
@@ -445,4 +472,4 @@ class LangfuseTracer(Tracer):
445
472
  Return the trace ID.
446
473
  :return: The trace ID.
447
474
  """
448
- return self._tracer.get_trace_id()
475
+ return self._tracer.get_current_trace_id()