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.
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/CHANGELOG.md +54 -8
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/LICENSE.txt +1 -1
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/PKG-INFO +3 -3
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/pyproject.toml +2 -1
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +20 -8
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/src/haystack_integrations/tracing/langfuse/tracer.py +28 -1
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/tests/test_langfuse_connector.py +78 -38
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/tests/test_tracer.py +73 -0
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/.gitignore +0 -0
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/README.md +0 -0
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/example/basic_rag.py +0 -0
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/example/chat.py +0 -0
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/example/requirements.txt +0 -0
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/pydoc/config_docusaurus.yml +0 -0
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/src/haystack_integrations/components/connectors/py.typed +0 -0
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/src/haystack_integrations/tracing/py.typed +0 -0
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/tests/__init__.py +0 -0
- {langfuse_haystack-5.1.0 → langfuse_haystack-6.0.0}/tests/conftest.py +0 -0
- {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
|
|
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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: langfuse-haystack
|
|
3
|
-
Version:
|
|
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>=
|
|
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>=
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
26
|
-
def test_run(self,
|
|
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.
|
|
34
|
-
public_key=Secret.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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.1.0 → langfuse_haystack-6.0.0}/src/haystack_integrations/tracing/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|