langfuse-haystack 3.3.1__tar.gz → 4.0.1__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-3.3.1 → langfuse_haystack-4.0.1}/CHANGELOG.md +23 -0
  2. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/PKG-INFO +3 -4
  3. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/pyproject.toml +3 -9
  4. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +8 -8
  5. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/src/haystack_integrations/tracing/langfuse/tracer.py +15 -11
  6. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/tests/test_tracer.py +40 -2
  7. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/.gitignore +0 -0
  8. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/LICENSE.txt +0 -0
  9. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/README.md +0 -0
  10. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/example/basic_rag.py +0 -0
  11. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/example/chat.py +0 -0
  12. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/example/requirements.txt +0 -0
  13. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/pydoc/config_docusaurus.yml +0 -0
  14. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/src/haystack_integrations/components/connectors/__init__.py +0 -0
  15. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
  16. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/src/haystack_integrations/components/connectors/py.typed +0 -0
  17. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
  18. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/src/haystack_integrations/tracing/py.typed +0 -0
  19. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/tests/__init__.py +0 -0
  20. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/tests/test_langfuse_connector.py +0 -0
  21. {langfuse_haystack-3.3.1 → langfuse_haystack-4.0.1}/tests/test_tracing.py +0 -0
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/langfuse-v4.0.0] - 2026-01-12
4
+
5
+ ### 🧹 Chores
6
+
7
+ - Make fmt command more forgiving (#2671)
8
+ - [**breaking**] Langfuse - drop Python 3.9 and use X|Y typing (#2709)
9
+
10
+
11
+ ## [integrations/langfuse-v3.3.1] - 2025-12-10
12
+
13
+ ### 🚀 Features
14
+
15
+ - *(langfuse)* Add embedder usage metrics for langfuse (#2542)
16
+
17
+ ### 🐛 Bug Fixes
18
+
19
+ - Correct token usage accounting, fix trace polling in tests (#2594)
20
+
21
+ ### 🧹 Chores
22
+
23
+ - Remove Readme API CI workflow and configs (#2573)
24
+
25
+
3
26
  ## [integrations/langfuse-v3.3.0] - 2025-11-21
4
27
 
5
28
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langfuse-haystack
3
- Version: 3.3.1
3
+ Version: 4.0.1
4
4
  Summary: Langfuse integration for Haystack
5
5
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse#readme
6
6
  Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
@@ -10,15 +10,14 @@ License-Expression: Apache-2.0
10
10
  License-File: LICENSE.txt
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Programming Language :: Python
13
- Classifier: Programming Language :: Python :: 3.9
14
13
  Classifier: Programming Language :: Python :: 3.10
15
14
  Classifier: Programming Language :: Python :: 3.11
16
15
  Classifier: Programming Language :: Python :: 3.12
17
16
  Classifier: Programming Language :: Python :: 3.13
18
17
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
18
  Classifier: Programming Language :: Python :: Implementation :: PyPy
20
- Requires-Python: >=3.9
21
- Requires-Dist: haystack-ai>=2.17.1
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: haystack-ai>=2.22.0
22
21
  Requires-Dist: langfuse<4.0.0,>=3.3.1
23
22
  Description-Content-Type: text/markdown
24
23
 
@@ -7,14 +7,13 @@ name = "langfuse-haystack"
7
7
  dynamic = ["version"]
8
8
  description = "Langfuse integration for Haystack"
9
9
  readme = "README.md"
10
- requires-python = ">=3.9"
10
+ requires-python = ">=3.10"
11
11
  license = "Apache-2.0"
12
12
  keywords = []
13
13
  authors = [{ name = "deepset GmbH", email = "info@deepset.ai" }]
14
14
  classifiers = [
15
15
  "Development Status :: 4 - Beta",
16
16
  "Programming Language :: Python",
17
- "Programming Language :: Python :: 3.9",
18
17
  "Programming Language :: Python :: 3.10",
19
18
  "Programming Language :: Python :: 3.11",
20
19
  "Programming Language :: Python :: 3.12",
@@ -22,7 +21,7 @@ classifiers = [
22
21
  "Programming Language :: Python :: Implementation :: CPython",
23
22
  "Programming Language :: Python :: Implementation :: PyPy",
24
23
  ]
25
- dependencies = ["haystack-ai>=2.17.1", "langfuse>=3.3.1, <4.0.0"]
24
+ dependencies = ["haystack-ai>=2.22.0", "langfuse>=3.3.1, <4.0.0"]
26
25
 
27
26
  [project.urls]
28
27
  Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse#readme"
@@ -47,7 +46,7 @@ dependencies = ["haystack-pydoc-tools", "ruff"]
47
46
 
48
47
  [tool.hatch.envs.default.scripts]
49
48
  docs = ["pydoc-markdown pydoc/config_docusaurus.yml"]
50
- fmt = "ruff check --fix {args} && ruff format {args}"
49
+ fmt = "ruff check --fix {args}; ruff format {args}"
51
50
  fmt-check = "ruff check {args} && ruff format --check {args}"
52
51
 
53
52
  [tool.hatch.envs.test]
@@ -82,7 +81,6 @@ allow-direct-references = true
82
81
 
83
82
 
84
83
  [tool.ruff]
85
- target-version = "py39"
86
84
  line-length = 120
87
85
 
88
86
  [tool.ruff.lint]
@@ -128,10 +126,6 @@ ignore = [
128
126
  # Asserts
129
127
  "S101",
130
128
  ]
131
- unfixable = [
132
- # Don't touch unused imports
133
- "F401",
134
- ]
135
129
 
136
130
  [tool.ruff.lint.isort]
137
131
  known-first-party = ["haystack_integrations"]
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- from typing import Any, Optional
5
+ from typing import Any
6
6
 
7
7
  import httpx
8
8
  from haystack import component, default_from_dict, default_to_dict, logging, tracing
@@ -118,13 +118,13 @@ class LangfuseConnector:
118
118
  self,
119
119
  name: str,
120
120
  public: bool = False,
121
- public_key: Optional[Secret] = Secret.from_env_var("LANGFUSE_PUBLIC_KEY"), # noqa: B008
122
- secret_key: Optional[Secret] = Secret.from_env_var("LANGFUSE_SECRET_KEY"), # noqa: B008
123
- httpx_client: Optional[httpx.Client] = None,
124
- span_handler: Optional[SpanHandler] = None,
121
+ public_key: Secret | None = Secret.from_env_var("LANGFUSE_PUBLIC_KEY"), # noqa: B008
122
+ secret_key: Secret | None = Secret.from_env_var("LANGFUSE_SECRET_KEY"), # noqa: B008
123
+ httpx_client: httpx.Client | None = None,
124
+ span_handler: SpanHandler | None = None,
125
125
  *,
126
- host: Optional[str] = None,
127
- langfuse_client_kwargs: Optional[dict[str, Any]] = None,
126
+ host: str | None = None,
127
+ langfuse_client_kwargs: dict[str, Any] | None = None,
128
128
  ) -> None:
129
129
  """
130
130
  Initialize the LangfuseConnector component.
@@ -172,7 +172,7 @@ class LangfuseConnector:
172
172
  tracing.enable_tracing(self.tracer)
173
173
 
174
174
  @component.output_types(name=str, trace_url=str, trace_id=str)
175
- def run(self, invocation_context: Optional[dict[str, Any]] = None) -> dict[str, str]:
175
+ def run(self, invocation_context: dict[str, Any] | None = None) -> dict[str, str]:
176
176
  """
177
177
  Runs the LangfuseConnector component.
178
178
 
@@ -12,7 +12,7 @@ from contextlib import AbstractContextManager
12
12
  from contextvars import ContextVar
13
13
  from dataclasses import dataclass
14
14
  from datetime import datetime
15
- from typing import Any, Literal, Optional, cast
15
+ from typing import Any, Literal, cast
16
16
 
17
17
  from haystack import default_from_dict, default_to_dict, logging
18
18
  from haystack.dataclasses import ChatMessage
@@ -22,7 +22,7 @@ from haystack.tracing import utils as tracing_utils
22
22
 
23
23
  import langfuse
24
24
  from langfuse import LangfuseSpan as LangfuseClientSpan
25
- from langfuse.types import TraceMetadata
25
+ from langfuse.types import TraceContext, TraceMetadata
26
26
 
27
27
  logger = logging.getLogger(__name__)
28
28
 
@@ -48,7 +48,7 @@ tracing_context_var: ContextVar[dict[Any, Any]] = ContextVar("tracing_context")
48
48
 
49
49
  # Internal span execution hierarchy for our tracer
50
50
  # Manages parent-child relationships and prevents cross-request span interleaving
51
- span_stack_var: ContextVar[Optional[list["LangfuseSpan"]]] = ContextVar("span_stack", default=None)
51
+ span_stack_var: ContextVar[list["LangfuseSpan"] | None] = ContextVar("span_stack", default=None)
52
52
 
53
53
 
54
54
  class LangfuseSpan(Span):
@@ -150,9 +150,9 @@ class SpanContext:
150
150
 
151
151
  name: str
152
152
  operation_name: str
153
- component_type: Optional[str]
153
+ component_type: str | None
154
154
  tags: dict[str, Any]
155
- parent_span: Optional[Span]
155
+ parent_span: Span | None
156
156
  trace_name: str = "Haystack"
157
157
  public: bool = False
158
158
 
@@ -189,7 +189,7 @@ class SpanHandler(ABC):
189
189
  """
190
190
 
191
191
  def __init__(self) -> None:
192
- self.tracer: Optional[langfuse.Langfuse] = None
192
+ self.tracer: langfuse.Langfuse | None = None
193
193
 
194
194
  def init_tracer(self, tracer: langfuse.Langfuse) -> None:
195
195
  """
@@ -215,7 +215,7 @@ class SpanHandler(ABC):
215
215
  pass
216
216
 
217
217
  @abstractmethod
218
- def handle(self, span: LangfuseSpan, component_type: Optional[str]) -> None:
218
+ def handle(self, span: LangfuseSpan, component_type: str | None) -> None:
219
219
  """
220
220
  Process a span after component execution by attaching metadata and metrics.
221
221
 
@@ -287,8 +287,12 @@ class DefaultSpanHandler(SpanHandler):
287
287
  root_span_type: Literal["agent", "span"] = (
288
288
  "agent" if context.operation_name == "haystack.agent.run" else "span"
289
289
  )
290
+ trace_context: TraceContext | None = cast(
291
+ TraceContext, {"trace_id": tracing_ctx.get("trace_id")} if tracing_ctx.get("trace_id") else None
292
+ )
290
293
  # Create a new trace when there's no parent span
291
294
  span_context_manager = self.tracer.start_as_current_observation(
295
+ trace_context=trace_context,
292
296
  name=context.trace_name,
293
297
  version=tracing_ctx.get("version"),
294
298
  as_type=root_span_type,
@@ -338,7 +342,7 @@ class DefaultSpanHandler(SpanHandler):
338
342
  else:
339
343
  return LangfuseSpan(self.tracer.start_as_current_span(name=context.name))
340
344
 
341
- def handle(self, span: LangfuseSpan, component_type: Optional[str]) -> None:
345
+ def handle(self, span: LangfuseSpan, component_type: str | None) -> None:
342
346
  # If the span is at the pipeline level, we add input and output keys to the span
343
347
  at_pipeline_level = span.get_data().get(_PIPELINE_INPUT_KEY) is not None
344
348
  if at_pipeline_level:
@@ -420,7 +424,7 @@ class LangfuseTracer(Tracer):
420
424
  tracer: langfuse.Langfuse,
421
425
  name: str = "Haystack",
422
426
  public: bool = False,
423
- span_handler: Optional[SpanHandler] = None,
427
+ span_handler: SpanHandler | None = None,
424
428
  ) -> None:
425
429
  """
426
430
  Initialize a LangfuseTracer instance.
@@ -450,7 +454,7 @@ class LangfuseTracer(Tracer):
450
454
 
451
455
  @contextlib.contextmanager
452
456
  def trace(
453
- self, operation_name: str, tags: Optional[dict[str, Any]] = None, parent_span: Optional[Span] = None
457
+ self, operation_name: str, tags: dict[str, Any] | None = None, parent_span: Span | None = None
454
458
  ) -> Iterator[Span]:
455
459
  tags = tags or {}
456
460
  span_name = tags.get(_COMPONENT_NAME_KEY, operation_name)
@@ -541,7 +545,7 @@ class LangfuseTracer(Tracer):
541
545
  def flush(self) -> None:
542
546
  self._tracer.flush()
543
547
 
544
- def current_span(self) -> Optional[Span]:
548
+ def current_span(self) -> Span | None:
545
549
  """
546
550
  Return the current active span.
547
551
 
@@ -6,7 +6,6 @@ import asyncio
6
6
  import datetime
7
7
  import logging
8
8
  import sys
9
- from typing import Optional
10
9
  from unittest.mock import MagicMock, Mock, patch
11
10
 
12
11
  import pytest
@@ -19,6 +18,7 @@ from haystack_integrations.tracing.langfuse.tracer import (
19
18
  LangfuseTracer,
20
19
  SpanContext,
21
20
  _sanitize_usage_data,
21
+ tracing_context_var,
22
22
  )
23
23
 
24
24
 
@@ -107,7 +107,7 @@ class MockLangfuseClient:
107
107
 
108
108
 
109
109
  class CustomSpanHandler(DefaultSpanHandler):
110
- def handle(self, span: LangfuseSpan, component_type: Optional[str]) -> None:
110
+ def handle(self, span: LangfuseSpan, component_type: str | None) -> None:
111
111
  if component_type == "OpenAIChatGenerator":
112
112
  output = span.get_data().get(_COMPONENT_OUTPUT_KEY, {})
113
113
  replies = output.get("replies", [])
@@ -223,6 +223,44 @@ class TestSanitizeUsageData:
223
223
 
224
224
 
225
225
  class TestDefaultSpanHandler:
226
+ def test_create_span_root_trace(self):
227
+ """Test creating a root span (no parent)."""
228
+ mock_client = Mock()
229
+ mock_client.start_as_current_span = Mock(return_value=MockContextManager())
230
+ mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
231
+
232
+ handler = DefaultSpanHandler()
233
+ handler.init_tracer(mock_client)
234
+
235
+ context = MagicMock(spec=SpanContext)
236
+ context.parent_span = None
237
+ context.trace_name = "test_trace"
238
+ context.operation_name = "haystack.pipeline.run"
239
+ context.public = False
240
+
241
+ # Set up tracing context
242
+ token = tracing_context_var.set(
243
+ {"user_id": "test_user", "session_id": "test_session", "trace_id": "test_trace_id"}
244
+ )
245
+ try:
246
+ result = handler.create_span(context)
247
+
248
+ # Assert tracer was called with correct parameters
249
+ handler.tracer.start_as_current_observation.assert_called_once_with(
250
+ trace_context={"trace_id": "test_trace_id"},
251
+ name="test_trace",
252
+ version=None,
253
+ as_type="span",
254
+ )
255
+ call_kwargs = handler.tracer.start_as_current_observation.call_args[1]
256
+ assert "test_trace" == call_kwargs["name"]
257
+ assert "span" == call_kwargs["as_type"]
258
+
259
+ # Assert result is a LangfuseSpan
260
+ assert isinstance(result, LangfuseSpan)
261
+ finally:
262
+ tracing_context_var.reset(token)
263
+
226
264
  def test_handle_generator(self):
227
265
  mock_span = Mock()
228
266
  mock_span.raw_span.return_value = mock_span