langfuse-haystack 4.0.1__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.0.1 → langfuse_haystack-5.0.0}/CHANGELOG.md +28 -0
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/PKG-INFO +2 -2
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/example/basic_rag.py +2 -2
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/example/chat.py +2 -2
- langfuse_haystack-5.0.0/pydoc/config_docusaurus.yml +14 -0
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/pyproject.toml +20 -7
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +1 -0
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/src/haystack_integrations/tracing/langfuse/tracer.py +101 -87
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/tests/test_tracer.py +114 -66
- langfuse_haystack-4.0.1/pydoc/config_docusaurus.yml +0 -29
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/.gitignore +0 -0
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/LICENSE.txt +0 -0
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/README.md +0 -0
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/example/requirements.txt +0 -0
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/src/haystack_integrations/components/connectors/__init__.py +0 -0
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/src/haystack_integrations/components/connectors/py.typed +0 -0
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/src/haystack_integrations/tracing/py.typed +0 -0
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/tests/__init__.py +0 -0
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/tests/test_langfuse_connector.py +0 -0
- {langfuse_haystack-4.0.1 → langfuse_haystack-5.0.0}/tests/test_tracing.py +0 -0
|
@@ -1,5 +1,33 @@
|
|
|
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
|
+
|
|
24
|
+
## [integrations/langfuse-v4.0.1] - 2026-01-27
|
|
25
|
+
|
|
26
|
+
### 🐛 Bug Fixes
|
|
27
|
+
|
|
28
|
+
- *(langfuse)* Apply trace_id from tracing context to Langfuse traces (#2781)
|
|
29
|
+
|
|
30
|
+
|
|
3
31
|
## [integrations/langfuse-v4.0.0] - 2026-01-12
|
|
4
32
|
|
|
5
33
|
### 🧹 Chores
|
|
@@ -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
|
|
@@ -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])
|
|
66
|
+
print(response["tracer"]["trace_url"])
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
loaders:
|
|
2
|
+
- modules:
|
|
3
|
+
- haystack_integrations.components.connectors.langfuse.langfuse_connector
|
|
4
|
+
- haystack_integrations.tracing.langfuse.tracer
|
|
5
|
+
search_path: [../src]
|
|
6
|
+
processors:
|
|
7
|
+
- type: filter
|
|
8
|
+
documented_only: true
|
|
9
|
+
skip_empty_modules: true
|
|
10
|
+
renderer:
|
|
11
|
+
description: Langfuse integration for Haystack
|
|
12
|
+
id: integrations-langfuse
|
|
13
|
+
filename: langfuse.md
|
|
14
|
+
title: langfuse
|
|
@@ -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"
|
|
@@ -45,7 +45,7 @@ installer = "uv"
|
|
|
45
45
|
dependencies = ["haystack-pydoc-tools", "ruff"]
|
|
46
46
|
|
|
47
47
|
[tool.hatch.envs.default.scripts]
|
|
48
|
-
docs = ["pydoc
|
|
48
|
+
docs = ["haystack-pydoc pydoc/config_docusaurus.yml"]
|
|
49
49
|
fmt = "ruff check --fix {args}; ruff format {args}"
|
|
50
50
|
fmt-check = "ruff check {args} && ruff format --check {args}"
|
|
51
51
|
|
|
@@ -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
|
|
|
@@ -76,8 +77,6 @@ non_interactive = true
|
|
|
76
77
|
check_untyped_defs = true
|
|
77
78
|
disallow_incomplete_defs = true
|
|
78
79
|
|
|
79
|
-
[tool.hatch.metadata]
|
|
80
|
-
allow-direct-references = true
|
|
81
80
|
|
|
82
81
|
|
|
83
82
|
[tool.ruff]
|
|
@@ -86,9 +85,17 @@ line-length = 120
|
|
|
86
85
|
[tool.ruff.lint]
|
|
87
86
|
select = [
|
|
88
87
|
"A",
|
|
88
|
+
"ANN",
|
|
89
89
|
"ARG",
|
|
90
90
|
"B",
|
|
91
91
|
"C",
|
|
92
|
+
"D102", # Missing docstring in public method
|
|
93
|
+
"D103", # Missing docstring in public function
|
|
94
|
+
"D205", # 1 blank line required between summary line and description
|
|
95
|
+
"D209", # Closing triple quotes go to new line
|
|
96
|
+
"D213", # summary lines must be positioned on the second physical line of the docstring
|
|
97
|
+
"D417", # Missing argument descriptions in the docstring
|
|
98
|
+
"D419", # Docstring is empty
|
|
92
99
|
"DTZ",
|
|
93
100
|
"E",
|
|
94
101
|
"EM",
|
|
@@ -125,6 +132,8 @@ ignore = [
|
|
|
125
132
|
"PLR0915",
|
|
126
133
|
# Asserts
|
|
127
134
|
"S101",
|
|
135
|
+
# Allow `Any` - used legitimately for dynamic types and SDK boundaries
|
|
136
|
+
"ANN401",
|
|
128
137
|
]
|
|
129
138
|
|
|
130
139
|
[tool.ruff.lint.isort]
|
|
@@ -135,14 +144,16 @@ ban-relative-imports = "parents"
|
|
|
135
144
|
|
|
136
145
|
[tool.ruff.lint.per-file-ignores]
|
|
137
146
|
# Tests can use magic values, assertions, and relative imports
|
|
138
|
-
"tests/**/*" = ["PLR2004", "S101", "TID252"]
|
|
147
|
+
"tests/**/*" = ["D", "PLR2004", "S101", "TID252", "ANN"]
|
|
139
148
|
# Examples can print their output
|
|
140
|
-
"examples/**" = ["T201"]
|
|
149
|
+
"examples/**" = ["T201", "ANN", "D"]
|
|
150
|
+
"example/**" = ["T201", "ANN", "D"]
|
|
141
151
|
"tests/**" = ["T201"]
|
|
142
152
|
|
|
143
153
|
[tool.coverage.run]
|
|
144
154
|
source = ["haystack_integrations"]
|
|
145
155
|
branch = true
|
|
156
|
+
relative_files = true
|
|
146
157
|
parallel = false
|
|
147
158
|
|
|
148
159
|
|
|
@@ -150,6 +161,8 @@ parallel = false
|
|
|
150
161
|
omit = ["*/tests/*", "*/__init__.py"]
|
|
151
162
|
show_missing = true
|
|
152
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
|
|
153
166
|
|
|
154
167
|
[tool.pytest.ini_options]
|
|
155
168
|
addopts = "--strict-markers"
|
|
@@ -19,6 +19,7 @@ logger = logging.getLogger(__name__)
|
|
|
19
19
|
class LangfuseConnector:
|
|
20
20
|
"""
|
|
21
21
|
LangfuseConnector connects Haystack LLM framework with [Langfuse](https://langfuse.com) in order to enable the
|
|
22
|
+
|
|
22
23
|
tracing of operations and data flow within various components of a pipeline.
|
|
23
24
|
|
|
24
25
|
To use LangfuseConnector, add it to your pipeline without connecting it to any other components.
|
|
@@ -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__()
|
|
@@ -91,7 +91,10 @@ class LangfuseSpan(Span):
|
|
|
91
91
|
if key.endswith(".input"):
|
|
92
92
|
if "messages" in value:
|
|
93
93
|
messages = [m.to_openai_dict_format(require_tool_call_ids=False) for m in value["messages"]]
|
|
94
|
-
|
|
94
|
+
if isinstance(gen_kwargs := value.get("generation_kwargs"), dict):
|
|
95
|
+
self._span.update(input={"messages": messages, "generation_kwargs": gen_kwargs})
|
|
96
|
+
else:
|
|
97
|
+
self._span.update(input=messages)
|
|
95
98
|
else:
|
|
96
99
|
coerced_value = tracing_utils.coerce_tag_value(value)
|
|
97
100
|
self._span.update(input=coerced_value)
|
|
@@ -125,6 +128,7 @@ class LangfuseSpan(Span):
|
|
|
125
128
|
return self._data
|
|
126
129
|
|
|
127
130
|
def get_correlation_data_for_logs(self) -> dict[str, Any]:
|
|
131
|
+
"""Return correlation data for log enrichment."""
|
|
128
132
|
return {}
|
|
129
133
|
|
|
130
134
|
|
|
@@ -234,9 +238,11 @@ class SpanHandler(ABC):
|
|
|
234
238
|
|
|
235
239
|
@classmethod
|
|
236
240
|
def from_dict(cls, data: dict[str, Any]) -> "SpanHandler":
|
|
241
|
+
"""Deserialize a SpanHandler from a dictionary."""
|
|
237
242
|
return default_from_dict(cls, data)
|
|
238
243
|
|
|
239
244
|
def to_dict(self) -> dict[str, Any]:
|
|
245
|
+
"""Serialize this SpanHandler to a dictionary."""
|
|
240
246
|
return default_to_dict(self)
|
|
241
247
|
|
|
242
248
|
|
|
@@ -273,6 +279,7 @@ class DefaultSpanHandler(SpanHandler):
|
|
|
273
279
|
"""DefaultSpanHandler provides the default Langfuse tracing behavior for Haystack."""
|
|
274
280
|
|
|
275
281
|
def create_span(self, context: SpanContext) -> LangfuseSpan:
|
|
282
|
+
"""Create a Langfuse span based on the given context."""
|
|
276
283
|
if self.tracer is None:
|
|
277
284
|
message = (
|
|
278
285
|
"Tracer is not initialized. "
|
|
@@ -290,33 +297,17 @@ class DefaultSpanHandler(SpanHandler):
|
|
|
290
297
|
trace_context: TraceContext | None = cast(
|
|
291
298
|
TraceContext, {"trace_id": tracing_ctx.get("trace_id")} if tracing_ctx.get("trace_id") else None
|
|
292
299
|
)
|
|
293
|
-
|
|
300
|
+
|
|
294
301
|
span_context_manager = self.tracer.start_as_current_observation(
|
|
295
302
|
trace_context=trace_context,
|
|
296
303
|
name=context.trace_name,
|
|
297
304
|
version=tracing_ctx.get("version"),
|
|
298
305
|
as_type=root_span_type,
|
|
299
306
|
)
|
|
300
|
-
|
|
301
|
-
# Create LangfuseSpan which will handle entering the context manager
|
|
302
307
|
span = LangfuseSpan(span_context_manager)
|
|
303
308
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
"name": context.trace_name,
|
|
307
|
-
"user_id": tracing_ctx.get("user_id"),
|
|
308
|
-
"session_id": tracing_ctx.get("session_id"),
|
|
309
|
-
"version": tracing_ctx.get("version"),
|
|
310
|
-
"metadata": None,
|
|
311
|
-
"tags": tracing_ctx.get("tags"),
|
|
312
|
-
"public": context.public,
|
|
313
|
-
"release": None,
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
# Filter out None values and apply trace attributes
|
|
317
|
-
trace_attrs = {k: v for k, v in trace_metadata.items() if v is not None}
|
|
318
|
-
if trace_attrs:
|
|
319
|
-
span._span.update_trace(**trace_attrs)
|
|
309
|
+
if context.public:
|
|
310
|
+
span.raw_span().set_trace_as_public()
|
|
320
311
|
|
|
321
312
|
return span
|
|
322
313
|
|
|
@@ -340,15 +331,16 @@ class DefaultSpanHandler(SpanHandler):
|
|
|
340
331
|
)
|
|
341
332
|
)
|
|
342
333
|
else:
|
|
343
|
-
return LangfuseSpan(self.tracer.
|
|
334
|
+
return LangfuseSpan(self.tracer.start_as_current_observation(name=context.name))
|
|
344
335
|
|
|
345
336
|
def handle(self, span: LangfuseSpan, component_type: str | None) -> None:
|
|
337
|
+
"""Process and enrich a span after component execution."""
|
|
346
338
|
# If the span is at the pipeline level, we add input and output keys to the span
|
|
347
339
|
at_pipeline_level = span.get_data().get(_PIPELINE_INPUT_KEY) is not None
|
|
348
340
|
if at_pipeline_level:
|
|
349
341
|
coerced_input = tracing_utils.coerce_tag_value(span.get_data().get(_PIPELINE_INPUT_KEY))
|
|
350
342
|
coerced_output = tracing_utils.coerce_tag_value(span.get_data().get(_PIPELINE_OUTPUT_KEY))
|
|
351
|
-
span.raw_span().
|
|
343
|
+
span.raw_span().update(input=coerced_input, output=coerced_output)
|
|
352
344
|
# special case for ToolInvoker (to update the span name to be: `original_component_name - [tool_names]`)
|
|
353
345
|
if component_type == "ToolInvoker":
|
|
354
346
|
tool_names: list[str] = []
|
|
@@ -456,9 +448,11 @@ class LangfuseTracer(Tracer):
|
|
|
456
448
|
def trace(
|
|
457
449
|
self, operation_name: str, tags: dict[str, Any] | None = None, parent_span: Span | None = None
|
|
458
450
|
) -> Iterator[Span]:
|
|
451
|
+
"""Create and manage a tracing span as a context manager."""
|
|
459
452
|
tags = tags or {}
|
|
460
453
|
span_name = tags.get(_COMPONENT_NAME_KEY, operation_name)
|
|
461
454
|
component_type = tags.get(_COMPONENT_TYPE_KEY)
|
|
455
|
+
is_root_span = not (parent_span or self.current_span())
|
|
462
456
|
|
|
463
457
|
# Create a new span context
|
|
464
458
|
span_context = SpanContext(
|
|
@@ -473,76 +467,94 @@ class LangfuseTracer(Tracer):
|
|
|
473
467
|
public=self._public,
|
|
474
468
|
)
|
|
475
469
|
|
|
476
|
-
|
|
477
|
-
span = self._span_handler.create_span(span_context)
|
|
470
|
+
tracing_ctx = tracing_context_var.get({})
|
|
478
471
|
|
|
479
|
-
#
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
+
)
|
|
484
486
|
|
|
485
|
-
|
|
487
|
+
# Create span using the handler
|
|
488
|
+
span = self._span_handler.create_span(span_context)
|
|
486
489
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
exc_info = sys.exc_info()
|
|
493
|
-
try:
|
|
494
|
-
# Process span data (may fail with nested pipeline exceptions)
|
|
495
|
-
self._span_handler.handle(span, component_type)
|
|
496
|
-
|
|
497
|
-
# End span with exception info (may fail if span data is corrupted)
|
|
498
|
-
raw_span = span.raw_span()
|
|
499
|
-
if span._context_manager is not None:
|
|
500
|
-
# Pass actual exception info to mark span as failed with ERROR level
|
|
501
|
-
span._context_manager.__exit__(*exc_info)
|
|
502
|
-
elif hasattr(raw_span, "end"):
|
|
503
|
-
# Only call end() if it's not a context manager
|
|
504
|
-
raw_span.end()
|
|
505
|
-
except Exception as cleanup_error:
|
|
506
|
-
# Log cleanup errors but don't let them corrupt context
|
|
507
|
-
logger.warning(
|
|
508
|
-
"Error during span cleanup for {operation_name}: {cleanup_error}",
|
|
509
|
-
operation_name=operation_name,
|
|
510
|
-
cleanup_error=cleanup_error,
|
|
511
|
-
)
|
|
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)
|
|
512
495
|
|
|
513
|
-
|
|
514
|
-
raise
|
|
515
|
-
else:
|
|
516
|
-
# No exception - clean exit with success status
|
|
517
|
-
# This preserves any manually-set log levels (WARNING, DEBUG)
|
|
518
|
-
try:
|
|
519
|
-
# Process span data
|
|
520
|
-
self._span_handler.handle(span, component_type)
|
|
521
|
-
|
|
522
|
-
# End span successfully
|
|
523
|
-
raw_span = span.raw_span()
|
|
524
|
-
# In v3, we need to properly exit context managers
|
|
525
|
-
if span._context_manager is not None:
|
|
526
|
-
# No exception - pass None to indicate success
|
|
527
|
-
span._context_manager.__exit__(None, None, None)
|
|
528
|
-
elif hasattr(raw_span, "end"):
|
|
529
|
-
# Only call end() if it's not a context manager
|
|
530
|
-
raw_span.end()
|
|
531
|
-
except Exception as cleanup_error:
|
|
532
|
-
# Log cleanup errors but don't let them corrupt context
|
|
533
|
-
logger.warning(
|
|
534
|
-
"Error during span cleanup for {operation_name}: {cleanup_error}",
|
|
535
|
-
operation_name=operation_name,
|
|
536
|
-
cleanup_error=cleanup_error,
|
|
537
|
-
)
|
|
538
|
-
finally:
|
|
539
|
-
# Restore previous span stack using saved token
|
|
540
|
-
span_stack_var.reset(token)
|
|
496
|
+
span.set_tags(tags)
|
|
541
497
|
|
|
542
|
-
|
|
543
|
-
|
|
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()
|
|
544
555
|
|
|
545
556
|
def flush(self) -> None:
|
|
557
|
+
"""Flush all pending spans to Langfuse."""
|
|
546
558
|
self._tracer.flush()
|
|
547
559
|
|
|
548
560
|
def current_span(self) -> Span | None:
|
|
@@ -558,6 +570,7 @@ class LangfuseTracer(Tracer):
|
|
|
558
570
|
def get_trace_url(self) -> str:
|
|
559
571
|
"""
|
|
560
572
|
Return the URL to the tracing data.
|
|
573
|
+
|
|
561
574
|
:return: The URL to the tracing data.
|
|
562
575
|
"""
|
|
563
576
|
return self._tracer.get_trace_url() or ""
|
|
@@ -565,6 +578,7 @@ class LangfuseTracer(Tracer):
|
|
|
565
578
|
def get_trace_id(self) -> str:
|
|
566
579
|
"""
|
|
567
580
|
Return the trace ID.
|
|
581
|
+
|
|
568
582
|
:return: The trace ID.
|
|
569
583
|
"""
|
|
570
584
|
return self._tracer.get_current_trace_id() or ""
|
|
@@ -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,44 +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
|
-
# test message input
|
|
153
144
|
span = LangfuseSpan(mock_context_manager)
|
|
154
|
-
span.set_content_tag("key.input", {"messages": [ChatMessage.from_user("message")]})
|
|
155
|
-
assert mock_context_manager._span.update.call_count == 1
|
|
156
|
-
# check we converted ChatMessage to OpenAI format
|
|
157
|
-
assert mock_context_manager._span.update.call_args_list[0][1] == {
|
|
158
|
-
"input": [{"role": "user", "content": "message"}]
|
|
159
|
-
}
|
|
160
|
-
# test replies ChatMessage list
|
|
161
|
-
mock_context_manager._span.update.reset_mock()
|
|
162
|
-
span.set_content_tag("key.output", {"replies": [ChatMessage.from_system("reply")]})
|
|
163
|
-
assert mock_context_manager._span.update.call_count == 1
|
|
164
|
-
# check we converted ChatMessage to OpenAI format
|
|
165
|
-
assert mock_context_manager._span.update.call_args_list[0][1] == {
|
|
166
|
-
"output": [{"role": "system", "content": "reply"}]
|
|
167
|
-
}
|
|
168
145
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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"}]
|
|
152
|
+
}
|
|
153
|
+
|
|
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"]}
|
|
175
182
|
|
|
176
183
|
|
|
177
184
|
class TestSpanContext:
|
|
@@ -226,7 +233,6 @@ class TestDefaultSpanHandler:
|
|
|
226
233
|
def test_create_span_root_trace(self):
|
|
227
234
|
"""Test creating a root span (no parent)."""
|
|
228
235
|
mock_client = Mock()
|
|
229
|
-
mock_client.start_as_current_span = Mock(return_value=MockContextManager())
|
|
230
236
|
mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
|
|
231
237
|
|
|
232
238
|
handler = DefaultSpanHandler()
|
|
@@ -261,6 +267,24 @@ class TestDefaultSpanHandler:
|
|
|
261
267
|
finally:
|
|
262
268
|
tracing_context_var.reset(token)
|
|
263
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
|
+
|
|
264
288
|
def test_handle_generator(self):
|
|
265
289
|
mock_span = Mock()
|
|
266
290
|
mock_span.raw_span.return_value = mock_span
|
|
@@ -332,7 +356,6 @@ class TestDefaultSpanHandler:
|
|
|
332
356
|
def test_create_span_custom_chat_generator(self):
|
|
333
357
|
"""Test that custom chat generators create 'generation' span type."""
|
|
334
358
|
mock_client = Mock()
|
|
335
|
-
mock_client.start_as_current_span = Mock(return_value=MockContextManager())
|
|
336
359
|
mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
|
|
337
360
|
|
|
338
361
|
handler = DefaultSpanHandler()
|
|
@@ -343,8 +366,9 @@ class TestDefaultSpanHandler:
|
|
|
343
366
|
operation_name="haystack.component.run",
|
|
344
367
|
component_type="MistralChatGenerator",
|
|
345
368
|
tags={},
|
|
346
|
-
parent_span=LangfuseSpan(mock_client.
|
|
369
|
+
parent_span=LangfuseSpan(mock_client.start_as_current_observation()),
|
|
347
370
|
)
|
|
371
|
+
mock_client.start_as_current_observation.reset_mock()
|
|
348
372
|
|
|
349
373
|
span = handler.create_span(context)
|
|
350
374
|
assert isinstance(span, LangfuseSpan)
|
|
@@ -355,7 +379,6 @@ class TestDefaultSpanHandler:
|
|
|
355
379
|
def test_create_span_custom_generator(self):
|
|
356
380
|
"""Test that custom generators create 'generation' span type."""
|
|
357
381
|
mock_client = Mock()
|
|
358
|
-
mock_client.start_as_current_span = Mock(return_value=MockContextManager())
|
|
359
382
|
mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
|
|
360
383
|
|
|
361
384
|
handler = DefaultSpanHandler()
|
|
@@ -366,8 +389,9 @@ class TestDefaultSpanHandler:
|
|
|
366
389
|
operation_name="haystack.component.run",
|
|
367
390
|
component_type="CustomAPIGenerator",
|
|
368
391
|
tags={},
|
|
369
|
-
parent_span=LangfuseSpan(mock_client.
|
|
392
|
+
parent_span=LangfuseSpan(mock_client.start_as_current_observation()),
|
|
370
393
|
)
|
|
394
|
+
mock_client.start_as_current_observation.reset_mock()
|
|
371
395
|
|
|
372
396
|
span = handler.create_span(context)
|
|
373
397
|
assert isinstance(span, LangfuseSpan)
|
|
@@ -378,7 +402,6 @@ class TestDefaultSpanHandler:
|
|
|
378
402
|
def test_create_span_retriever(self):
|
|
379
403
|
"""Test that retrievers create 'retriever' span type."""
|
|
380
404
|
mock_client = Mock()
|
|
381
|
-
mock_client.start_as_current_span = Mock(return_value=MockContextManager())
|
|
382
405
|
mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
|
|
383
406
|
|
|
384
407
|
handler = DefaultSpanHandler()
|
|
@@ -389,8 +412,9 @@ class TestDefaultSpanHandler:
|
|
|
389
412
|
operation_name="haystack.component.run",
|
|
390
413
|
component_type="InMemoryBM25Retriever",
|
|
391
414
|
tags={},
|
|
392
|
-
parent_span=LangfuseSpan(mock_client.
|
|
415
|
+
parent_span=LangfuseSpan(mock_client.start_as_current_observation()),
|
|
393
416
|
)
|
|
417
|
+
mock_client.start_as_current_observation.reset_mock()
|
|
394
418
|
|
|
395
419
|
span = handler.create_span(context)
|
|
396
420
|
assert isinstance(span, LangfuseSpan)
|
|
@@ -401,7 +425,6 @@ class TestDefaultSpanHandler:
|
|
|
401
425
|
def test_create_span_embedder(self):
|
|
402
426
|
"""Test that embedders create 'embedding' span type."""
|
|
403
427
|
mock_client = Mock()
|
|
404
|
-
mock_client.start_as_current_span = Mock(return_value=MockContextManager())
|
|
405
428
|
mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
|
|
406
429
|
|
|
407
430
|
handler = DefaultSpanHandler()
|
|
@@ -412,8 +435,9 @@ class TestDefaultSpanHandler:
|
|
|
412
435
|
operation_name="haystack.component.run",
|
|
413
436
|
component_type="SentenceTransformersDocumentEmbedder",
|
|
414
437
|
tags={},
|
|
415
|
-
parent_span=LangfuseSpan(mock_client.
|
|
438
|
+
parent_span=LangfuseSpan(mock_client.start_as_current_observation()),
|
|
416
439
|
)
|
|
440
|
+
mock_client.start_as_current_observation.reset_mock()
|
|
417
441
|
|
|
418
442
|
span = handler.create_span(context)
|
|
419
443
|
assert isinstance(span, LangfuseSpan)
|
|
@@ -422,9 +446,8 @@ class TestDefaultSpanHandler:
|
|
|
422
446
|
)
|
|
423
447
|
|
|
424
448
|
def test_create_span_non_component(self):
|
|
425
|
-
"""Test that non-matching components create
|
|
449
|
+
"""Test that non-matching components create default span type."""
|
|
426
450
|
mock_client = Mock()
|
|
427
|
-
mock_client.start_as_current_span = Mock(return_value=MockContextManager())
|
|
428
451
|
mock_client.start_as_current_observation = Mock(return_value=MockContextManager())
|
|
429
452
|
|
|
430
453
|
handler = DefaultSpanHandler()
|
|
@@ -435,15 +458,14 @@ class TestDefaultSpanHandler:
|
|
|
435
458
|
operation_name="haystack.component.run",
|
|
436
459
|
component_type="DocumentJoiner",
|
|
437
460
|
tags={},
|
|
438
|
-
parent_span=LangfuseSpan(mock_client.
|
|
461
|
+
parent_span=LangfuseSpan(mock_client.start_as_current_observation()),
|
|
439
462
|
)
|
|
463
|
+
mock_client.start_as_current_observation.reset_mock()
|
|
440
464
|
|
|
441
465
|
span = handler.create_span(context)
|
|
442
466
|
assert isinstance(span, LangfuseSpan)
|
|
443
|
-
# Non-matching components
|
|
444
|
-
mock_client.start_as_current_observation.
|
|
445
|
-
# Verify start_as_current_span was called for the actual span creation (not just parent)
|
|
446
|
-
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")
|
|
447
469
|
|
|
448
470
|
def test_handle_embedder_with_openai_format(self):
|
|
449
471
|
"""Test that embedder usage is extracted in OpenAI format."""
|
|
@@ -568,7 +590,7 @@ class TestLangfuseTracer:
|
|
|
568
590
|
assert tracer._public
|
|
569
591
|
|
|
570
592
|
def test_create_new_span(self):
|
|
571
|
-
mock_raw_span =
|
|
593
|
+
mock_raw_span = Mock()
|
|
572
594
|
mock_raw_span.operation_name = "operation_name"
|
|
573
595
|
mock_raw_span.metadata = {"tag1": "value1", "tag2": "value2"}
|
|
574
596
|
|
|
@@ -586,8 +608,7 @@ class TestLangfuseTracer:
|
|
|
586
608
|
mock_span_instance.get_data.return_value = {}
|
|
587
609
|
mock_span_instance._context_manager = mock_context_manager
|
|
588
610
|
|
|
589
|
-
mock_tracer =
|
|
590
|
-
mock_tracer.start_as_current_span.return_value = mock_context_manager
|
|
611
|
+
mock_tracer = Mock()
|
|
591
612
|
mock_tracer.start_as_current_observation.return_value = mock_context_manager
|
|
592
613
|
|
|
593
614
|
tracer = LangfuseTracer(tracer=mock_tracer, name="Haystack", public=False)
|
|
@@ -603,15 +624,42 @@ class TestLangfuseTracer:
|
|
|
603
624
|
# Check that the span is cleaned up after tracing
|
|
604
625
|
assert tracer.current_span() is None, "There should be no active span after tracing completes"
|
|
605
626
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
with
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
|
612
637
|
|
|
613
|
-
|
|
614
|
-
|
|
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)
|
|
615
663
|
|
|
616
664
|
def test_trace_generation(self):
|
|
617
665
|
with patch("haystack_integrations.tracing.langfuse.tracer.langfuse.get_client"):
|
|
@@ -1,29 +0,0 @@
|
|
|
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
|
|
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.0.1 → langfuse_haystack-5.0.0}/src/haystack_integrations/tracing/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|