langfuse-haystack 2.3.0__tar.gz → 3.1.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-2.3.0 → langfuse_haystack-3.1.0}/CHANGELOG.md +21 -0
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/PKG-INFO +3 -3
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/example/basic_rag.py +2 -2
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/example/chat.py +2 -3
- langfuse_haystack-3.1.0/pydoc/config_docusaurus.yml +29 -0
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/pyproject.toml +7 -28
- langfuse_haystack-3.1.0/src/haystack_integrations/components/connectors/py.typed +0 -0
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/src/haystack_integrations/tracing/langfuse/tracer.py +46 -29
- langfuse_haystack-3.1.0/src/haystack_integrations/tracing/py.typed +0 -0
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/tests/test_tracer.py +143 -88
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/tests/test_tracing.py +27 -19
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/.gitignore +0 -0
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/LICENSE.txt +0 -0
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/README.md +0 -0
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/example/requirements.txt +0 -0
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/pydoc/config.yml +0 -0
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/src/haystack_integrations/components/connectors/__init__.py +0 -0
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +0 -0
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/tests/__init__.py +0 -0
- {langfuse_haystack-2.3.0 → langfuse_haystack-3.1.0}/tests/test_langfuse_connector.py +0 -0
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [integrations/langfuse-v3.0.0] - 2025-09-19
|
|
4
|
+
|
|
5
|
+
### 🌀 Miscellaneous
|
|
6
|
+
|
|
7
|
+
- Migrate langfuse to v3 (#2247)
|
|
8
|
+
|
|
9
|
+
## [integrations/langfuse-v2.3.0] - 2025-08-25
|
|
10
|
+
|
|
11
|
+
### 🐛 Bug Fixes
|
|
12
|
+
|
|
13
|
+
- Avoid mixed Langfuse traces in async envs (#2207)
|
|
14
|
+
|
|
15
|
+
### 🧪 Testing
|
|
16
|
+
|
|
17
|
+
- Make tests successfully run from forks (#2203)
|
|
18
|
+
|
|
19
|
+
### 🧹 Chores
|
|
20
|
+
|
|
21
|
+
- Standardize readmes - part 2 (#2205)
|
|
22
|
+
|
|
23
|
+
|
|
3
24
|
## [integrations/langfuse-v2.2.1] - 2025-08-07
|
|
4
25
|
|
|
5
26
|
### 🚀 Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langfuse-haystack
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.1.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,8 +18,8 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
18
18
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
19
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
20
|
Requires-Python: >=3.9
|
|
21
|
-
Requires-Dist: haystack-ai>=2.
|
|
22
|
-
Requires-Dist: langfuse<
|
|
21
|
+
Requires-Dist: haystack-ai>=2.17.1
|
|
22
|
+
Requires-Dist: langfuse<4.0.0,>=3.3.0
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
|
|
25
25
|
# langfuse-haystack
|
|
@@ -62,5 +62,5 @@ if __name__ == "__main__":
|
|
|
62
62
|
question = "What does Rhodes Statue look like?"
|
|
63
63
|
response = pipeline.run({"text_embedder": {"text": question}, "prompt_builder": {"question": question}})
|
|
64
64
|
|
|
65
|
-
print(response["llm"]["replies"][0])
|
|
66
|
-
print(response["tracer"]["trace_url"])
|
|
65
|
+
print(response["llm"]["replies"][0]) # noqa: T201
|
|
66
|
+
print(response["tracer"]["trace_url"]) # noqa: T201
|
|
@@ -36,7 +36,6 @@ generators = {
|
|
|
36
36
|
selected_chat_generator = generators[selected_chat_generator]()
|
|
37
37
|
|
|
38
38
|
if __name__ == "__main__":
|
|
39
|
-
|
|
40
39
|
pipe = Pipeline()
|
|
41
40
|
pipe.add_component("tracer", LangfuseConnector("Chat example"))
|
|
42
41
|
pipe.add_component("prompt_builder", ChatPromptBuilder())
|
|
@@ -60,5 +59,5 @@ if __name__ == "__main__":
|
|
|
60
59
|
},
|
|
61
60
|
}
|
|
62
61
|
)
|
|
63
|
-
print(response["llm"]["replies"][0])
|
|
64
|
-
print(response["tracer"]["trace_url"])
|
|
62
|
+
print(response["llm"]["replies"][0]) # noqa: T201
|
|
63
|
+
print(response["tracer"]["trace_url"]) # noqa: T201
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
loaders:
|
|
2
|
+
- ignore_when_discovered:
|
|
3
|
+
- __init__
|
|
4
|
+
modules:
|
|
5
|
+
- haystack_integrations.components.connectors.langfuse.langfuse_connector
|
|
6
|
+
- haystack_integrations.tracing.langfuse.tracer
|
|
7
|
+
search_path:
|
|
8
|
+
- ../src
|
|
9
|
+
type: haystack_pydoc_tools.loaders.CustomPythonLoader
|
|
10
|
+
processors:
|
|
11
|
+
- do_not_filter_modules: false
|
|
12
|
+
documented_only: true
|
|
13
|
+
expression: null
|
|
14
|
+
skip_empty_modules: true
|
|
15
|
+
type: filter
|
|
16
|
+
- type: smart
|
|
17
|
+
- type: crossref
|
|
18
|
+
renderer:
|
|
19
|
+
description: Langfuse integration for Haystack
|
|
20
|
+
id: integrations-langfuse
|
|
21
|
+
markdown:
|
|
22
|
+
add_member_class_prefix: false
|
|
23
|
+
add_method_class_prefix: true
|
|
24
|
+
classdef_code_block: false
|
|
25
|
+
descriptive_class_title: false
|
|
26
|
+
descriptive_module_title: true
|
|
27
|
+
filename: langfuse.md
|
|
28
|
+
title: langfuse
|
|
29
|
+
type: haystack_pydoc_tools.renderers.DocusaurusRenderer
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
23
23
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
24
24
|
]
|
|
25
|
-
dependencies = ["haystack-ai>=2.
|
|
25
|
+
dependencies = ["haystack-ai>=2.17.1", "langfuse>=3.3.0, <4.0.0"]
|
|
26
26
|
|
|
27
27
|
[project.urls]
|
|
28
28
|
Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse#readme"
|
|
@@ -69,22 +69,13 @@ integration = 'pytest -m "integration" {args:tests}'
|
|
|
69
69
|
all = 'pytest {args:tests}'
|
|
70
70
|
cov-retry = 'all --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x'
|
|
71
71
|
|
|
72
|
-
types = "mypy
|
|
72
|
+
types = "mypy -p haystack_integrations.components.connectors.langfuse -p haystack_integrations.tracing.langfuse {args}"
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
detached = true
|
|
80
|
-
dependencies = [
|
|
81
|
-
"pip",
|
|
82
|
-
"mypy>=1.0.0",
|
|
83
|
-
"ruff>=0.0.243",
|
|
84
|
-
]
|
|
85
|
-
|
|
86
|
-
[tool.hatch.envs.lint.scripts]
|
|
87
|
-
typing = "mypy --install-types --non-interactive --explicit-package-bases {args:src/ tests}"
|
|
74
|
+
[tool.mypy]
|
|
75
|
+
install_types = true
|
|
76
|
+
non_interactive = true
|
|
77
|
+
check_untyped_defs = true
|
|
78
|
+
disallow_incomplete_defs = true
|
|
88
79
|
|
|
89
80
|
[tool.hatch.metadata]
|
|
90
81
|
allow-direct-references = true
|
|
@@ -93,7 +84,6 @@ allow-direct-references = true
|
|
|
93
84
|
[tool.ruff]
|
|
94
85
|
target-version = "py38"
|
|
95
86
|
line-length = 120
|
|
96
|
-
exclude = ["example", "tests"]
|
|
97
87
|
|
|
98
88
|
[tool.ruff.lint]
|
|
99
89
|
select = [
|
|
@@ -167,17 +157,6 @@ omit = ["*/tests/*", "*/__init__.py"]
|
|
|
167
157
|
show_missing = true
|
|
168
158
|
exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
|
|
169
159
|
|
|
170
|
-
[[tool.mypy.overrides]]
|
|
171
|
-
module = [
|
|
172
|
-
"langfuse.*",
|
|
173
|
-
"haystack.*",
|
|
174
|
-
"haystack_integrations.*",
|
|
175
|
-
"pytest.*",
|
|
176
|
-
"numpy.*",
|
|
177
|
-
"httpx.*",
|
|
178
|
-
]
|
|
179
|
-
ignore_missing_imports = true
|
|
180
|
-
|
|
181
160
|
[tool.pytest.ini_options]
|
|
182
161
|
addopts = "--strict-markers"
|
|
183
162
|
markers = ["integration: integration tests"]
|
|
File without changes
|
|
@@ -6,24 +6,21 @@ import contextlib
|
|
|
6
6
|
import os
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
from collections import Counter
|
|
9
|
+
from contextlib import AbstractContextManager
|
|
9
10
|
from contextvars import ContextVar
|
|
10
11
|
from dataclasses import dataclass
|
|
11
12
|
from datetime import datetime
|
|
12
|
-
from typing import Any, Dict, Iterator, List, Optional
|
|
13
|
+
from typing import Any, Dict, Iterator, List, Optional
|
|
13
14
|
|
|
14
15
|
from haystack import default_from_dict, default_to_dict, logging
|
|
15
16
|
from haystack.dataclasses import ChatMessage
|
|
16
17
|
from haystack.tracing import Span, Tracer
|
|
17
18
|
from haystack.tracing import tracer as proxy_tracer
|
|
18
19
|
from haystack.tracing import utils as tracing_utils
|
|
19
|
-
from typing_extensions import TypeAlias
|
|
20
20
|
|
|
21
21
|
import langfuse
|
|
22
|
-
from langfuse
|
|
23
|
-
|
|
24
|
-
# Type alias for Langfuse stateful clients
|
|
25
|
-
LangfuseStatefulClient: TypeAlias = Union[StatefulTraceClient, StatefulSpanClient, StatefulGenerationClient]
|
|
26
|
-
|
|
22
|
+
from langfuse import LangfuseSpan as LangfuseClientSpan
|
|
23
|
+
from langfuse.types import TraceMetadata
|
|
27
24
|
|
|
28
25
|
logger = logging.getLogger(__name__)
|
|
29
26
|
|
|
@@ -75,15 +72,17 @@ class LangfuseSpan(Span):
|
|
|
75
72
|
Internal class representing a bridge between the Haystack span tracing API and Langfuse.
|
|
76
73
|
"""
|
|
77
74
|
|
|
78
|
-
def __init__(self,
|
|
75
|
+
def __init__(self, context_manager: AbstractContextManager) -> None:
|
|
79
76
|
"""
|
|
80
77
|
Initialize a LangfuseSpan instance.
|
|
81
78
|
|
|
82
|
-
:param
|
|
79
|
+
:param context_manager: The context manager from Langfuse created with
|
|
80
|
+
`langfuse.get_client().start_as_current_span` or
|
|
81
|
+
`langfuse.get_client().start_as_current_observation`.
|
|
83
82
|
"""
|
|
84
|
-
self._span =
|
|
85
|
-
# locally cache tags
|
|
83
|
+
self._span = context_manager.__enter__()
|
|
86
84
|
self._data: Dict[str, Any] = {}
|
|
85
|
+
self._context_manager = context_manager
|
|
87
86
|
|
|
88
87
|
def set_tag(self, key: str, value: Any) -> None:
|
|
89
88
|
"""
|
|
@@ -125,7 +124,7 @@ class LangfuseSpan(Span):
|
|
|
125
124
|
|
|
126
125
|
self._data[key] = value
|
|
127
126
|
|
|
128
|
-
def raw_span(self) ->
|
|
127
|
+
def raw_span(self) -> LangfuseClientSpan:
|
|
129
128
|
"""
|
|
130
129
|
Return the underlying span instance.
|
|
131
130
|
|
|
@@ -273,21 +272,36 @@ class DefaultSpanHandler(SpanHandler):
|
|
|
273
272
|
tracing_ctx = tracing_context_var.get({})
|
|
274
273
|
if not context.parent_span:
|
|
275
274
|
# Create a new trace when there's no parent span
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
public=context.public,
|
|
280
|
-
id=tracing_ctx.get("trace_id"),
|
|
281
|
-
user_id=tracing_ctx.get("user_id"),
|
|
282
|
-
session_id=tracing_ctx.get("session_id"),
|
|
283
|
-
tags=tracing_ctx.get("tags"),
|
|
284
|
-
version=tracing_ctx.get("version"),
|
|
285
|
-
)
|
|
275
|
+
span_context_manager = self.tracer.start_as_current_span(
|
|
276
|
+
name=context.trace_name,
|
|
277
|
+
version=tracing_ctx.get("version"),
|
|
286
278
|
)
|
|
279
|
+
|
|
280
|
+
# Create LangfuseSpan which will handle entering the context manager
|
|
281
|
+
span = LangfuseSpan(span_context_manager)
|
|
282
|
+
|
|
283
|
+
# Build trace metadata from context
|
|
284
|
+
trace_metadata: TraceMetadata = {
|
|
285
|
+
"name": context.trace_name,
|
|
286
|
+
"user_id": tracing_ctx.get("user_id"),
|
|
287
|
+
"session_id": tracing_ctx.get("session_id"),
|
|
288
|
+
"version": tracing_ctx.get("version"),
|
|
289
|
+
"metadata": None,
|
|
290
|
+
"tags": tracing_ctx.get("tags"),
|
|
291
|
+
"public": context.public,
|
|
292
|
+
"release": None,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# Filter out None values and apply trace attributes
|
|
296
|
+
trace_attrs = {k: v for k, v in trace_metadata.items() if v is not None}
|
|
297
|
+
if trace_attrs:
|
|
298
|
+
span._span.update_trace(**trace_attrs)
|
|
299
|
+
|
|
300
|
+
return span
|
|
287
301
|
elif context.component_type in _ALL_SUPPORTED_GENERATORS:
|
|
288
|
-
return LangfuseSpan(
|
|
302
|
+
return LangfuseSpan(self.tracer.start_as_current_observation(name=context.name, as_type="generation"))
|
|
289
303
|
else:
|
|
290
|
-
return LangfuseSpan(
|
|
304
|
+
return LangfuseSpan(self.tracer.start_as_current_span(name=context.name))
|
|
291
305
|
|
|
292
306
|
def handle(self, span: LangfuseSpan, component_type: Optional[str]) -> None:
|
|
293
307
|
# If the span is at the pipeline level, we add input and output keys to the span
|
|
@@ -295,8 +309,7 @@ class DefaultSpanHandler(SpanHandler):
|
|
|
295
309
|
if at_pipeline_level:
|
|
296
310
|
coerced_input = tracing_utils.coerce_tag_value(span.get_data().get(_PIPELINE_INPUT_KEY))
|
|
297
311
|
coerced_output = tracing_utils.coerce_tag_value(span.get_data().get(_PIPELINE_OUTPUT_KEY))
|
|
298
|
-
span.raw_span().
|
|
299
|
-
|
|
312
|
+
span.raw_span().update_trace(input=coerced_input, output=coerced_output)
|
|
300
313
|
# special case for ToolInvoker (to update the span name to be: `original_component_name - [tool_names]`)
|
|
301
314
|
if component_type == "ToolInvoker":
|
|
302
315
|
tool_names: List[str] = []
|
|
@@ -415,7 +428,11 @@ class LangfuseTracer(Tracer):
|
|
|
415
428
|
|
|
416
429
|
# End span (may fail if span data is corrupted)
|
|
417
430
|
raw_span = span.raw_span()
|
|
418
|
-
|
|
431
|
+
# In v3, we need to properly exit context managers
|
|
432
|
+
if span._context_manager is not None:
|
|
433
|
+
span._context_manager.__exit__(None, None, None)
|
|
434
|
+
elif hasattr(raw_span, "end"):
|
|
435
|
+
# Only call end() if it's not a context manager
|
|
419
436
|
raw_span.end()
|
|
420
437
|
except Exception as cleanup_error:
|
|
421
438
|
# Log cleanup errors but don't let them corrupt context
|
|
@@ -449,11 +466,11 @@ class LangfuseTracer(Tracer):
|
|
|
449
466
|
Return the URL to the tracing data.
|
|
450
467
|
:return: The URL to the tracing data.
|
|
451
468
|
"""
|
|
452
|
-
return self._tracer.get_trace_url()
|
|
469
|
+
return self._tracer.get_trace_url() or ""
|
|
453
470
|
|
|
454
471
|
def get_trace_id(self) -> str:
|
|
455
472
|
"""
|
|
456
473
|
Return the trace ID.
|
|
457
474
|
:return: The trace ID.
|
|
458
475
|
"""
|
|
459
|
-
return self._tracer.
|
|
476
|
+
return self._tracer.get_current_trace_id() or ""
|
|
File without changes
|
|
@@ -6,12 +6,10 @@ import asyncio
|
|
|
6
6
|
import datetime
|
|
7
7
|
import logging
|
|
8
8
|
import sys
|
|
9
|
-
import json
|
|
10
9
|
from typing import Optional
|
|
11
10
|
from unittest.mock import MagicMock, Mock, patch
|
|
12
11
|
|
|
13
12
|
import pytest
|
|
14
|
-
from haystack import Pipeline, component
|
|
15
13
|
from haystack.dataclasses import ChatMessage, ToolCall
|
|
16
14
|
|
|
17
15
|
from haystack_integrations.tracing.langfuse.tracer import (
|
|
@@ -21,15 +19,41 @@ from haystack_integrations.tracing.langfuse.tracer import (
|
|
|
21
19
|
LangfuseTracer,
|
|
22
20
|
SpanContext,
|
|
23
21
|
)
|
|
24
|
-
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Mock functions for Langfuse v3 API
|
|
25
|
+
def mock_get_client():
|
|
26
|
+
mock_client = Mock()
|
|
27
|
+
mock_client.start_as_current_span = Mock(return_value=MockContextManager())
|
|
28
|
+
mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
|
|
29
|
+
mock_client.get_current_trace_id = Mock(return_value="mock_trace_id_123")
|
|
30
|
+
return mock_client
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MockContextManager:
|
|
34
|
+
"""Mock context manager that simulates Langfuse v3 context managers"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, name="mock_span"):
|
|
37
|
+
self._span = MockSpan(name)
|
|
38
|
+
|
|
39
|
+
def __enter__(self):
|
|
40
|
+
return self._span
|
|
41
|
+
|
|
42
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
43
|
+
pass
|
|
25
44
|
|
|
26
45
|
|
|
27
46
|
class MockSpan:
|
|
28
47
|
def __init__(self, name="mock_span"):
|
|
29
48
|
self._data = {}
|
|
30
|
-
self._span = self
|
|
31
49
|
self.operation_name = name
|
|
32
50
|
self._name = name
|
|
51
|
+
# Make update a Mock so we can assert on it, but also make it actually work
|
|
52
|
+
self.update = Mock(side_effect=self._update_data)
|
|
53
|
+
|
|
54
|
+
def _update_data(self, **kwargs):
|
|
55
|
+
"""Helper method to actually update _data when update is called"""
|
|
56
|
+
self._data.update(kwargs)
|
|
33
57
|
|
|
34
58
|
def raw_span(self):
|
|
35
59
|
return self
|
|
@@ -38,7 +62,8 @@ class MockSpan:
|
|
|
38
62
|
# Return a new mock span for child spans
|
|
39
63
|
return MockSpan(name=name or "child_span")
|
|
40
64
|
|
|
41
|
-
def
|
|
65
|
+
def update_trace(self, **kwargs):
|
|
66
|
+
# v3 API method for updating trace-level data
|
|
42
67
|
self._data.update(kwargs)
|
|
43
68
|
|
|
44
69
|
def generation(self, name=None):
|
|
@@ -50,7 +75,7 @@ class MockSpan:
|
|
|
50
75
|
|
|
51
76
|
|
|
52
77
|
class MockTracer:
|
|
53
|
-
def trace(self, name, **kwargs):
|
|
78
|
+
def trace(self, name, **kwargs): # noqa: ARG002
|
|
54
79
|
# Return a unique mock span for each trace call
|
|
55
80
|
return MockSpan(name=name)
|
|
56
81
|
|
|
@@ -58,6 +83,28 @@ class MockTracer:
|
|
|
58
83
|
pass
|
|
59
84
|
|
|
60
85
|
|
|
86
|
+
class MockLangfuseClient:
|
|
87
|
+
"""Mock Langfuse client that has all the required methods"""
|
|
88
|
+
|
|
89
|
+
def __init__(self):
|
|
90
|
+
self._mock_context_manager = MockContextManager()
|
|
91
|
+
|
|
92
|
+
def start_as_current_span(self, _name=None, **_kwargs):
|
|
93
|
+
return self._mock_context_manager
|
|
94
|
+
|
|
95
|
+
def start_as_current_observation(self, _name=None, _as_type=None, **_kwargs):
|
|
96
|
+
return self._mock_context_manager
|
|
97
|
+
|
|
98
|
+
def get_current_trace_id(self):
|
|
99
|
+
return "mock_trace_id_123"
|
|
100
|
+
|
|
101
|
+
def get_current_observation_id(self):
|
|
102
|
+
return "mock_observation_id_123"
|
|
103
|
+
|
|
104
|
+
def flush(self):
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
61
108
|
class CustomSpanHandler(DefaultSpanHandler):
|
|
62
109
|
def handle(self, span: LangfuseSpan, component_type: Optional[str]) -> None:
|
|
63
110
|
if component_type == "OpenAIChatGenerator":
|
|
@@ -70,58 +117,60 @@ class CustomSpanHandler(DefaultSpanHandler):
|
|
|
70
117
|
class TestLangfuseSpan:
|
|
71
118
|
# LangfuseSpan can be initialized with a span object
|
|
72
119
|
def test_initialized_with_span_object(self):
|
|
73
|
-
|
|
74
|
-
span = LangfuseSpan(
|
|
75
|
-
assert span.raw_span() ==
|
|
120
|
+
mock_context_manager = MockContextManager()
|
|
121
|
+
span = LangfuseSpan(mock_context_manager)
|
|
122
|
+
assert span.raw_span() == mock_context_manager._span
|
|
76
123
|
|
|
77
124
|
# set_tag method can update metadata of the span object
|
|
78
125
|
def test_set_tag_updates_metadata(self):
|
|
79
|
-
|
|
80
|
-
span = LangfuseSpan(
|
|
126
|
+
mock_context_manager = MockContextManager()
|
|
127
|
+
span = LangfuseSpan(mock_context_manager)
|
|
81
128
|
|
|
82
129
|
span.set_tag("key", "value")
|
|
83
|
-
|
|
130
|
+
mock_context_manager._span.update.assert_called_once_with(metadata={"key": "value"})
|
|
84
131
|
assert span._data["key"] == "value"
|
|
85
132
|
|
|
86
133
|
# set_content_tag method can update input and output of the span object
|
|
87
134
|
def test_set_content_tag_updates_input_and_output(self):
|
|
88
|
-
|
|
135
|
+
mock_context_manager = MockContextManager()
|
|
89
136
|
|
|
90
|
-
span = LangfuseSpan(
|
|
91
|
-
span.set_content_tag("
|
|
92
|
-
|
|
137
|
+
span = LangfuseSpan(mock_context_manager)
|
|
138
|
+
span.set_content_tag("test.input", "input_value")
|
|
139
|
+
# Check that the span.update method was called with input parameter
|
|
140
|
+
mock_context_manager._span.update.assert_called_with(input="input_value")
|
|
93
141
|
|
|
94
|
-
|
|
95
|
-
span.set_content_tag("
|
|
96
|
-
|
|
142
|
+
mock_context_manager._span.update.reset_mock()
|
|
143
|
+
span.set_content_tag("test.output", "output_value")
|
|
144
|
+
# Check that the span.update method was called with output parameter
|
|
145
|
+
mock_context_manager._span.update.assert_called_with(output="output_value")
|
|
97
146
|
|
|
98
147
|
# set_content_tag method can update input and output of the span object with messages/replies
|
|
99
148
|
def test_set_content_tag_updates_input_and_output_with_messages(self):
|
|
100
|
-
|
|
149
|
+
mock_context_manager = MockContextManager()
|
|
101
150
|
|
|
102
151
|
# test message input
|
|
103
|
-
span = LangfuseSpan(
|
|
152
|
+
span = LangfuseSpan(mock_context_manager)
|
|
104
153
|
span.set_content_tag("key.input", {"messages": [ChatMessage.from_user("message")]})
|
|
105
|
-
assert
|
|
154
|
+
assert mock_context_manager._span.update.call_count == 1
|
|
106
155
|
# check we converted ChatMessage to OpenAI format
|
|
107
|
-
assert
|
|
108
|
-
|
|
109
|
-
|
|
156
|
+
assert mock_context_manager._span.update.call_args_list[0][1] == {
|
|
157
|
+
"input": [{"role": "user", "content": "message"}]
|
|
158
|
+
}
|
|
110
159
|
# test replies ChatMessage list
|
|
111
|
-
|
|
160
|
+
mock_context_manager._span.update.reset_mock()
|
|
112
161
|
span.set_content_tag("key.output", {"replies": [ChatMessage.from_system("reply")]})
|
|
113
|
-
assert
|
|
162
|
+
assert mock_context_manager._span.update.call_count == 1
|
|
114
163
|
# check we converted ChatMessage to OpenAI format
|
|
115
|
-
assert
|
|
116
|
-
|
|
164
|
+
assert mock_context_manager._span.update.call_args_list[0][1] == {
|
|
165
|
+
"output": [{"role": "system", "content": "reply"}]
|
|
166
|
+
}
|
|
117
167
|
|
|
118
168
|
# test replies string list
|
|
119
|
-
|
|
169
|
+
mock_context_manager._span.update.reset_mock()
|
|
120
170
|
span.set_content_tag("key.output", {"replies": ["reply1", "reply2"]})
|
|
121
|
-
assert
|
|
171
|
+
assert mock_context_manager._span.update.call_count == 1
|
|
122
172
|
# check we handle properly string list replies
|
|
123
|
-
assert
|
|
124
|
-
assert span._data["key.output"] == {"replies": ["reply1", "reply2"]}
|
|
173
|
+
assert mock_context_manager._span.update.call_args_list[0][1] == {"output": ["reply1", "reply2"]}
|
|
125
174
|
|
|
126
175
|
|
|
127
176
|
class TestSpanContext:
|
|
@@ -178,7 +227,9 @@ class TestDefaultSpanHandler:
|
|
|
178
227
|
assert mock_span.update.call_args_list[0][1] == {
|
|
179
228
|
"usage": None,
|
|
180
229
|
"model": "test_model",
|
|
181
|
-
"completion_start_time": datetime.datetime(
|
|
230
|
+
"completion_start_time": datetime.datetime( # noqa: DTZ001
|
|
231
|
+
2021, 7, 27, 16, 2, 8, 12345
|
|
232
|
+
),
|
|
182
233
|
}
|
|
183
234
|
|
|
184
235
|
def test_handle_bad_completion_start_time(self, caplog):
|
|
@@ -250,15 +301,14 @@ class TestLangfuseTracer:
|
|
|
250
301
|
mock_raw_span.operation_name = "operation_name"
|
|
251
302
|
mock_raw_span.metadata = {"tag1": "value1", "tag2": "value2"}
|
|
252
303
|
|
|
253
|
-
with patch("haystack_integrations.tracing.langfuse.tracer.LangfuseSpan") as
|
|
254
|
-
mock_span_instance =
|
|
304
|
+
with patch("haystack_integrations.tracing.langfuse.tracer.LangfuseSpan") as mock_langfuse_span:
|
|
305
|
+
mock_span_instance = mock_langfuse_span.return_value
|
|
255
306
|
mock_span_instance.raw_span.return_value = mock_raw_span
|
|
256
307
|
|
|
257
|
-
mock_context_manager =
|
|
258
|
-
mock_context_manager.
|
|
259
|
-
|
|
308
|
+
mock_context_manager = MockContextManager()
|
|
309
|
+
mock_context_manager._span = mock_raw_span
|
|
260
310
|
mock_tracer = MagicMock()
|
|
261
|
-
mock_tracer.
|
|
311
|
+
mock_tracer.start_as_current_span.return_value = mock_context_manager
|
|
262
312
|
|
|
263
313
|
tracer = LangfuseTracer(tracer=mock_tracer, name="Haystack", public=False)
|
|
264
314
|
|
|
@@ -275,30 +325,32 @@ class TestLangfuseTracer:
|
|
|
275
325
|
|
|
276
326
|
# check that update method is called on the span instance with the provided key value pairs
|
|
277
327
|
def test_update_span_with_pipeline_input_output_data(self):
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
328
|
+
with patch("haystack_integrations.tracing.langfuse.tracer.langfuse.get_client"):
|
|
329
|
+
tracer = LangfuseTracer(tracer=MockLangfuseClient(), name="Haystack", public=False)
|
|
330
|
+
with tracer.trace(operation_name="operation_name", tags={"haystack.pipeline.input_data": "hello"}) as span:
|
|
331
|
+
assert span.raw_span()._data["metadata"] == {"haystack.pipeline.input_data": "hello"}
|
|
281
332
|
|
|
282
|
-
|
|
283
|
-
|
|
333
|
+
with tracer.trace(operation_name="operation_name", tags={"haystack.pipeline.output_data": "bye"}) as span:
|
|
334
|
+
assert span.raw_span()._data["metadata"] == {"haystack.pipeline.output_data": "bye"}
|
|
284
335
|
|
|
285
336
|
def test_trace_generation(self):
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
"
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
337
|
+
with patch("haystack_integrations.tracing.langfuse.tracer.langfuse.get_client"):
|
|
338
|
+
tracer = LangfuseTracer(tracer=MockLangfuseClient(), name="Haystack", public=False)
|
|
339
|
+
tags = {
|
|
340
|
+
"haystack.component.type": "OpenAIChatGenerator",
|
|
341
|
+
"haystack.component.output": {
|
|
342
|
+
"replies": [
|
|
343
|
+
ChatMessage.from_assistant(
|
|
344
|
+
"", meta={"completion_start_time": "2021-07-27T16:02:08.012345", "model": "test_model"}
|
|
345
|
+
)
|
|
346
|
+
]
|
|
347
|
+
},
|
|
348
|
+
}
|
|
349
|
+
with tracer.trace(operation_name="operation_name", tags=tags) as span:
|
|
350
|
+
...
|
|
351
|
+
assert span.raw_span()._data["usage"] is None
|
|
352
|
+
assert span.raw_span()._data["model"] == "test_model"
|
|
353
|
+
assert span.raw_span()._data["completion_start_time"] == datetime.datetime(2021, 7, 27, 16, 2, 8, 12345) # noqa: DTZ001
|
|
302
354
|
|
|
303
355
|
def test_handle_tool_invoker(self):
|
|
304
356
|
"""
|
|
@@ -342,7 +394,7 @@ class TestLangfuseTracer:
|
|
|
342
394
|
updated_name = name_update_call[1]["name"]
|
|
343
395
|
|
|
344
396
|
# verify the format of the updated span name to be: `original_component_name - [list_of_tool_names]`
|
|
345
|
-
assert updated_name != "tool_invoker",
|
|
397
|
+
assert updated_name != "tool_invoker", "Expected 'tool_invoker` to be upddated with tool names"
|
|
346
398
|
assert " - " in updated_name, f"Expected ' - ' in {updated_name}"
|
|
347
399
|
assert "[" in updated_name, f"Expected '[' in {updated_name}"
|
|
348
400
|
assert "]" in updated_name, f"Expected ']' in {updated_name}"
|
|
@@ -351,47 +403,51 @@ class TestLangfuseTracer:
|
|
|
351
403
|
assert "weather_tool" in updated_name, f"Expected 'weather_tool' in {updated_name}"
|
|
352
404
|
|
|
353
405
|
def test_trace_generation_invalid_start_time(self):
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
"
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
406
|
+
with patch("haystack_integrations.tracing.langfuse.tracer.langfuse.get_client"):
|
|
407
|
+
tracer = LangfuseTracer(tracer=MockLangfuseClient(), name="Haystack", public=False)
|
|
408
|
+
tags = {
|
|
409
|
+
"haystack.component.type": "OpenAIChatGenerator",
|
|
410
|
+
"haystack.component.output": {
|
|
411
|
+
"replies": [
|
|
412
|
+
ChatMessage.from_assistant("", meta={"completion_start_time": "foobar", "model": "test_model"}),
|
|
413
|
+
]
|
|
414
|
+
},
|
|
415
|
+
}
|
|
416
|
+
with tracer.trace(operation_name="operation_name", tags=tags) as span:
|
|
417
|
+
...
|
|
418
|
+
assert span.raw_span()._data["usage"] is None
|
|
419
|
+
assert span.raw_span()._data["model"] == "test_model"
|
|
420
|
+
assert span.raw_span()._data["completion_start_time"] is None
|
|
368
421
|
|
|
369
422
|
def test_update_span_gets_flushed_by_default(self):
|
|
370
|
-
tracer_mock =
|
|
423
|
+
tracer_mock = MockLangfuseClient()
|
|
424
|
+
tracer_mock.flush = Mock() # Make flush a mock for assertions
|
|
371
425
|
|
|
372
426
|
tracer = LangfuseTracer(tracer=tracer_mock, name="Haystack", public=False)
|
|
373
|
-
with tracer.trace(operation_name="operation_name", tags={"haystack.pipeline.input_data": "hello"})
|
|
427
|
+
with tracer.trace(operation_name="operation_name", tags={"haystack.pipeline.input_data": "hello"}):
|
|
374
428
|
pass
|
|
375
429
|
|
|
376
430
|
tracer_mock.flush.assert_called_once()
|
|
377
431
|
|
|
378
432
|
def test_update_span_flush_disable(self, monkeypatch):
|
|
379
433
|
monkeypatch.setenv("HAYSTACK_LANGFUSE_ENFORCE_FLUSH", "false")
|
|
380
|
-
tracer_mock =
|
|
434
|
+
tracer_mock = MockLangfuseClient()
|
|
435
|
+
tracer_mock.flush = Mock() # Make flush a mock for assertions
|
|
381
436
|
|
|
382
|
-
|
|
437
|
+
# Re-import LangfuseTracer to ensure it picks up the new environment variable
|
|
438
|
+
from haystack_integrations.tracing.langfuse.tracer import LangfuseTracer # noqa: PLC0415
|
|
383
439
|
|
|
384
440
|
tracer = LangfuseTracer(tracer=tracer_mock, name="Haystack", public=False)
|
|
385
|
-
with tracer.trace(operation_name="operation_name", tags={"haystack.pipeline.input_data": "hello"})
|
|
441
|
+
with tracer.trace(operation_name="operation_name", tags={"haystack.pipeline.input_data": "hello"}):
|
|
386
442
|
pass
|
|
387
443
|
|
|
388
444
|
tracer_mock.flush.assert_not_called()
|
|
389
445
|
|
|
390
446
|
def test_context_is_empty_after_tracing(self):
|
|
391
|
-
tracer_mock =
|
|
447
|
+
tracer_mock = MockLangfuseClient()
|
|
392
448
|
|
|
393
449
|
tracer = LangfuseTracer(tracer=tracer_mock, name="Haystack", public=False)
|
|
394
|
-
with tracer.trace(operation_name="operation_name", tags={"haystack.pipeline.input_data": "hello"})
|
|
450
|
+
with tracer.trace(operation_name="operation_name", tags={"haystack.pipeline.input_data": "hello"}):
|
|
395
451
|
pass
|
|
396
452
|
|
|
397
453
|
# Check behavioral state instead of internal _context list
|
|
@@ -406,11 +462,11 @@ class TestLangfuseTracer:
|
|
|
406
462
|
# Re-import LangfuseTracer and instantiate it with tracing disabled
|
|
407
463
|
with caplog.at_level(logging.WARNING):
|
|
408
464
|
monkeypatch.setenv("HAYSTACK_CONTENT_TRACING_ENABLED", "false")
|
|
409
|
-
from haystack_integrations.tracing.langfuse import LangfuseTracer
|
|
465
|
+
from haystack_integrations.tracing.langfuse import LangfuseTracer # noqa: PLC0415
|
|
410
466
|
|
|
411
|
-
LangfuseTracer(tracer=
|
|
467
|
+
LangfuseTracer(tracer=MockLangfuseClient(), name="Haystack", public=False)
|
|
412
468
|
assert "tracing is disabled" in caplog.text
|
|
413
|
-
|
|
469
|
+
|
|
414
470
|
def test_async_concurrency_span_isolation(self):
|
|
415
471
|
"""
|
|
416
472
|
Test that concurrent async traces maintain isolated span contexts.
|
|
@@ -418,7 +474,7 @@ class TestLangfuseTracer:
|
|
|
418
474
|
This test verifies that the context-local span stack prevents cross-request
|
|
419
475
|
span interleaving in concurrent environments like FastAPI servers.
|
|
420
476
|
"""
|
|
421
|
-
tracer = LangfuseTracer(tracer=
|
|
477
|
+
tracer = LangfuseTracer(tracer=MockLangfuseClient(), name="Haystack", public=False)
|
|
422
478
|
|
|
423
479
|
# Track spans from each task for verification
|
|
424
480
|
task1_spans = []
|
|
@@ -482,4 +538,3 @@ class TestLangfuseTracer:
|
|
|
482
538
|
assert task2_spans[1][2] == task2_inner # current_span during inner
|
|
483
539
|
assert task2_spans[2][2] == task2_outer # current_span after inner
|
|
484
540
|
assert task2_spans[3][2] is None # current_span after outer
|
|
485
|
-
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
import time
|
|
7
8
|
from typing import Any, Dict, List
|
|
8
9
|
from urllib.parse import urlparse
|
|
9
|
-
import json
|
|
10
10
|
|
|
11
11
|
import pytest
|
|
12
12
|
import requests
|
|
@@ -17,12 +17,23 @@ from haystack.dataclasses import ChatMessage
|
|
|
17
17
|
from requests.auth import HTTPBasicAuth
|
|
18
18
|
|
|
19
19
|
from haystack_integrations.components.connectors.langfuse import LangfuseConnector
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
|
|
21
|
+
# We use the try/except block to prevent import errors when running unit tests with lowest direct dependencies,
|
|
22
|
+
# where the subdependencies of LLM integrations might not be respected.
|
|
23
|
+
try:
|
|
24
|
+
from haystack_integrations.components.generators.anthropic import AnthropicChatGenerator
|
|
25
|
+
except ImportError:
|
|
26
|
+
AnthropicChatGenerator = None
|
|
27
|
+
try:
|
|
28
|
+
from haystack_integrations.components.generators.cohere import CohereChatGenerator
|
|
29
|
+
except ImportError:
|
|
30
|
+
CohereChatGenerator = None
|
|
22
31
|
|
|
23
32
|
# don't remove (or move) this env var setting from here, it's needed to turn tracing on
|
|
24
33
|
os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
|
|
25
34
|
|
|
35
|
+
os.environ.setdefault("LANGFUSE_HOST", "https://cloud.langfuse.com")
|
|
36
|
+
|
|
26
37
|
|
|
27
38
|
def poll_langfuse(url: str):
|
|
28
39
|
"""Utility function to poll Langfuse API until the trace is ready"""
|
|
@@ -36,7 +47,7 @@ def poll_langfuse(url: str):
|
|
|
36
47
|
|
|
37
48
|
res = None
|
|
38
49
|
while attempts > 0:
|
|
39
|
-
res = requests.get(url, auth=auth)
|
|
50
|
+
res = requests.get(url, auth=auth, timeout=60.0)
|
|
40
51
|
if res.status_code == 200:
|
|
41
52
|
return res
|
|
42
53
|
|
|
@@ -71,7 +82,7 @@ def basic_pipeline(llm_class, expected_trace):
|
|
|
71
82
|
(CohereChatGenerator, "COHERE_API_KEY", "Cohere"),
|
|
72
83
|
],
|
|
73
84
|
)
|
|
74
|
-
def test_tracing_integration(
|
|
85
|
+
def test_tracing_integration(env_var, expected_trace, basic_pipeline):
|
|
75
86
|
if not all([os.environ.get("LANGFUSE_SECRET_KEY"), os.environ.get("LANGFUSE_PUBLIC_KEY"), os.environ.get(env_var)]):
|
|
76
87
|
pytest.skip(f"Missing required environment variable: {env_var}")
|
|
77
88
|
|
|
@@ -92,7 +103,7 @@ def test_tracing_integration(llm_class, env_var, expected_trace, basic_pipeline)
|
|
|
92
103
|
|
|
93
104
|
trace_url = response["tracer"]["trace_url"]
|
|
94
105
|
uuid = os.path.basename(urlparse(trace_url).path)
|
|
95
|
-
url = f"
|
|
106
|
+
url = f"{os.environ['LANGFUSE_HOST']}/api/public/traces/{uuid}"
|
|
96
107
|
|
|
97
108
|
res = poll_langfuse(url)
|
|
98
109
|
assert res.status_code == 200, f"Failed to retrieve data from Langfuse API: {res.status_code}"
|
|
@@ -104,7 +115,8 @@ def test_tracing_integration(llm_class, env_var, expected_trace, basic_pipeline)
|
|
|
104
115
|
assert isinstance(res_json["output"], dict)
|
|
105
116
|
assert isinstance(res_json["metadata"], dict)
|
|
106
117
|
assert isinstance(res_json["observations"], list)
|
|
107
|
-
|
|
118
|
+
# at least one observation should be a generation
|
|
119
|
+
assert any(obs["type"] == "GENERATION" for obs in res_json["observations"])
|
|
108
120
|
|
|
109
121
|
|
|
110
122
|
@pytest.mark.skipif(
|
|
@@ -119,7 +131,6 @@ def test_tracing_integration(llm_class, env_var, expected_trace, basic_pipeline)
|
|
|
119
131
|
)
|
|
120
132
|
@pytest.mark.integration
|
|
121
133
|
def test_tracing_with_sub_pipelines():
|
|
122
|
-
|
|
123
134
|
@component
|
|
124
135
|
class SubGenerator:
|
|
125
136
|
def __init__(self):
|
|
@@ -165,7 +176,7 @@ def test_tracing_with_sub_pipelines():
|
|
|
165
176
|
|
|
166
177
|
trace_url = response["tracer"]["trace_url"]
|
|
167
178
|
uuid = os.path.basename(urlparse(trace_url).path)
|
|
168
|
-
url = f"
|
|
179
|
+
url = f"{os.environ['LANGFUSE_HOST']}/api/public/traces/{uuid}"
|
|
169
180
|
|
|
170
181
|
res = poll_langfuse(url)
|
|
171
182
|
assert res.status_code == 200, f"Failed to retrieve data from Langfuse API: {res.status_code}"
|
|
@@ -173,23 +184,20 @@ def test_tracing_with_sub_pipelines():
|
|
|
173
184
|
res_json = res.json()
|
|
174
185
|
assert res_json["name"] == "Sub-pipeline example"
|
|
175
186
|
assert isinstance(res_json["input"], dict)
|
|
176
|
-
assert "sub_pipeline" in res_json["input"]
|
|
177
|
-
assert "messages" in res_json["input"]["sub_pipeline"]
|
|
178
|
-
assert res_json["input"]["tracer"]["invocation_context"]["user_id"] == "user_42"
|
|
179
187
|
assert isinstance(res_json["output"], dict)
|
|
180
188
|
assert isinstance(res_json["metadata"], dict)
|
|
181
189
|
assert isinstance(res_json["observations"], list)
|
|
182
190
|
|
|
183
191
|
observations = res_json["observations"]
|
|
192
|
+
assert len(observations) == 8
|
|
184
193
|
|
|
185
194
|
haystack_pipeline_run_observations = [obs for obs in observations if obs["name"] == "haystack.pipeline.run"]
|
|
186
195
|
# There should be two observations for the haystack.pipeline.run span: one for each sub pipeline
|
|
187
196
|
# Main pipeline is stored under the name "Sub-pipeline example"
|
|
188
197
|
assert len(haystack_pipeline_run_observations) == 2
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
assert "llm" in component_names
|
|
198
|
+
assert "prompt_builder" in str(haystack_pipeline_run_observations[0])
|
|
199
|
+
assert "llm" in str(haystack_pipeline_run_observations[1])
|
|
200
|
+
|
|
193
201
|
|
|
194
202
|
@pytest.mark.skipif(
|
|
195
203
|
not all(
|
|
@@ -243,7 +251,7 @@ def test_context_cleanup_after_nested_failures():
|
|
|
243
251
|
# Test 1: First run will fail and should clean up context
|
|
244
252
|
try:
|
|
245
253
|
main_pipeline.run({"nested_component": {"input_data": "invalid json"}})
|
|
246
|
-
except Exception:
|
|
254
|
+
except Exception: # noqa: S110
|
|
247
255
|
pass # Expected to fail
|
|
248
256
|
|
|
249
257
|
# Critical assertion: context should be empty after failed operation
|
|
@@ -251,6 +259,6 @@ def test_context_cleanup_after_nested_failures():
|
|
|
251
259
|
|
|
252
260
|
# Test 2: Second run should work normally with clean context
|
|
253
261
|
main_pipeline.run({"nested_component": {"input_data": '{"key": "valid"}'}})
|
|
254
|
-
|
|
262
|
+
|
|
255
263
|
# Critical assertion: context should be empty after successful operation
|
|
256
|
-
assert len(tracer.tracer._context) == 0
|
|
264
|
+
assert len(tracer.tracer._context) == 0
|
|
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
|