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.
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/CHANGELOG.md +26 -0
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/PKG-INFO +1 -1
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/example/basic_rag.py +4 -2
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/example/chat.py +2 -1
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +12 -6
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/src/haystack_integrations/tracing/langfuse/tracer.py +28 -1
- langfuse_haystack-5.2.0/tests/conftest.py +11 -0
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/tests/test_langfuse_connector.py +37 -8
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/tests/test_tracer.py +73 -0
- langfuse_haystack-5.0.2/src/haystack_integrations/components/connectors/__init__.py +0 -3
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/.gitignore +0 -0
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/LICENSE.txt +0 -0
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/README.md +0 -0
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/example/requirements.txt +0 -0
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/pydoc/config_docusaurus.yml +0 -0
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/pyproject.toml +0 -0
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/src/haystack_integrations/components/connectors/py.typed +0 -0
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/src/haystack_integrations/tracing/py.typed +0 -0
- {langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/tests/__init__.py +0 -0
- {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
|
|
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
|
|
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.
|
|
10
|
-
from haystack.utils
|
|
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 =
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langfuse_haystack-5.0.2 → langfuse_haystack-5.2.0}/src/haystack_integrations/tracing/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|