langfuse-haystack 4.0.0__tar.gz → 4.1.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 (22) hide show
  1. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/CHANGELOG.md +15 -0
  2. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/PKG-INFO +1 -1
  3. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/example/basic_rag.py +2 -2
  4. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/example/chat.py +2 -2
  5. langfuse_haystack-4.1.0/pydoc/config_docusaurus.yml +14 -0
  6. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/pyproject.toml +14 -5
  7. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +1 -0
  8. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/src/haystack_integrations/tracing/langfuse/tracer.py +18 -2
  9. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/tests/test_tracer.py +55 -3
  10. langfuse_haystack-4.0.0/pydoc/config_docusaurus.yml +0 -29
  11. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/.gitignore +0 -0
  12. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/LICENSE.txt +0 -0
  13. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/README.md +0 -0
  14. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/example/requirements.txt +0 -0
  15. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/src/haystack_integrations/components/connectors/__init__.py +0 -0
  16. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
  17. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/src/haystack_integrations/components/connectors/py.typed +0 -0
  18. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
  19. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/src/haystack_integrations/tracing/py.typed +0 -0
  20. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/tests/__init__.py +0 -0
  21. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/tests/test_langfuse_connector.py +0 -0
  22. {langfuse_haystack-4.0.0 → langfuse_haystack-4.1.0}/tests/test_tracing.py +0 -0
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/langfuse-v4.0.1] - 2026-01-27
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - *(langfuse)* Apply trace_id from tracing context to Langfuse traces (#2781)
8
+
9
+
10
+ ## [integrations/langfuse-v4.0.0] - 2026-01-12
11
+
12
+ ### 🧹 Chores
13
+
14
+ - Make fmt command more forgiving (#2671)
15
+ - [**breaking**] Langfuse - drop Python 3.9 and use X|Y typing (#2709)
16
+
17
+
3
18
  ## [integrations/langfuse-v3.3.1] - 2025-12-10
4
19
 
5
20
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langfuse-haystack
3
- Version: 4.0.0
3
+ Version: 4.1.0
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
@@ -62,5 +62,5 @@ if __name__ == "__main__":
62
62
  question = "What does Rhodes Statue look like?"
63
63
  response = pipeline.run({"text_embedder": {"text": question}, "prompt_builder": {"question": question}})
64
64
 
65
- print(response["llm"]["replies"][0]) # noqa: T201
66
- print(response["tracer"]["trace_url"]) # noqa: T201
65
+ print(response["llm"]["replies"][0])
66
+ print(response["tracer"]["trace_url"])
@@ -59,5 +59,5 @@ if __name__ == "__main__":
59
59
  },
60
60
  }
61
61
  )
62
- print(response["llm"]["replies"][0]) # noqa: T201
63
- print(response["tracer"]["trace_url"]) # noqa: T201
62
+ print(response["llm"]["replies"][0])
63
+ print(response["tracer"]["trace_url"])
@@ -0,0 +1,14 @@
1
+ loaders:
2
+ - modules:
3
+ - haystack_integrations.components.connectors.langfuse.langfuse_connector
4
+ - haystack_integrations.tracing.langfuse.tracer
5
+ search_path: [../src]
6
+ processors:
7
+ - type: filter
8
+ documented_only: true
9
+ skip_empty_modules: true
10
+ renderer:
11
+ description: Langfuse integration for Haystack
12
+ id: integrations-langfuse
13
+ filename: langfuse.md
14
+ title: langfuse
@@ -45,7 +45,7 @@ installer = "uv"
45
45
  dependencies = ["haystack-pydoc-tools", "ruff"]
46
46
 
47
47
  [tool.hatch.envs.default.scripts]
48
- docs = ["pydoc-markdown pydoc/config_docusaurus.yml"]
48
+ docs = ["haystack-pydoc pydoc/config_docusaurus.yml"]
49
49
  fmt = "ruff check --fix {args}; ruff format {args}"
50
50
  fmt-check = "ruff check {args} && ruff format --check {args}"
51
51
 
@@ -76,8 +76,6 @@ non_interactive = true
76
76
  check_untyped_defs = true
77
77
  disallow_incomplete_defs = true
78
78
 
79
- [tool.hatch.metadata]
80
- allow-direct-references = true
81
79
 
82
80
 
83
81
  [tool.ruff]
@@ -86,9 +84,17 @@ line-length = 120
86
84
  [tool.ruff.lint]
87
85
  select = [
88
86
  "A",
87
+ "ANN",
89
88
  "ARG",
90
89
  "B",
91
90
  "C",
91
+ "D102", # Missing docstring in public method
92
+ "D103", # Missing docstring in public function
93
+ "D205", # 1 blank line required between summary line and description
94
+ "D209", # Closing triple quotes go to new line
95
+ "D213", # summary lines must be positioned on the second physical line of the docstring
96
+ "D417", # Missing argument descriptions in the docstring
97
+ "D419", # Docstring is empty
92
98
  "DTZ",
93
99
  "E",
94
100
  "EM",
@@ -125,6 +131,8 @@ ignore = [
125
131
  "PLR0915",
126
132
  # Asserts
127
133
  "S101",
134
+ # Allow `Any` - used legitimately for dynamic types and SDK boundaries
135
+ "ANN401",
128
136
  ]
129
137
 
130
138
  [tool.ruff.lint.isort]
@@ -135,9 +143,10 @@ ban-relative-imports = "parents"
135
143
 
136
144
  [tool.ruff.lint.per-file-ignores]
137
145
  # Tests can use magic values, assertions, and relative imports
138
- "tests/**/*" = ["PLR2004", "S101", "TID252"]
146
+ "tests/**/*" = ["D", "PLR2004", "S101", "TID252", "ANN"]
139
147
  # Examples can print their output
140
- "examples/**" = ["T201"]
148
+ "examples/**" = ["T201", "ANN", "D"]
149
+ "example/**" = ["T201", "ANN", "D"]
141
150
  "tests/**" = ["T201"]
142
151
 
143
152
  [tool.coverage.run]
@@ -19,6 +19,7 @@ logger = logging.getLogger(__name__)
19
19
  class LangfuseConnector:
20
20
  """
21
21
  LangfuseConnector connects Haystack LLM framework with [Langfuse](https://langfuse.com) in order to enable the
22
+
22
23
  tracing of operations and data flow within various components of a pipeline.
23
24
 
24
25
  To use LangfuseConnector, add it to your pipeline without connecting it to any other components.
@@ -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
 
@@ -91,7 +91,10 @@ class LangfuseSpan(Span):
91
91
  if key.endswith(".input"):
92
92
  if "messages" in value:
93
93
  messages = [m.to_openai_dict_format(require_tool_call_ids=False) for m in value["messages"]]
94
- self._span.update(input=messages)
94
+ if isinstance(gen_kwargs := value.get("generation_kwargs"), dict):
95
+ self._span.update(input={"messages": messages, "generation_kwargs": gen_kwargs})
96
+ else:
97
+ self._span.update(input=messages)
95
98
  else:
96
99
  coerced_value = tracing_utils.coerce_tag_value(value)
97
100
  self._span.update(input=coerced_value)
@@ -125,6 +128,7 @@ class LangfuseSpan(Span):
125
128
  return self._data
126
129
 
127
130
  def get_correlation_data_for_logs(self) -> dict[str, Any]:
131
+ """Return correlation data for log enrichment."""
128
132
  return {}
129
133
 
130
134
 
@@ -234,9 +238,11 @@ class SpanHandler(ABC):
234
238
 
235
239
  @classmethod
236
240
  def from_dict(cls, data: dict[str, Any]) -> "SpanHandler":
241
+ """Deserialize a SpanHandler from a dictionary."""
237
242
  return default_from_dict(cls, data)
238
243
 
239
244
  def to_dict(self) -> dict[str, Any]:
245
+ """Serialize this SpanHandler to a dictionary."""
240
246
  return default_to_dict(self)
241
247
 
242
248
 
@@ -273,6 +279,7 @@ class DefaultSpanHandler(SpanHandler):
273
279
  """DefaultSpanHandler provides the default Langfuse tracing behavior for Haystack."""
274
280
 
275
281
  def create_span(self, context: SpanContext) -> LangfuseSpan:
282
+ """Create a Langfuse span based on the given context."""
276
283
  if self.tracer is None:
277
284
  message = (
278
285
  "Tracer is not initialized. "
@@ -287,8 +294,12 @@ class DefaultSpanHandler(SpanHandler):
287
294
  root_span_type: Literal["agent", "span"] = (
288
295
  "agent" if context.operation_name == "haystack.agent.run" else "span"
289
296
  )
297
+ trace_context: TraceContext | None = cast(
298
+ TraceContext, {"trace_id": tracing_ctx.get("trace_id")} if tracing_ctx.get("trace_id") else None
299
+ )
290
300
  # Create a new trace when there's no parent span
291
301
  span_context_manager = self.tracer.start_as_current_observation(
302
+ trace_context=trace_context,
292
303
  name=context.trace_name,
293
304
  version=tracing_ctx.get("version"),
294
305
  as_type=root_span_type,
@@ -339,6 +350,7 @@ class DefaultSpanHandler(SpanHandler):
339
350
  return LangfuseSpan(self.tracer.start_as_current_span(name=context.name))
340
351
 
341
352
  def handle(self, span: LangfuseSpan, component_type: str | None) -> None:
353
+ """Process and enrich a span after component execution."""
342
354
  # If the span is at the pipeline level, we add input and output keys to the span
343
355
  at_pipeline_level = span.get_data().get(_PIPELINE_INPUT_KEY) is not None
344
356
  if at_pipeline_level:
@@ -452,6 +464,7 @@ class LangfuseTracer(Tracer):
452
464
  def trace(
453
465
  self, operation_name: str, tags: dict[str, Any] | None = None, parent_span: Span | None = None
454
466
  ) -> Iterator[Span]:
467
+ """Create and manage a tracing span as a context manager."""
455
468
  tags = tags or {}
456
469
  span_name = tags.get(_COMPONENT_NAME_KEY, operation_name)
457
470
  component_type = tags.get(_COMPONENT_TYPE_KEY)
@@ -539,6 +552,7 @@ class LangfuseTracer(Tracer):
539
552
  self.flush()
540
553
 
541
554
  def flush(self) -> None:
555
+ """Flush all pending spans to Langfuse."""
542
556
  self._tracer.flush()
543
557
 
544
558
  def current_span(self) -> Span | None:
@@ -554,6 +568,7 @@ class LangfuseTracer(Tracer):
554
568
  def get_trace_url(self) -> str:
555
569
  """
556
570
  Return the URL to the tracing data.
571
+
557
572
  :return: The URL to the tracing data.
558
573
  """
559
574
  return self._tracer.get_trace_url() or ""
@@ -561,6 +576,7 @@ class LangfuseTracer(Tracer):
561
576
  def get_trace_id(self) -> str:
562
577
  """
563
578
  Return the trace ID.
579
+
564
580
  :return: The trace ID.
565
581
  """
566
582
  return self._tracer.get_current_trace_id() or ""
@@ -18,6 +18,7 @@ from haystack_integrations.tracing.langfuse.tracer import (
18
18
  LangfuseTracer,
19
19
  SpanContext,
20
20
  _sanitize_usage_data,
21
+ tracing_context_var,
21
22
  )
22
23
 
23
24
 
@@ -148,7 +149,6 @@ class TestLangfuseSpan:
148
149
  def test_set_content_tag_updates_input_and_output_with_messages(self):
149
150
  mock_context_manager = MockContextManager()
150
151
 
151
- # test message input
152
152
  span = LangfuseSpan(mock_context_manager)
153
153
  span.set_content_tag("key.input", {"messages": [ChatMessage.from_user("message")]})
154
154
  assert mock_context_manager._span.update.call_count == 1
@@ -156,6 +156,20 @@ class TestLangfuseSpan:
156
156
  assert mock_context_manager._span.update.call_args_list[0][1] == {
157
157
  "input": [{"role": "user", "content": "message"}]
158
158
  }
159
+
160
+ # test generation_kwargs are captured
161
+ mock_context_manager._span.update.reset_mock()
162
+ span.set_content_tag(
163
+ "key.input",
164
+ {"messages": [ChatMessage.from_user("message")], "generation_kwargs": {"n": 2, "temperature": 0.7}},
165
+ )
166
+ assert mock_context_manager._span.update.call_count == 1
167
+ assert mock_context_manager._span.update.call_args_list[0][1] == {
168
+ "input": {
169
+ "messages": [{"role": "user", "content": "message"}],
170
+ "generation_kwargs": {"n": 2, "temperature": 0.7},
171
+ }
172
+ }
159
173
  # test replies ChatMessage list
160
174
  mock_context_manager._span.update.reset_mock()
161
175
  span.set_content_tag("key.output", {"replies": [ChatMessage.from_system("reply")]})
@@ -222,6 +236,44 @@ class TestSanitizeUsageData:
222
236
 
223
237
 
224
238
  class TestDefaultSpanHandler:
239
+ def test_create_span_root_trace(self):
240
+ """Test creating a root span (no parent)."""
241
+ mock_client = Mock()
242
+ mock_client.start_as_current_span = Mock(return_value=MockContextManager())
243
+ mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
244
+
245
+ handler = DefaultSpanHandler()
246
+ handler.init_tracer(mock_client)
247
+
248
+ context = MagicMock(spec=SpanContext)
249
+ context.parent_span = None
250
+ context.trace_name = "test_trace"
251
+ context.operation_name = "haystack.pipeline.run"
252
+ context.public = False
253
+
254
+ # Set up tracing context
255
+ token = tracing_context_var.set(
256
+ {"user_id": "test_user", "session_id": "test_session", "trace_id": "test_trace_id"}
257
+ )
258
+ try:
259
+ result = handler.create_span(context)
260
+
261
+ # Assert tracer was called with correct parameters
262
+ handler.tracer.start_as_current_observation.assert_called_once_with(
263
+ trace_context={"trace_id": "test_trace_id"},
264
+ name="test_trace",
265
+ version=None,
266
+ as_type="span",
267
+ )
268
+ call_kwargs = handler.tracer.start_as_current_observation.call_args[1]
269
+ assert "test_trace" == call_kwargs["name"]
270
+ assert "span" == call_kwargs["as_type"]
271
+
272
+ # Assert result is a LangfuseSpan
273
+ assert isinstance(result, LangfuseSpan)
274
+ finally:
275
+ tracing_context_var.reset(token)
276
+
225
277
  def test_handle_generator(self):
226
278
  mock_span = Mock()
227
279
  mock_span.raw_span.return_value = mock_span
@@ -529,7 +581,7 @@ class TestLangfuseTracer:
529
581
  assert tracer._public
530
582
 
531
583
  def test_create_new_span(self):
532
- mock_raw_span = MagicMock()
584
+ mock_raw_span = Mock()
533
585
  mock_raw_span.operation_name = "operation_name"
534
586
  mock_raw_span.metadata = {"tag1": "value1", "tag2": "value2"}
535
587
 
@@ -547,7 +599,7 @@ class TestLangfuseTracer:
547
599
  mock_span_instance.get_data.return_value = {}
548
600
  mock_span_instance._context_manager = mock_context_manager
549
601
 
550
- mock_tracer = MagicMock()
602
+ mock_tracer = Mock()
551
603
  mock_tracer.start_as_current_span.return_value = mock_context_manager
552
604
  mock_tracer.start_as_current_observation.return_value = mock_context_manager
553
605
 
@@ -1,29 +0,0 @@
1
- loaders:
2
- - ignore_when_discovered:
3
- - __init__
4
- modules:
5
- - haystack_integrations.components.connectors.langfuse.langfuse_connector
6
- - haystack_integrations.tracing.langfuse.tracer
7
- search_path:
8
- - ../src
9
- type: haystack_pydoc_tools.loaders.CustomPythonLoader
10
- processors:
11
- - do_not_filter_modules: false
12
- documented_only: true
13
- expression: null
14
- skip_empty_modules: true
15
- type: filter
16
- - type: smart
17
- - type: crossref
18
- renderer:
19
- description: Langfuse integration for Haystack
20
- id: integrations-langfuse
21
- markdown:
22
- add_member_class_prefix: false
23
- add_method_class_prefix: true
24
- classdef_code_block: false
25
- descriptive_class_title: false
26
- descriptive_module_title: true
27
- filename: langfuse.md
28
- title: langfuse
29
- type: haystack_pydoc_tools.renderers.DocusaurusRenderer