langfuse-haystack 5.0.2__tar.gz → 5.2.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-5.0.2 → langfuse_haystack-5.2.0}/CHANGELOG.md +26 -0
  2. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/PKG-INFO +1 -1
  3. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/example/basic_rag.py +4 -2
  4. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/example/chat.py +2 -1
  5. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +12 -6
  6. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/src/haystack_integrations/tracing/langfuse/tracer.py +28 -1
  7. langfuse_haystack-5.2.0/tests/conftest.py +11 -0
  8. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/tests/test_langfuse_connector.py +37 -8
  9. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/tests/test_tracer.py +73 -0
  10. langfuse_haystack-5.0.2/src/haystack_integrations/components/connectors/__init__.py +0 -3
  11. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/.gitignore +0 -0
  12. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/LICENSE.txt +0 -0
  13. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/README.md +0 -0
  14. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/example/requirements.txt +0 -0
  15. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/pydoc/config_docusaurus.yml +0 -0
  16. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/pyproject.toml +0 -0
  17. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
  18. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/src/haystack_integrations/components/connectors/py.typed +0 -0
  19. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
  20. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/src/haystack_integrations/tracing/py.typed +0 -0
  21. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/tests/__init__.py +0 -0
  22. {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/tests/test_tracing.py +0 -0
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/langfuse-v5.1.0] - 2026-07-08
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - *(langfuse)* Stop relying on `haystack.utils.base_serialization` helpers (#3563)
8
+
9
+ ### 📚 Documentation
10
+
11
+ - Replace old haystack core imports with haystack_integrations paths (#3545)
12
+
13
+ ### 🧪 Testing
14
+
15
+ - Trust test modules under Haystack 3.0's deserialization allowlist (#3537)
16
+
17
+ ### 🧹 Chores
18
+
19
+ - Improve consistency of integrations folder structure (#3430)
20
+
21
+
22
+ ## [integrations/langfuse-v5.0.2] - 2026-06-01
23
+
24
+ ### 🐛 Bug Fixes
25
+
26
+ - Update langfuse deserialization to not require `span_handler` key (#3378)
27
+
28
+
3
29
  ## [integrations/langfuse-v5.0.1] - 2026-05-18
4
30
 
5
31
  ### 🐛 Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langfuse-haystack
3
- Version: 5.0.2
3
+ Version: 5.2.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
@@ -6,12 +6,15 @@ os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
6
6
  from datasets import load_dataset
7
7
  from haystack import Document, Pipeline
8
8
  from haystack.components.builders import PromptBuilder
9
- from haystack.components.embedders import SentenceTransformersDocumentEmbedder, SentenceTransformersTextEmbedder
10
9
  from haystack.components.generators import OpenAIGenerator
11
10
  from haystack.components.retrievers import InMemoryEmbeddingRetriever
12
11
  from haystack.document_stores.in_memory import InMemoryDocumentStore
13
12
 
14
13
  from haystack_integrations.components.connectors.langfuse import LangfuseConnector
14
+ from haystack_integrations.components.embedders.sentence_transformers import (
15
+ SentenceTransformersDocumentEmbedder,
16
+ SentenceTransformersTextEmbedder,
17
+ )
15
18
 
16
19
 
17
20
  def get_pipeline(document_store: InMemoryDocumentStore):
@@ -54,7 +57,6 @@ if __name__ == "__main__":
54
57
  document_store = InMemoryDocumentStore()
55
58
  dataset = load_dataset("bilgeyucel/seven-wonders", split="train")
56
59
  embedder = SentenceTransformersDocumentEmbedder("sentence-transformers/all-MiniLM-L6-v2")
57
- embedder.warm_up()
58
60
  docs_with_embeddings = embedder.run([Document(**ds) for ds in dataset]).get("documents") or [] # type: ignore
59
61
  document_store.write_documents(docs_with_embeddings)
60
62
 
@@ -9,7 +9,7 @@ os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
9
9
 
10
10
  from haystack import Pipeline
11
11
  from haystack.components.builders import ChatPromptBuilder
12
- from haystack.components.generators.chat import HuggingFaceAPIChatGenerator, OpenAIChatGenerator
12
+ from haystack.components.generators.chat import OpenAIChatGenerator
13
13
  from haystack.dataclasses import ChatMessage
14
14
  from haystack.utils.auth import Secret
15
15
  from haystack.utils.hf import HFGenerationAPIType
@@ -17,6 +17,7 @@ from haystack.utils.hf import HFGenerationAPIType
17
17
  from haystack_integrations.components.connectors.langfuse import LangfuseConnector
18
18
  from haystack_integrations.components.generators.anthropic import AnthropicChatGenerator
19
19
  from haystack_integrations.components.generators.cohere import CohereChatGenerator
20
+ from haystack_integrations.components.generators.huggingface_api import HuggingFaceAPIChatGenerator
20
21
 
21
22
  os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
22
23
 
@@ -6,11 +6,11 @@ from typing import Any
6
6
 
7
7
  import httpx
8
8
  from haystack import component, default_from_dict, default_to_dict, logging, tracing
9
- from haystack.utils import Secret, deserialize_secrets_inplace
10
- from haystack.utils.base_serialization import deserialize_class_instance, serialize_class_instance
11
- from langfuse import Langfuse
9
+ from haystack.core.serialization import component_to_dict
10
+ from haystack.utils import Secret, deserialize_chatgenerator_inplace, deserialize_secrets_inplace
12
11
 
13
12
  from haystack_integrations.tracing.langfuse import LangfuseTracer, SpanHandler
13
+ from langfuse import Langfuse
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
@@ -198,7 +198,7 @@ class LangfuseConnector:
198
198
 
199
199
  :returns: The serialized component as a dictionary.
200
200
  """
201
- span_handler = serialize_class_instance(self.span_handler) if self.span_handler else None
201
+ span_handler = component_to_dict(self.span_handler, "span_handler") if self.span_handler else None
202
202
  if self.langfuse_client_kwargs:
203
203
  # pop httpx_client and mask from self._langfuse_client_kwargs to prevent serialization issues
204
204
  langfuse_client_kwargs = {
@@ -228,6 +228,12 @@ class LangfuseConnector:
228
228
  """
229
229
  init_params = data["init_parameters"]
230
230
  deserialize_secrets_inplace(init_params, keys=["secret_key", "public_key"])
231
- if init_params.get("span_handler") is not None:
232
- init_params["span_handler"] = deserialize_class_instance(init_params["span_handler"])
231
+ span_handler_data = init_params.get("span_handler")
232
+ if span_handler_data is not None:
233
+ if "data" in span_handler_data:
234
+ # Pipelines serialized before this fix wrap the span handler as
235
+ # {"type": ..., "data": {"type": ..., "init_parameters": ...}}; unwrap it here so older
236
+ # serialized pipelines keep working.
237
+ init_params["span_handler"] = span_handler_data["data"]
238
+ deserialize_chatgenerator_inplace(init_params, key="span_handler")
233
239
  return default_from_dict(cls, data)
@@ -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:
@@ -0,0 +1,11 @@
1
+ import pytest
2
+
3
+
4
+ @pytest.fixture(autouse=True)
5
+ def allow_deserialization_of_test_modules(monkeypatch):
6
+ """
7
+ haystack-ai >= 3.0 refuses to deserialize classes and callables from modules outside its
8
+ trusted-module allowlist. Tools and callbacks defined in the test modules live outside that
9
+ allowlist, so trust them explicitly; haystack-ai 2.x ignores this environment variable.
10
+ """
11
+ monkeypatch.setenv("HAYSTACK_DESERIALIZATION_ALLOWLIST", "tests,test_*")
@@ -104,10 +104,7 @@ class TestLangfuseConnector:
104
104
  },
105
105
  "span_handler": {
106
106
  "type": "tests.test_langfuse_connector.CustomSpanHandler",
107
- "data": {
108
- "type": "tests.test_langfuse_connector.CustomSpanHandler",
109
- "init_parameters": {},
110
- },
107
+ "init_parameters": {},
111
108
  },
112
109
  "host": "https://example.com",
113
110
  "langfuse_client_kwargs": {"timeout": 30.0},
@@ -199,10 +196,7 @@ class TestLangfuseConnector:
199
196
  },
200
197
  "span_handler": {
201
198
  "type": "tests.test_langfuse_connector.CustomSpanHandler",
202
- "data": {
203
- "type": "tests.test_langfuse_connector.CustomSpanHandler",
204
- "init_parameters": {},
205
- },
199
+ "init_parameters": {},
206
200
  },
207
201
  "host": "https://example.com",
208
202
  "langfuse_client_kwargs": {"timeout": 30.0},
@@ -218,6 +212,41 @@ class TestLangfuseConnector:
218
212
  assert langfuse_connector.host == "https://example.com"
219
213
  assert langfuse_connector.langfuse_client_kwargs == {"timeout": 30.0}
220
214
 
215
+ def test_from_dict_with_legacy_span_handler_format(self, monkeypatch):
216
+ # 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
+
220
+ data = {
221
+ "type": "haystack_integrations.components.connectors.langfuse.langfuse_connector.LangfuseConnector",
222
+ "init_parameters": {
223
+ "name": "Chat example - OpenAI",
224
+ "public": True,
225
+ "secret_key": {
226
+ "type": "env_var",
227
+ "env_vars": ["LANGFUSE_SECRET_KEY"],
228
+ "strict": True,
229
+ },
230
+ "public_key": {
231
+ "type": "env_var",
232
+ "env_vars": ["LANGFUSE_PUBLIC_KEY"],
233
+ "strict": True,
234
+ },
235
+ "span_handler": {
236
+ "type": "tests.test_langfuse_connector.CustomSpanHandler",
237
+ "data": {
238
+ "type": "tests.test_langfuse_connector.CustomSpanHandler",
239
+ "init_parameters": {},
240
+ },
241
+ },
242
+ "host": "https://example.com",
243
+ "langfuse_client_kwargs": {"timeout": 30.0},
244
+ },
245
+ }
246
+
247
+ langfuse_connector = LangfuseConnector.from_dict(data)
248
+ assert isinstance(langfuse_connector.span_handler, CustomSpanHandler)
249
+
221
250
  def test_pipeline_serialization(self, monkeypatch):
222
251
  # Set test env vars
223
252
  monkeypatch.setenv("LANGFUSE_SECRET_KEY", "secret")
@@ -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)
@@ -1,3 +0,0 @@
1
- # SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
2
- #
3
- # SPDX-License-Identifier: Apache-2.0