langfuse-haystack 4.1.0__tar.gz → 5.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-4.1.0 → langfuse_haystack-5.0.0}/CHANGELOG.md +21 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/PKG-INFO +2 -2
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/pyproject.toml +6 -2
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/src/haystack_integrations/tracing/langfuse/tracer.py +88 -86
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/tests/test_tracer.py +110 -75
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/.gitignore +0 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/LICENSE.txt +0 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/README.md +0 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/example/basic_rag.py +0 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/example/chat.py +0 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/example/requirements.txt +0 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/pydoc/config_docusaurus.yml +0 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/src/haystack_integrations/components/connectors/__init__.py +0 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +0 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/src/haystack_integrations/components/connectors/py.typed +0 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/src/haystack_integrations/tracing/py.typed +0 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/tests/__init__.py +0 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/tests/test_langfuse_connector.py +0 -0
- {langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/tests/test_tracing.py +0 -0
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [integrations/langfuse-v4.1.0] - 2026-03-26
|
|
4
|
+
|
|
5
|
+
### 🚀 Features
|
|
6
|
+
|
|
7
|
+
- Log generation_kwargs in Langfuse spans (#3017)
|
|
8
|
+
|
|
9
|
+
### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- *(langfuse)* Replace MagicMock with Mock to prevent RAM exhaustion (#2543) (#2798)
|
|
12
|
+
|
|
13
|
+
### 📚 Documentation
|
|
14
|
+
|
|
15
|
+
- Simplify pydoc configs (#2855)
|
|
16
|
+
|
|
17
|
+
### 🧹 Chores
|
|
18
|
+
|
|
19
|
+
- Remove unused allow-direct-references (#2866)
|
|
20
|
+
- Add ANN type annotations to google_genai, hanlp, jina, langfuse, lara (#2990)
|
|
21
|
+
- Enforce ruff docstring rules (D102/D103/D205/D209/D213/D417/D419) in integrations 21-30 (#3010)
|
|
22
|
+
|
|
23
|
+
|
|
3
24
|
## [integrations/langfuse-v4.0.1] - 2026-01-27
|
|
4
25
|
|
|
5
26
|
### 🐛 Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langfuse-haystack
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.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
|
|
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
|
18
18
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Requires-Dist: haystack-ai>=2.22.0
|
|
21
|
-
Requires-Dist: langfuse
|
|
21
|
+
Requires-Dist: langfuse>=4.0.0
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
24
24
|
# langfuse-haystack
|
|
@@ -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>=
|
|
24
|
+
dependencies = ["haystack-ai>=2.22.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"
|
|
@@ -66,7 +66,8 @@ dependencies = [
|
|
|
66
66
|
unit = 'pytest -m "not integration" {args:tests}'
|
|
67
67
|
integration = 'pytest -m "integration" {args:tests}'
|
|
68
68
|
all = 'pytest {args:tests}'
|
|
69
|
-
cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x {args:tests}'
|
|
69
|
+
unit-cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x -m "not integration" {args:tests}'
|
|
70
|
+
integration-cov-append-retry = 'pytest --cov=haystack_integrations --cov-append --reruns 3 --reruns-delay 30 -x -m "integration" {args:tests}'
|
|
70
71
|
|
|
71
72
|
types = "mypy -p haystack_integrations.components.connectors.langfuse -p haystack_integrations.tracing.langfuse {args}"
|
|
72
73
|
|
|
@@ -152,6 +153,7 @@ ban-relative-imports = "parents"
|
|
|
152
153
|
[tool.coverage.run]
|
|
153
154
|
source = ["haystack_integrations"]
|
|
154
155
|
branch = true
|
|
156
|
+
relative_files = true
|
|
155
157
|
parallel = false
|
|
156
158
|
|
|
157
159
|
|
|
@@ -159,6 +161,8 @@ parallel = false
|
|
|
159
161
|
omit = ["*/tests/*", "*/__init__.py"]
|
|
160
162
|
show_missing = true
|
|
161
163
|
exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
|
|
164
|
+
# coverage automatically detects some of the test dependencies but cannot find the source, so we ignore the errors
|
|
165
|
+
ignore_errors = true
|
|
162
166
|
|
|
163
167
|
[tool.pytest.ini_options]
|
|
164
168
|
addopts = "--strict-markers"
|
|
@@ -22,7 +22,8 @@ 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
|
|
25
|
+
from langfuse import propagate_attributes
|
|
26
|
+
from langfuse.types import TraceContext
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
|
28
29
|
|
|
@@ -61,7 +62,6 @@ class LangfuseSpan(Span):
|
|
|
61
62
|
Initialize a LangfuseSpan instance.
|
|
62
63
|
|
|
63
64
|
:param context_manager: The context manager from Langfuse created with
|
|
64
|
-
`langfuse.get_client().start_as_current_span` or
|
|
65
65
|
`langfuse.get_client().start_as_current_observation`.
|
|
66
66
|
"""
|
|
67
67
|
self._span = context_manager.__enter__()
|
|
@@ -297,33 +297,17 @@ class DefaultSpanHandler(SpanHandler):
|
|
|
297
297
|
trace_context: TraceContext | None = cast(
|
|
298
298
|
TraceContext, {"trace_id": tracing_ctx.get("trace_id")} if tracing_ctx.get("trace_id") else None
|
|
299
299
|
)
|
|
300
|
-
|
|
300
|
+
|
|
301
301
|
span_context_manager = self.tracer.start_as_current_observation(
|
|
302
302
|
trace_context=trace_context,
|
|
303
303
|
name=context.trace_name,
|
|
304
304
|
version=tracing_ctx.get("version"),
|
|
305
305
|
as_type=root_span_type,
|
|
306
306
|
)
|
|
307
|
-
|
|
308
|
-
# Create LangfuseSpan which will handle entering the context manager
|
|
309
307
|
span = LangfuseSpan(span_context_manager)
|
|
310
308
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
"name": context.trace_name,
|
|
314
|
-
"user_id": tracing_ctx.get("user_id"),
|
|
315
|
-
"session_id": tracing_ctx.get("session_id"),
|
|
316
|
-
"version": tracing_ctx.get("version"),
|
|
317
|
-
"metadata": None,
|
|
318
|
-
"tags": tracing_ctx.get("tags"),
|
|
319
|
-
"public": context.public,
|
|
320
|
-
"release": None,
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
# Filter out None values and apply trace attributes
|
|
324
|
-
trace_attrs = {k: v for k, v in trace_metadata.items() if v is not None}
|
|
325
|
-
if trace_attrs:
|
|
326
|
-
span._span.update_trace(**trace_attrs)
|
|
309
|
+
if context.public:
|
|
310
|
+
span.raw_span().set_trace_as_public()
|
|
327
311
|
|
|
328
312
|
return span
|
|
329
313
|
|
|
@@ -347,7 +331,7 @@ class DefaultSpanHandler(SpanHandler):
|
|
|
347
331
|
)
|
|
348
332
|
)
|
|
349
333
|
else:
|
|
350
|
-
return LangfuseSpan(self.tracer.
|
|
334
|
+
return LangfuseSpan(self.tracer.start_as_current_observation(name=context.name))
|
|
351
335
|
|
|
352
336
|
def handle(self, span: LangfuseSpan, component_type: str | None) -> None:
|
|
353
337
|
"""Process and enrich a span after component execution."""
|
|
@@ -356,7 +340,7 @@ class DefaultSpanHandler(SpanHandler):
|
|
|
356
340
|
if at_pipeline_level:
|
|
357
341
|
coerced_input = tracing_utils.coerce_tag_value(span.get_data().get(_PIPELINE_INPUT_KEY))
|
|
358
342
|
coerced_output = tracing_utils.coerce_tag_value(span.get_data().get(_PIPELINE_OUTPUT_KEY))
|
|
359
|
-
span.raw_span().
|
|
343
|
+
span.raw_span().update(input=coerced_input, output=coerced_output)
|
|
360
344
|
# special case for ToolInvoker (to update the span name to be: `original_component_name - [tool_names]`)
|
|
361
345
|
if component_type == "ToolInvoker":
|
|
362
346
|
tool_names: list[str] = []
|
|
@@ -468,6 +452,7 @@ class LangfuseTracer(Tracer):
|
|
|
468
452
|
tags = tags or {}
|
|
469
453
|
span_name = tags.get(_COMPONENT_NAME_KEY, operation_name)
|
|
470
454
|
component_type = tags.get(_COMPONENT_TYPE_KEY)
|
|
455
|
+
is_root_span = not (parent_span or self.current_span())
|
|
471
456
|
|
|
472
457
|
# Create a new span context
|
|
473
458
|
span_context = SpanContext(
|
|
@@ -482,74 +467,91 @@ class LangfuseTracer(Tracer):
|
|
|
482
467
|
public=self._public,
|
|
483
468
|
)
|
|
484
469
|
|
|
485
|
-
|
|
486
|
-
span = self._span_handler.create_span(span_context)
|
|
470
|
+
tracing_ctx = tracing_context_var.get({})
|
|
487
471
|
|
|
488
|
-
#
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
472
|
+
# we need to properly exit context managers
|
|
473
|
+
with contextlib.ExitStack() as stack:
|
|
474
|
+
if is_root_span:
|
|
475
|
+
# propagate_attributes sets trace_name and other trace-level metadata on the root
|
|
476
|
+
# span and ALL child spans created within this context (full trace lifetime)
|
|
477
|
+
stack.enter_context(
|
|
478
|
+
propagate_attributes(
|
|
479
|
+
trace_name=self._name,
|
|
480
|
+
user_id=tracing_ctx.get("user_id") if tracing_ctx.get("user_id") is not None else None,
|
|
481
|
+
session_id=tracing_ctx.get("session_id") if tracing_ctx.get("session_id") is not None else None,
|
|
482
|
+
version=tracing_ctx.get("version") if tracing_ctx.get("version") is not None else None,
|
|
483
|
+
tags=tracing_ctx.get("tags") if tracing_ctx.get("tags") is not None else None,
|
|
484
|
+
)
|
|
485
|
+
)
|
|
493
486
|
|
|
494
|
-
|
|
487
|
+
# Create span using the handler
|
|
488
|
+
span = self._span_handler.create_span(span_context)
|
|
495
489
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
exc_info = sys.exc_info()
|
|
502
|
-
try:
|
|
503
|
-
# Process span data (may fail with nested pipeline exceptions)
|
|
504
|
-
self._span_handler.handle(span, component_type)
|
|
505
|
-
|
|
506
|
-
# End span with exception info (may fail if span data is corrupted)
|
|
507
|
-
raw_span = span.raw_span()
|
|
508
|
-
if span._context_manager is not None:
|
|
509
|
-
# Pass actual exception info to mark span as failed with ERROR level
|
|
510
|
-
span._context_manager.__exit__(*exc_info)
|
|
511
|
-
elif hasattr(raw_span, "end"):
|
|
512
|
-
# Only call end() if it's not a context manager
|
|
513
|
-
raw_span.end()
|
|
514
|
-
except Exception as cleanup_error:
|
|
515
|
-
# Log cleanup errors but don't let them corrupt context
|
|
516
|
-
logger.warning(
|
|
517
|
-
"Error during span cleanup for {operation_name}: {cleanup_error}",
|
|
518
|
-
operation_name=operation_name,
|
|
519
|
-
cleanup_error=cleanup_error,
|
|
520
|
-
)
|
|
490
|
+
# Build new span hierarchy: copy existing stack, add new span, save for restoration
|
|
491
|
+
prev_stack = span_stack_var.get()
|
|
492
|
+
new_stack = (prev_stack or []).copy()
|
|
493
|
+
new_stack.append(span)
|
|
494
|
+
token = span_stack_var.set(new_stack)
|
|
521
495
|
|
|
522
|
-
|
|
523
|
-
raise
|
|
524
|
-
else:
|
|
525
|
-
# No exception - clean exit with success status
|
|
526
|
-
# This preserves any manually-set log levels (WARNING, DEBUG)
|
|
527
|
-
try:
|
|
528
|
-
# Process span data
|
|
529
|
-
self._span_handler.handle(span, component_type)
|
|
530
|
-
|
|
531
|
-
# End span successfully
|
|
532
|
-
raw_span = span.raw_span()
|
|
533
|
-
# In v3, we need to properly exit context managers
|
|
534
|
-
if span._context_manager is not None:
|
|
535
|
-
# No exception - pass None to indicate success
|
|
536
|
-
span._context_manager.__exit__(None, None, None)
|
|
537
|
-
elif hasattr(raw_span, "end"):
|
|
538
|
-
# Only call end() if it's not a context manager
|
|
539
|
-
raw_span.end()
|
|
540
|
-
except Exception as cleanup_error:
|
|
541
|
-
# Log cleanup errors but don't let them corrupt context
|
|
542
|
-
logger.warning(
|
|
543
|
-
"Error during span cleanup for {operation_name}: {cleanup_error}",
|
|
544
|
-
operation_name=operation_name,
|
|
545
|
-
cleanup_error=cleanup_error,
|
|
546
|
-
)
|
|
547
|
-
finally:
|
|
548
|
-
# Restore previous span stack using saved token
|
|
549
|
-
span_stack_var.reset(token)
|
|
496
|
+
span.set_tags(tags)
|
|
550
497
|
|
|
551
|
-
|
|
552
|
-
|
|
498
|
+
try:
|
|
499
|
+
yield span
|
|
500
|
+
except Exception:
|
|
501
|
+
# Exception occurred - capture exception info and pass to __exit__
|
|
502
|
+
# This allows Langfuse/OpenTelemetry to properly mark the span with ERROR level
|
|
503
|
+
exc_info = sys.exc_info()
|
|
504
|
+
try:
|
|
505
|
+
# Process span data (may fail with nested pipeline exceptions)
|
|
506
|
+
self._span_handler.handle(span, component_type)
|
|
507
|
+
|
|
508
|
+
# End span with exception info (may fail if span data is corrupted)
|
|
509
|
+
raw_span = span.raw_span()
|
|
510
|
+
if span._context_manager is not None:
|
|
511
|
+
# Pass actual exception info to mark span as failed with ERROR level
|
|
512
|
+
span._context_manager.__exit__(*exc_info)
|
|
513
|
+
elif hasattr(raw_span, "end"):
|
|
514
|
+
# Only call end() if it's not a context manager
|
|
515
|
+
raw_span.end()
|
|
516
|
+
except Exception as cleanup_error:
|
|
517
|
+
# Log cleanup errors but don't let them corrupt context
|
|
518
|
+
logger.warning(
|
|
519
|
+
"Error during span cleanup for {operation_name}: {cleanup_error}",
|
|
520
|
+
operation_name=operation_name,
|
|
521
|
+
cleanup_error=cleanup_error,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Re-raise the original exception
|
|
525
|
+
raise
|
|
526
|
+
else:
|
|
527
|
+
# No exception - clean exit with success status
|
|
528
|
+
# This preserves any manually-set log levels (WARNING, DEBUG)
|
|
529
|
+
try:
|
|
530
|
+
# Process span data
|
|
531
|
+
self._span_handler.handle(span, component_type)
|
|
532
|
+
|
|
533
|
+
# End span successfully
|
|
534
|
+
raw_span = span.raw_span()
|
|
535
|
+
# In v3, we need to properly exit context managers
|
|
536
|
+
if span._context_manager is not None:
|
|
537
|
+
# No exception - pass None to indicate success
|
|
538
|
+
span._context_manager.__exit__(None, None, None)
|
|
539
|
+
elif hasattr(raw_span, "end"):
|
|
540
|
+
# Only call end() if it's not a context manager
|
|
541
|
+
raw_span.end()
|
|
542
|
+
except Exception as cleanup_error:
|
|
543
|
+
# Log cleanup errors but don't let them corrupt context
|
|
544
|
+
logger.warning(
|
|
545
|
+
"Error during span cleanup for {operation_name}: {cleanup_error}",
|
|
546
|
+
operation_name=operation_name,
|
|
547
|
+
cleanup_error=cleanup_error,
|
|
548
|
+
)
|
|
549
|
+
finally:
|
|
550
|
+
# Restore previous span stack using saved token
|
|
551
|
+
span_stack_var.reset(token)
|
|
552
|
+
|
|
553
|
+
if self.enforce_flush:
|
|
554
|
+
self.flush()
|
|
553
555
|
|
|
554
556
|
def flush(self) -> None:
|
|
555
557
|
"""Flush all pending spans to Langfuse."""
|
|
@@ -22,17 +22,15 @@ from haystack_integrations.tracing.langfuse.tracer import (
|
|
|
22
22
|
)
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
# Mock functions for Langfuse v3 API
|
|
26
25
|
def mock_get_client():
|
|
27
26
|
mock_client = Mock()
|
|
28
|
-
mock_client.start_as_current_span = Mock(return_value=MockContextManager())
|
|
29
27
|
mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
|
|
30
28
|
mock_client.get_current_trace_id = Mock(return_value="mock_trace_id_123")
|
|
31
29
|
return mock_client
|
|
32
30
|
|
|
33
31
|
|
|
34
32
|
class MockContextManager:
|
|
35
|
-
"""Mock context manager that simulates Langfuse
|
|
33
|
+
"""Mock context manager that simulates Langfuse v4 context managers"""
|
|
36
34
|
|
|
37
35
|
def __init__(self, name="mock_span"):
|
|
38
36
|
self._span = MockSpan(name)
|
|
@@ -63,9 +61,8 @@ class MockSpan:
|
|
|
63
61
|
# Return a new mock span for child spans
|
|
64
62
|
return MockSpan(name=name or "child_span")
|
|
65
63
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
self._data.update(kwargs)
|
|
64
|
+
def set_trace_as_public(self):
|
|
65
|
+
self._data["public"] = True
|
|
69
66
|
|
|
70
67
|
def generation(self, name=None):
|
|
71
68
|
# Return a new mock span for generation spans
|
|
@@ -90,9 +87,6 @@ class MockLangfuseClient:
|
|
|
90
87
|
def __init__(self):
|
|
91
88
|
self._mock_context_manager = MockContextManager()
|
|
92
89
|
|
|
93
|
-
def start_as_current_span(self, _name=None, **_kwargs):
|
|
94
|
-
return self._mock_context_manager
|
|
95
|
-
|
|
96
90
|
def start_as_current_observation(self, _name=None, _as_type=None, **_kwargs):
|
|
97
91
|
return self._mock_context_manager
|
|
98
92
|
|
|
@@ -134,57 +128,57 @@ class TestLangfuseSpan:
|
|
|
134
128
|
# set_content_tag method can update input and output of the span object
|
|
135
129
|
def test_set_content_tag_updates_input_and_output(self):
|
|
136
130
|
mock_context_manager = MockContextManager()
|
|
137
|
-
|
|
138
131
|
span = LangfuseSpan(mock_context_manager)
|
|
139
|
-
span.set_content_tag("test.input", "input_value")
|
|
140
|
-
# Check that the span.update method was called with input parameter
|
|
141
|
-
mock_context_manager._span.update.assert_called_with(input="input_value")
|
|
142
132
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
with patch("haystack_integrations.tracing.langfuse.tracer.proxy_tracer.is_content_tracing_enabled", True):
|
|
134
|
+
span.set_content_tag("test.input", "input_value")
|
|
135
|
+
mock_context_manager._span.update.assert_called_with(input="input_value")
|
|
136
|
+
|
|
137
|
+
mock_context_manager._span.update.reset_mock()
|
|
138
|
+
span.set_content_tag("test.output", "output_value")
|
|
139
|
+
mock_context_manager._span.update.assert_called_with(output="output_value")
|
|
147
140
|
|
|
148
141
|
# set_content_tag method can update input and output of the span object with messages/replies
|
|
149
142
|
def test_set_content_tag_updates_input_and_output_with_messages(self):
|
|
150
143
|
mock_context_manager = MockContextManager()
|
|
151
|
-
|
|
152
144
|
span = LangfuseSpan(mock_context_manager)
|
|
153
|
-
span.set_content_tag("key.input", {"messages": [ChatMessage.from_user("message")]})
|
|
154
|
-
assert mock_context_manager._span.update.call_count == 1
|
|
155
|
-
# check we converted ChatMessage to OpenAI format
|
|
156
|
-
assert mock_context_manager._span.update.call_args_list[0][1] == {
|
|
157
|
-
"input": [{"role": "user", "content": "message"}]
|
|
158
|
-
}
|
|
159
145
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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},
|
|
146
|
+
with patch("haystack_integrations.tracing.langfuse.tracer.proxy_tracer.is_content_tracing_enabled", True):
|
|
147
|
+
span.set_content_tag("key.input", {"messages": [ChatMessage.from_user("message")]})
|
|
148
|
+
assert mock_context_manager._span.update.call_count == 1
|
|
149
|
+
# check we converted ChatMessage to OpenAI format
|
|
150
|
+
assert mock_context_manager._span.update.call_args_list[0][1] == {
|
|
151
|
+
"input": [{"role": "user", "content": "message"}]
|
|
171
152
|
}
|
|
172
|
-
}
|
|
173
|
-
# test replies ChatMessage list
|
|
174
|
-
mock_context_manager._span.update.reset_mock()
|
|
175
|
-
span.set_content_tag("key.output", {"replies": [ChatMessage.from_system("reply")]})
|
|
176
|
-
assert mock_context_manager._span.update.call_count == 1
|
|
177
|
-
# check we converted ChatMessage to OpenAI format
|
|
178
|
-
assert mock_context_manager._span.update.call_args_list[0][1] == {
|
|
179
|
-
"output": [{"role": "system", "content": "reply"}]
|
|
180
|
-
}
|
|
181
153
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
154
|
+
# test generation_kwargs are captured
|
|
155
|
+
mock_context_manager._span.update.reset_mock()
|
|
156
|
+
span.set_content_tag(
|
|
157
|
+
"key.input",
|
|
158
|
+
{"messages": [ChatMessage.from_user("message")], "generation_kwargs": {"n": 2, "temperature": 0.7}},
|
|
159
|
+
)
|
|
160
|
+
assert mock_context_manager._span.update.call_count == 1
|
|
161
|
+
assert mock_context_manager._span.update.call_args_list[0][1] == {
|
|
162
|
+
"input": {
|
|
163
|
+
"messages": [{"role": "user", "content": "message"}],
|
|
164
|
+
"generation_kwargs": {"n": 2, "temperature": 0.7},
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
# test replies ChatMessage list
|
|
168
|
+
mock_context_manager._span.update.reset_mock()
|
|
169
|
+
span.set_content_tag("key.output", {"replies": [ChatMessage.from_system("reply")]})
|
|
170
|
+
assert mock_context_manager._span.update.call_count == 1
|
|
171
|
+
# check we converted ChatMessage to OpenAI format
|
|
172
|
+
assert mock_context_manager._span.update.call_args_list[0][1] == {
|
|
173
|
+
"output": [{"role": "system", "content": "reply"}]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# test replies string list
|
|
177
|
+
mock_context_manager._span.update.reset_mock()
|
|
178
|
+
span.set_content_tag("key.output", {"replies": ["reply1", "reply2"]})
|
|
179
|
+
assert mock_context_manager._span.update.call_count == 1
|
|
180
|
+
# check we handle properly string list replies
|
|
181
|
+
assert mock_context_manager._span.update.call_args_list[0][1] == {"output": ["reply1", "reply2"]}
|
|
188
182
|
|
|
189
183
|
|
|
190
184
|
class TestSpanContext:
|
|
@@ -239,7 +233,6 @@ class TestDefaultSpanHandler:
|
|
|
239
233
|
def test_create_span_root_trace(self):
|
|
240
234
|
"""Test creating a root span (no parent)."""
|
|
241
235
|
mock_client = Mock()
|
|
242
|
-
mock_client.start_as_current_span = Mock(return_value=MockContextManager())
|
|
243
236
|
mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
|
|
244
237
|
|
|
245
238
|
handler = DefaultSpanHandler()
|
|
@@ -274,6 +267,24 @@ class TestDefaultSpanHandler:
|
|
|
274
267
|
finally:
|
|
275
268
|
tracing_context_var.reset(token)
|
|
276
269
|
|
|
270
|
+
def test_create_span_root_trace_public(self):
|
|
271
|
+
"""Test that public=True calls set_trace_as_public on the span."""
|
|
272
|
+
mock_client = Mock()
|
|
273
|
+
mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
|
|
274
|
+
|
|
275
|
+
handler = DefaultSpanHandler()
|
|
276
|
+
handler.init_tracer(mock_client)
|
|
277
|
+
|
|
278
|
+
context = MagicMock(spec=SpanContext)
|
|
279
|
+
context.parent_span = None
|
|
280
|
+
context.trace_name = "public_trace"
|
|
281
|
+
context.operation_name = "haystack.pipeline.run"
|
|
282
|
+
context.public = True
|
|
283
|
+
|
|
284
|
+
result = handler.create_span(context)
|
|
285
|
+
|
|
286
|
+
assert result.raw_span()._data.get("public") is True
|
|
287
|
+
|
|
277
288
|
def test_handle_generator(self):
|
|
278
289
|
mock_span = Mock()
|
|
279
290
|
mock_span.raw_span.return_value = mock_span
|
|
@@ -345,7 +356,6 @@ class TestDefaultSpanHandler:
|
|
|
345
356
|
def test_create_span_custom_chat_generator(self):
|
|
346
357
|
"""Test that custom chat generators create 'generation' span type."""
|
|
347
358
|
mock_client = Mock()
|
|
348
|
-
mock_client.start_as_current_span = Mock(return_value=MockContextManager())
|
|
349
359
|
mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
|
|
350
360
|
|
|
351
361
|
handler = DefaultSpanHandler()
|
|
@@ -356,8 +366,9 @@ class TestDefaultSpanHandler:
|
|
|
356
366
|
operation_name="haystack.component.run",
|
|
357
367
|
component_type="MistralChatGenerator",
|
|
358
368
|
tags={},
|
|
359
|
-
parent_span=LangfuseSpan(mock_client.
|
|
369
|
+
parent_span=LangfuseSpan(mock_client.start_as_current_observation()),
|
|
360
370
|
)
|
|
371
|
+
mock_client.start_as_current_observation.reset_mock()
|
|
361
372
|
|
|
362
373
|
span = handler.create_span(context)
|
|
363
374
|
assert isinstance(span, LangfuseSpan)
|
|
@@ -368,7 +379,6 @@ class TestDefaultSpanHandler:
|
|
|
368
379
|
def test_create_span_custom_generator(self):
|
|
369
380
|
"""Test that custom generators create 'generation' span type."""
|
|
370
381
|
mock_client = Mock()
|
|
371
|
-
mock_client.start_as_current_span = Mock(return_value=MockContextManager())
|
|
372
382
|
mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
|
|
373
383
|
|
|
374
384
|
handler = DefaultSpanHandler()
|
|
@@ -379,8 +389,9 @@ class TestDefaultSpanHandler:
|
|
|
379
389
|
operation_name="haystack.component.run",
|
|
380
390
|
component_type="CustomAPIGenerator",
|
|
381
391
|
tags={},
|
|
382
|
-
parent_span=LangfuseSpan(mock_client.
|
|
392
|
+
parent_span=LangfuseSpan(mock_client.start_as_current_observation()),
|
|
383
393
|
)
|
|
394
|
+
mock_client.start_as_current_observation.reset_mock()
|
|
384
395
|
|
|
385
396
|
span = handler.create_span(context)
|
|
386
397
|
assert isinstance(span, LangfuseSpan)
|
|
@@ -391,7 +402,6 @@ class TestDefaultSpanHandler:
|
|
|
391
402
|
def test_create_span_retriever(self):
|
|
392
403
|
"""Test that retrievers create 'retriever' span type."""
|
|
393
404
|
mock_client = Mock()
|
|
394
|
-
mock_client.start_as_current_span = Mock(return_value=MockContextManager())
|
|
395
405
|
mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
|
|
396
406
|
|
|
397
407
|
handler = DefaultSpanHandler()
|
|
@@ -402,8 +412,9 @@ class TestDefaultSpanHandler:
|
|
|
402
412
|
operation_name="haystack.component.run",
|
|
403
413
|
component_type="InMemoryBM25Retriever",
|
|
404
414
|
tags={},
|
|
405
|
-
parent_span=LangfuseSpan(mock_client.
|
|
415
|
+
parent_span=LangfuseSpan(mock_client.start_as_current_observation()),
|
|
406
416
|
)
|
|
417
|
+
mock_client.start_as_current_observation.reset_mock()
|
|
407
418
|
|
|
408
419
|
span = handler.create_span(context)
|
|
409
420
|
assert isinstance(span, LangfuseSpan)
|
|
@@ -414,7 +425,6 @@ class TestDefaultSpanHandler:
|
|
|
414
425
|
def test_create_span_embedder(self):
|
|
415
426
|
"""Test that embedders create 'embedding' span type."""
|
|
416
427
|
mock_client = Mock()
|
|
417
|
-
mock_client.start_as_current_span = Mock(return_value=MockContextManager())
|
|
418
428
|
mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
|
|
419
429
|
|
|
420
430
|
handler = DefaultSpanHandler()
|
|
@@ -425,8 +435,9 @@ class TestDefaultSpanHandler:
|
|
|
425
435
|
operation_name="haystack.component.run",
|
|
426
436
|
component_type="SentenceTransformersDocumentEmbedder",
|
|
427
437
|
tags={},
|
|
428
|
-
parent_span=LangfuseSpan(mock_client.
|
|
438
|
+
parent_span=LangfuseSpan(mock_client.start_as_current_observation()),
|
|
429
439
|
)
|
|
440
|
+
mock_client.start_as_current_observation.reset_mock()
|
|
430
441
|
|
|
431
442
|
span = handler.create_span(context)
|
|
432
443
|
assert isinstance(span, LangfuseSpan)
|
|
@@ -435,9 +446,8 @@ class TestDefaultSpanHandler:
|
|
|
435
446
|
)
|
|
436
447
|
|
|
437
448
|
def test_create_span_non_component(self):
|
|
438
|
-
"""Test that non-matching components create
|
|
449
|
+
"""Test that non-matching components create default span type."""
|
|
439
450
|
mock_client = Mock()
|
|
440
|
-
mock_client.start_as_current_span = Mock(return_value=MockContextManager())
|
|
441
451
|
mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
|
|
442
452
|
|
|
443
453
|
handler = DefaultSpanHandler()
|
|
@@ -448,15 +458,14 @@ class TestDefaultSpanHandler:
|
|
|
448
458
|
operation_name="haystack.component.run",
|
|
449
459
|
component_type="DocumentJoiner",
|
|
450
460
|
tags={},
|
|
451
|
-
parent_span=LangfuseSpan(mock_client.
|
|
461
|
+
parent_span=LangfuseSpan(mock_client.start_as_current_observation()),
|
|
452
462
|
)
|
|
463
|
+
mock_client.start_as_current_observation.reset_mock()
|
|
453
464
|
|
|
454
465
|
span = handler.create_span(context)
|
|
455
466
|
assert isinstance(span, LangfuseSpan)
|
|
456
|
-
# Non-matching components
|
|
457
|
-
mock_client.start_as_current_observation.
|
|
458
|
-
# Verify start_as_current_span was called for the actual span creation (not just parent)
|
|
459
|
-
assert mock_client.start_as_current_span.call_count == 2 # Once for parent, once for the span
|
|
467
|
+
# Non-matching components use start_as_current_observation without explicit as_type
|
|
468
|
+
mock_client.start_as_current_observation.assert_called_once_with(name="DocumentJoiner")
|
|
460
469
|
|
|
461
470
|
def test_handle_embedder_with_openai_format(self):
|
|
462
471
|
"""Test that embedder usage is extracted in OpenAI format."""
|
|
@@ -600,7 +609,6 @@ class TestLangfuseTracer:
|
|
|
600
609
|
mock_span_instance._context_manager = mock_context_manager
|
|
601
610
|
|
|
602
611
|
mock_tracer = Mock()
|
|
603
|
-
mock_tracer.start_as_current_span.return_value = mock_context_manager
|
|
604
612
|
mock_tracer.start_as_current_observation.return_value = mock_context_manager
|
|
605
613
|
|
|
606
614
|
tracer = LangfuseTracer(tracer=mock_tracer, name="Haystack", public=False)
|
|
@@ -616,15 +624,42 @@ class TestLangfuseTracer:
|
|
|
616
624
|
# Check that the span is cleaned up after tracing
|
|
617
625
|
assert tracer.current_span() is None, "There should be no active span after tracing completes"
|
|
618
626
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
with
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
627
|
+
def test_pipeline_input_output_written_to_root_observation(self):
|
|
628
|
+
tracer = LangfuseTracer(tracer=MockLangfuseClient(), name="Haystack", public=False)
|
|
629
|
+
with tracer.trace(
|
|
630
|
+
operation_name="operation_name",
|
|
631
|
+
tags={
|
|
632
|
+
"haystack.pipeline.input_data": "hello",
|
|
633
|
+
"haystack.pipeline.output_data": "bye",
|
|
634
|
+
},
|
|
635
|
+
) as span:
|
|
636
|
+
pass
|
|
625
637
|
|
|
626
|
-
|
|
627
|
-
|
|
638
|
+
# MockSpan._update_data mirrors kwargs into _data, so after the context exits
|
|
639
|
+
# we can verify update(input=..., output=...) was actually called.
|
|
640
|
+
assert span.raw_span()._data["input"] == "hello"
|
|
641
|
+
assert span.raw_span()._data["output"] == "bye"
|
|
642
|
+
|
|
643
|
+
def test_propagate_attributes_called_with_tracing_ctx_vars(self):
|
|
644
|
+
token = tracing_context_var.set(
|
|
645
|
+
{"user_id": "user123", "session_id": "sess456", "version": "v1", "tags": ["tag1"]}
|
|
646
|
+
)
|
|
647
|
+
try:
|
|
648
|
+
with patch("haystack_integrations.tracing.langfuse.tracer.propagate_attributes") as mock_propagate:
|
|
649
|
+
mock_propagate.return_value.__enter__ = Mock(return_value=None)
|
|
650
|
+
mock_propagate.return_value.__exit__ = Mock(return_value=False)
|
|
651
|
+
tracer = LangfuseTracer(tracer=MockLangfuseClient(), name="MyPipeline", public=False)
|
|
652
|
+
with tracer.trace(operation_name="haystack.pipeline.run"):
|
|
653
|
+
pass
|
|
654
|
+
mock_propagate.assert_called_once_with(
|
|
655
|
+
trace_name="MyPipeline",
|
|
656
|
+
user_id="user123",
|
|
657
|
+
session_id="sess456",
|
|
658
|
+
version="v1",
|
|
659
|
+
tags=["tag1"],
|
|
660
|
+
)
|
|
661
|
+
finally:
|
|
662
|
+
tracing_context_var.reset(token)
|
|
628
663
|
|
|
629
664
|
def test_trace_generation(self):
|
|
630
665
|
with patch("haystack_integrations.tracing.langfuse.tracer.langfuse.get_client"):
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langfuse_haystack-4.1.0 → langfuse_haystack-5.0.0}/src/haystack_integrations/tracing/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|