langfuse-haystack 5.1.0__tar.gz → 6.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-5.1.0 → langfuse_haystack-6.0.0}/CHANGELOG.md +54 -8
  2. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/LICENSE.txt +1 -1
  3. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/PKG-INFO +3 -3
  4. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/pyproject.toml +2 -1
  5. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +20 -8
  6. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/src/haystack_integrations/tracing/langfuse/tracer.py +28 -1
  7. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/tests/test_langfuse_connector.py +78 -38
  8. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/tests/test_tracer.py +73 -0
  9. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/.gitignore +0 -0
  10. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/README.md +0 -0
  11. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/example/basic_rag.py +0 -0
  12. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/example/chat.py +0 -0
  13. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/example/requirements.txt +0 -0
  14. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/pydoc/config_docusaurus.yml +0 -0
  15. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
  16. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/src/haystack_integrations/components/connectors/py.typed +0 -0
  17. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
  18. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/src/haystack_integrations/tracing/py.typed +0 -0
  19. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/tests/__init__.py +0 -0
  20. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/tests/conftest.py +0 -0
  21. {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/tests/test_tracing.py +0 -0
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/langfuse-v5.2.0] - 2026-07-15
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - *(langfuse)* Replace noisy ToolInvoker input with focused tool call arguments (#3520)
8
+
9
+
10
+ ## [integrations/langfuse-v5.1.0] - 2026-07-08
11
+
12
+ ### 🐛 Bug Fixes
13
+
14
+ - *(langfuse)* Stop relying on `haystack.utils.base_serialization` helpers (#3563)
15
+
16
+ ### 📚 Documentation
17
+
18
+ - Replace old haystack core imports with haystack_integrations paths (#3545)
19
+
20
+ ### 🧪 Testing
21
+
22
+ - Trust test modules under Haystack 3.0's deserialization allowlist (#3537)
23
+
24
+ ### 🧹 Chores
25
+
26
+ - Improve consistency of integrations folder structure (#3430)
27
+
28
+
3
29
  ## [integrations/langfuse-v5.0.2] - 2026-06-01
4
30
 
5
31
  ### 🐛 Bug Fixes
@@ -165,14 +191,19 @@
165
191
 
166
192
  - Properly cleanup Langfuse tracing context after pipeline run failures (#1999)
167
193
 
194
+ ### 🧹 Chores
195
+
196
+ - Remove black (#1985)
197
+
198
+
199
+ ## [integrations/langfuse-v2.1.0] - 2025-06-16
200
+
168
201
 
169
202
  ### 🧹 Chores
170
203
 
171
204
  - Pin langfuse<3.0.0 (#1904)
172
205
  - Align core-integrations Hatch scripts (#1898)
173
206
  - Update md files for new hatch scripts (#1911)
174
- - Remove black (#1985)
175
-
176
207
 
177
208
  ## [integrations/langfuse-v2.0.1] - 2025-06-02
178
209
 
@@ -229,10 +260,15 @@
229
260
 
230
261
  ### 🚀 Features
231
262
 
232
- - Adapt Ollama metadata to OpenAI format; support Ollama in Langfuse (#1577)
233
263
  - Unify traces of sub-pipelines within pipelines with Langfuse (#1624)
234
264
 
235
265
 
266
+ ## [integrations/langfuse-v0.10.0] - 2025-04-04
267
+
268
+ ### 🚀 Features
269
+
270
+ - Adapt Ollama metadata to OpenAI format; support Ollama in Langfuse (#1577)
271
+
236
272
 
237
273
  ## [integrations/langfuse-v0.9.0] - 2025-04-04
238
274
 
@@ -292,6 +328,12 @@
292
328
 
293
329
  ## [integrations/langfuse-v0.6.2] - 2025-01-02
294
330
 
331
+ ### 🌀 Miscellaneous
332
+
333
+ - Fix messages conversion to OpenAI format (#1272)
334
+
335
+ ## [integrations/langfuse-v0.6.1] - 2024-12-11
336
+
295
337
  ### 🚀 Features
296
338
 
297
339
  - Warn if LangfuseTracer initialized without tracing enabled (#1231)
@@ -303,7 +345,6 @@
303
345
  ### 🌀 Miscellaneous
304
346
 
305
347
  - Chore: Fix tracing_context_var lint errors (#1220)
306
- - Fix messages conversion to OpenAI format (#1272)
307
348
 
308
349
  ## [integrations/langfuse-v0.6.0] - 2024-11-18
309
350
 
@@ -336,6 +377,9 @@
336
377
 
337
378
  - Langfuse - support generation span for more LLMs (#1087)
338
379
 
380
+
381
+ ## [integrations/langfuse-v0.3.0] - 2024-09-11
382
+
339
383
  ### 🚜 Refactor
340
384
 
341
385
  - Remove usage of deprecated `ChatMessage.to_openai_format` (#1001)
@@ -371,10 +415,6 @@
371
415
 
372
416
  ## [integrations/langfuse-v0.1.0] - 2024-06-13
373
417
 
374
- ### 🚀 Features
375
-
376
- - Langfuse integration (#686)
377
-
378
418
  ### 🐛 Bug Fixes
379
419
 
380
420
  - Performance optimizations and value error when streaming in langfuse (#798)
@@ -388,4 +428,10 @@
388
428
  - Chore: change the pydoc renderer class (#718)
389
429
  - Docs: add missing api references (#728)
390
430
 
431
+ ## [integrations/langfuse-v0.0.4] - 2024-05-02
432
+
433
+ ### 🚀 Features
434
+
435
+ - Langfuse integration (#686)
436
+
391
437
  <!-- generated by git-cliff -->
@@ -58,7 +58,7 @@ APPENDIX: How to apply the Apache License to your work.
58
58
 
59
59
  To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
60
60
 
61
- Copyright [yyyy] [name of copyright owner]
61
+ Copyright 2023-present deepset GmbH
62
62
 
63
63
  Licensed under the Apache License, Version 2.0 (the "License");
64
64
  you may not use this file except in compliance with the License.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: langfuse-haystack
3
- Version: 5.1.0
3
+ Version: 6.0.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
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Programming Language :: Python :: Implementation :: CPython
18
18
  Classifier: Programming Language :: Python :: Implementation :: PyPy
19
19
  Requires-Python: >=3.10
20
- Requires-Dist: haystack-ai>=2.22.0
20
+ Requires-Dist: haystack-ai>=3.0.0
21
21
  Requires-Dist: langfuse>=4.0.0
22
22
  Description-Content-Type: text/markdown
23
23
 
@@ -21,7 +21,7 @@ classifiers = [
21
21
  "Programming Language :: Python :: Implementation :: CPython",
22
22
  "Programming Language :: Python :: Implementation :: PyPy",
23
23
  ]
24
- dependencies = ["haystack-ai>=2.22.0", "langfuse>=4.0.0"]
24
+ dependencies = ["haystack-ai>=3.0.0", "langfuse>=4.0.0"]
25
25
 
26
26
  [project.urls]
27
27
  Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse#readme"
@@ -130,6 +130,7 @@ ignore = [
130
130
  "PLR0912",
131
131
  "PLR0913",
132
132
  "PLR0915",
133
+ "PLR0917",
133
134
  # Asserts
134
135
  "S101",
135
136
  # Allow `Any` - used legitimately for dynamic types and SDK boundaries
@@ -157,18 +157,28 @@ class LangfuseConnector:
157
157
  self.span_handler = span_handler
158
158
  self.host = host
159
159
  self.langfuse_client_kwargs = langfuse_client_kwargs
160
+ self._httpx_client = httpx_client
161
+ self.tracer: LangfuseTracer | None = None
162
+
163
+ def warm_up(self) -> None:
164
+ """
165
+ Initialize the Langfuse client and enable tracing once.
166
+ """
167
+ if self.tracer is not None:
168
+ return
169
+
160
170
  resolved_langfuse_client_kwargs = {
161
- "secret_key": secret_key.resolve_value() if secret_key else None,
162
- "public_key": public_key.resolve_value() if public_key else None,
163
- "httpx_client": httpx_client,
164
- "host": host,
165
- **(langfuse_client_kwargs or {}),
171
+ "secret_key": self.secret_key.resolve_value() if self.secret_key else None,
172
+ "public_key": self.public_key.resolve_value() if self.public_key else None,
173
+ "httpx_client": self._httpx_client,
174
+ "host": self.host,
175
+ **(self.langfuse_client_kwargs or {}),
166
176
  }
167
177
  self.tracer = LangfuseTracer(
168
178
  tracer=Langfuse(**resolved_langfuse_client_kwargs),
169
- name=name,
170
- public=public,
171
- span_handler=span_handler,
179
+ name=self.name,
180
+ public=self.public,
181
+ span_handler=self.span_handler,
172
182
  )
173
183
  tracing.enable_tracing(self.tracer)
174
184
 
@@ -186,6 +196,8 @@ class LangfuseConnector:
186
196
  - `trace_url`: The URL to the tracing data.
187
197
  - `trace_id`: The ID of the trace.
188
198
  """
199
+ self.warm_up()
200
+ assert self.tracer is not None
189
201
  logger.debug(
190
202
  "Langfuse tracer invoked with the following context: '{invocation_context}'",
191
203
  invocation_context=invocation_context,
@@ -345,10 +345,13 @@ class DefaultSpanHandler(SpanHandler):
345
345
  # special case for ToolInvoker (to update the span name to be: `original_component_name - [tool_names]`)
346
346
  if component_type == "ToolInvoker":
347
347
  tool_names: list[str] = []
348
+ tool_calls_input: list[dict[str, Any]] = []
348
349
  messages = span.get_data().get(_COMPONENT_INPUT_KEY, {}).get("messages", [])
349
350
  for message in messages:
350
351
  if isinstance(message, ChatMessage) and message.tool_calls:
351
- tool_names.extend(call.tool_name for call in message.tool_calls)
352
+ for call in message.tool_calls:
353
+ tool_names.append(call.tool_name)
354
+ tool_calls_input.append({"tool_name": call.tool_name, "arguments": call.arguments})
352
355
 
353
356
  if tool_names:
354
357
  # Fallback to "ToolInvoker" if we can't retrieve component name
@@ -357,6 +360,30 @@ class DefaultSpanHandler(SpanHandler):
357
360
  formatted_names = [f"{name} (x{count})" if count > 1 else name for name, count in tool_counts.items()]
358
361
  span.raw_span().update(name=f"{tool_invoker_name} - {sorted(formatted_names)}")
359
362
 
363
+ if tool_calls_input and proxy_tracer.is_content_tracing_enabled:
364
+ # Replace the noisy full message history with just the tool call arguments
365
+ span.raw_span().update(input=tool_calls_input)
366
+
367
+ output_messages = span.get_data().get(_COMPONENT_OUTPUT_KEY, {}).get("tool_messages", [])
368
+ tool_results: list[dict[str, Any]] = []
369
+ for message in output_messages:
370
+ if isinstance(message, ChatMessage) and message.tool_call_results:
371
+ for tcr in message.tool_call_results:
372
+ origin = tcr.origin
373
+ # Keys `name`, `arguments` and `id` let Langfuse detect these as tool
374
+ # calls at ingestion and populate the Tool Call Name filter in the UI.
375
+ tool_results.append(
376
+ {
377
+ "id": origin.id if origin else None,
378
+ "name": origin.tool_name if origin else None,
379
+ "arguments": origin.arguments if origin else None,
380
+ "result": tcr.result,
381
+ "error": tcr.error,
382
+ }
383
+ )
384
+ if tool_results:
385
+ span.raw_span().update(output=tool_results)
386
+
360
387
  if component_type and component_type.endswith("ChatGenerator"):
361
388
  replies = span.get_data().get(_COMPONENT_OUTPUT_KEY, {}).get("replies")
362
389
  if replies:
@@ -6,48 +6,53 @@ import os
6
6
 
7
7
  os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
8
8
 
9
- from unittest.mock import Mock
9
+ from unittest.mock import Mock, patch
10
10
 
11
- from haystack import Pipeline
11
+ import httpx
12
+ import pytest
13
+ from haystack import Pipeline, tracing
12
14
  from haystack.components.builders import ChatPromptBuilder
13
15
  from haystack.components.generators.chat import OpenAIChatGenerator
16
+ from haystack.tracing.tracer import NullTracer
14
17
  from haystack.utils import Secret
15
18
 
16
19
  from haystack_integrations.components.connectors.langfuse import LangfuseConnector
17
20
  from haystack_integrations.tracing.langfuse import DefaultSpanHandler
18
21
 
19
22
 
23
+ @pytest.fixture
24
+ def mock_langfuse(monkeypatch):
25
+ monkeypatch.setattr(tracing.tracer, "actual_tracer", NullTracer())
26
+ with patch("haystack_integrations.components.connectors.langfuse.langfuse_connector.Langfuse") as constructor:
27
+ constructor.return_value.get_trace_url.return_value = "https://example.com/trace"
28
+ constructor.return_value.get_current_trace_id.return_value = "12345"
29
+ yield constructor
30
+
31
+
20
32
  class CustomSpanHandler(DefaultSpanHandler):
21
33
  def handle(self, span, component_type=None):
22
34
  pass
23
35
 
24
36
 
25
- class TestLangfuseConnector:
26
- def test_run(self, monkeypatch):
27
- monkeypatch.setenv("LANGFUSE_SECRET_KEY", "secret")
28
- monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "public")
29
-
37
+ class TestRun:
38
+ def test_run(self, mock_langfuse):
30
39
  langfuse_connector = LangfuseConnector(
31
40
  name="Chat example - OpenAI",
32
41
  public=True,
33
- secret_key=Secret.from_env_var("LANGFUSE_SECRET_KEY"),
34
- public_key=Secret.from_env_var("LANGFUSE_PUBLIC_KEY"),
42
+ secret_key=Secret.from_token("secret"),
43
+ public_key=Secret.from_token("public"),
35
44
  )
36
45
 
37
- mock_tracer = Mock()
38
- mock_tracer.get_trace_url.return_value = "https://example.com/trace"
39
- mock_tracer.get_trace_id.return_value = "12345"
40
- langfuse_connector.tracer = mock_tracer
41
-
42
46
  response = langfuse_connector.run(invocation_context={"some_key": "some_value"})
43
47
  assert response["name"] == "Chat example - OpenAI"
44
48
  assert response["trace_url"] == "https://example.com/trace"
45
49
  assert response["trace_id"] == "12345"
50
+ mock_langfuse.assert_called_once()
51
+ assert tracing.tracer.actual_tracer is langfuse_connector.tracer
46
52
 
47
- def test_to_dict(self, monkeypatch):
48
- monkeypatch.setenv("LANGFUSE_SECRET_KEY", "secret")
49
- monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "public")
50
53
 
54
+ class TestSerialization:
55
+ def test_to_dict(self):
51
56
  langfuse_connector = LangfuseConnector(name="Chat example - OpenAI")
52
57
  serialized = langfuse_connector.to_dict()
53
58
 
@@ -72,10 +77,7 @@ class TestLangfuseConnector:
72
77
  },
73
78
  }
74
79
 
75
- def test_to_dict_with_params(self, monkeypatch):
76
- monkeypatch.setenv("LANGFUSE_SECRET_KEY", "secret")
77
- monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "public")
78
-
80
+ def test_to_dict_with_params(self):
79
81
  langfuse_connector = LangfuseConnector(
80
82
  name="Chat example - OpenAI",
81
83
  public=True,
@@ -111,10 +113,7 @@ class TestLangfuseConnector:
111
113
  },
112
114
  }
113
115
 
114
- def test_from_dict(self, monkeypatch):
115
- monkeypatch.setenv("LANGFUSE_SECRET_KEY", "secret")
116
- monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "public")
117
-
116
+ def test_from_dict(self):
118
117
  data = {
119
118
  "type": "haystack_integrations.components.connectors.langfuse.langfuse_connector.LangfuseConnector",
120
119
  "init_parameters": {
@@ -144,10 +143,7 @@ class TestLangfuseConnector:
144
143
  assert langfuse_connector.host is None
145
144
  assert langfuse_connector.langfuse_client_kwargs is None
146
145
 
147
- def test_from_dict_without_span_handler(self, monkeypatch):
148
- monkeypatch.setenv("LANGFUSE_SECRET_KEY", "secret")
149
- monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "public")
150
-
146
+ def test_from_dict_without_span_handler(self):
151
147
  # All keys that would point to None (span_handler, host, langfuse_client_kwargs) are intentionally absent
152
148
  data = {
153
149
  "type": "haystack_integrations.components.connectors.langfuse.langfuse_connector.LangfuseConnector",
@@ -175,10 +171,7 @@ class TestLangfuseConnector:
175
171
  assert langfuse_connector.host is None
176
172
  assert langfuse_connector.langfuse_client_kwargs is None
177
173
 
178
- def test_from_dict_with_params(self, monkeypatch):
179
- monkeypatch.setenv("LANGFUSE_SECRET_KEY", "secret")
180
- monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "public")
181
-
174
+ def test_from_dict_with_params(self):
182
175
  data = {
183
176
  "type": "haystack_integrations.components.connectors.langfuse.langfuse_connector.LangfuseConnector",
184
177
  "init_parameters": {
@@ -212,10 +205,8 @@ class TestLangfuseConnector:
212
205
  assert langfuse_connector.host == "https://example.com"
213
206
  assert langfuse_connector.langfuse_client_kwargs == {"timeout": 30.0}
214
207
 
215
- def test_from_dict_with_legacy_span_handler_format(self, monkeypatch):
208
+ def test_from_dict_with_legacy_span_handler_format(self):
216
209
  # Pipelines serialized before this fix wrap span_handler as {"type": ..., "data": {...}}
217
- monkeypatch.setenv("LANGFUSE_SECRET_KEY", "secret")
218
- monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "public")
219
210
 
220
211
  data = {
221
212
  "type": "haystack_integrations.components.connectors.langfuse.langfuse_connector.LangfuseConnector",
@@ -249,8 +240,6 @@ class TestLangfuseConnector:
249
240
 
250
241
  def test_pipeline_serialization(self, monkeypatch):
251
242
  # Set test env vars
252
- monkeypatch.setenv("LANGFUSE_SECRET_KEY", "secret")
253
- monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "public")
254
243
  monkeypatch.setenv("OPENAI_API_KEY", "openai_api_key")
255
244
 
256
245
  # Create pipeline with OpenAI LLM
@@ -285,3 +274,54 @@ class TestLangfuseConnector:
285
274
 
286
275
  # Verify pipeline is the same
287
276
  assert new_pipe == pipe
277
+
278
+
279
+ class TestComponentLifecycle:
280
+ @pytest.mark.parametrize("key", ["LANGFUSE_SECRET_KEY", "LANGFUSE_PUBLIC_KEY"])
281
+ def test_key_resolved_at_warm_up_not_init(self, key, monkeypatch, mock_langfuse):
282
+ monkeypatch.setenv("LANGFUSE_SECRET_KEY", "secret")
283
+ monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "public")
284
+ monkeypatch.delenv(key)
285
+ connector = LangfuseConnector("test")
286
+ assert connector.tracer is None
287
+ mock_langfuse.assert_not_called()
288
+ assert not tracing.is_tracing_enabled()
289
+
290
+ with pytest.raises(ValueError, match=key):
291
+ connector.warm_up()
292
+ assert connector.tracer is None
293
+ mock_langfuse.assert_not_called()
294
+
295
+ monkeypatch.setenv(key, "available-now")
296
+ connector.warm_up()
297
+ assert connector.tracer is not None
298
+ mock_langfuse.assert_called_once()
299
+
300
+ def test_warm_up_passes_configuration_and_is_idempotent(self, mock_langfuse):
301
+ client = Mock(spec=httpx.Client)
302
+ handler = CustomSpanHandler()
303
+ connector = LangfuseConnector(
304
+ "configured",
305
+ public=True,
306
+ public_key=Secret.from_token("public"),
307
+ secret_key=Secret.from_token("secret"),
308
+ httpx_client=client,
309
+ span_handler=handler,
310
+ host="https://example.com",
311
+ langfuse_client_kwargs={"timeout": 30, "host": "https://override.example.com"},
312
+ )
313
+ connector.warm_up()
314
+ first_tracer = connector.tracer
315
+ connector.warm_up()
316
+ assert connector.tracer is first_tracer
317
+ mock_langfuse.assert_called_once_with(
318
+ public_key="public",
319
+ secret_key="secret",
320
+ httpx_client=client,
321
+ host="https://override.example.com",
322
+ timeout=30,
323
+ )
324
+ assert handler.tracer is mock_langfuse.return_value
325
+ assert connector.tracer is not None
326
+ assert connector.tracer._name == "configured"
327
+ assert connector.tracer._public is True
@@ -748,6 +748,79 @@ class TestLangfuseTracer:
748
748
  assert "search_tool (x2)" in updated_name, f"Expected 'search_tool (x2)' in {updated_name}"
749
749
  assert "weather_tool" in updated_name, f"Expected 'weather_tool' in {updated_name}"
750
750
 
751
+ def test_handle_tool_invoker_input_output_with_content_tracing(self):
752
+ """
753
+ Test that ToolInvoker spans replace the noisy full-message input with just tool call
754
+ arguments, and populate output with tool results, when content tracing is enabled.
755
+ """
756
+ mock_span = Mock()
757
+ mock_span.raw_span.return_value = mock_span
758
+
759
+ tool_call = ToolCall(id="call_123", tool_name="search_tool", arguments={"query": "RAG pipelines"})
760
+
761
+ span_data = {
762
+ "haystack.component.name": "tool_invoker",
763
+ "haystack.component.type": "ToolInvoker",
764
+ "haystack.component.input": {
765
+ "messages": [
766
+ ChatMessage.from_user("what is RAG?"),
767
+ ChatMessage.from_assistant(text="Calling search", tool_calls=[tool_call]),
768
+ ]
769
+ },
770
+ "haystack.component.output": {
771
+ "tool_messages": [ChatMessage.from_tool("RAG stands for Retrieval-Augmented Generation", tool_call)]
772
+ },
773
+ }
774
+ mock_span.get_data.return_value = span_data
775
+
776
+ handler = DefaultSpanHandler()
777
+ with patch("haystack_integrations.tracing.langfuse.tracer.proxy_tracer.is_content_tracing_enabled", True):
778
+ handler.handle(mock_span, component_type="ToolInvoker")
779
+
780
+ update_calls = {k: v for call in mock_span.update.call_args_list for k, v in call[1].items()}
781
+
782
+ # input should be just the tool call arguments, not the full message list
783
+ assert update_calls["input"] == [{"tool_name": "search_tool", "arguments": {"query": "RAG pipelines"}}]
784
+ # output carries id/name/arguments so Langfuse detects the tool call and populates its filter
785
+ assert update_calls["output"] == [
786
+ {
787
+ "id": "call_123",
788
+ "name": "search_tool",
789
+ "arguments": {"query": "RAG pipelines"},
790
+ "result": "RAG stands for Retrieval-Augmented Generation",
791
+ "error": False,
792
+ }
793
+ ]
794
+
795
+ def test_handle_tool_invoker_no_content_tracing(self):
796
+ """
797
+ Test that ToolInvoker input/output is NOT updated when content tracing is disabled.
798
+ The span name update (tool names) should still happen.
799
+ """
800
+ mock_span = Mock()
801
+ mock_span.raw_span.return_value = mock_span
802
+
803
+ tool_call = ToolCall(tool_name="weather_tool", arguments={"location": "Tokyo"})
804
+
805
+ span_data = {
806
+ "haystack.component.name": "tool_invoker",
807
+ "haystack.component.type": "ToolInvoker",
808
+ "haystack.component.input": {"messages": [ChatMessage.from_assistant(text="", tool_calls=[tool_call])]},
809
+ "haystack.component.output": {"tool_messages": [ChatMessage.from_tool("Sunny, 28°C", tool_call)]},
810
+ }
811
+ mock_span.get_data.return_value = span_data
812
+
813
+ handler = DefaultSpanHandler()
814
+ with patch("haystack_integrations.tracing.langfuse.tracer.proxy_tracer.is_content_tracing_enabled", False):
815
+ handler.handle(mock_span, component_type="ToolInvoker")
816
+
817
+ update_kwargs_keys = {k for call in mock_span.update.call_args_list for k in call[1]}
818
+ # name should still be updated
819
+ assert "name" in update_kwargs_keys
820
+ # input and output must NOT be set when content tracing is off
821
+ assert "input" not in update_kwargs_keys
822
+ assert "output" not in update_kwargs_keys
823
+
751
824
  def test_trace_generation_invalid_start_time(self):
752
825
  with patch("haystack_integrations.tracing.langfuse.tracer.langfuse.get_client"):
753
826
  tracer = LangfuseTracer(tracer=MockLangfuseClient(), name="Haystack", public=False)