langfuse-haystack 2.2.1__tar.gz → 3.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-2.2.1 → langfuse_haystack-3.0.0}/CHANGELOG.md +22 -0
- langfuse_haystack-3.0.0/PKG-INFO +45 -0
- langfuse_haystack-3.0.0/README.md +21 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/pyproject.toml +1 -1
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/src/haystack_integrations/tracing/langfuse/tracer.py +63 -36
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/tests/test_tracer.py +237 -148
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/tests/test_tracing.py +75 -11
- langfuse_haystack-2.2.1/PKG-INFO +0 -154
- langfuse_haystack-2.2.1/README.md +0 -130
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/.gitignore +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/LICENSE.txt +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/example/basic_rag.py +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/example/chat.py +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/example/requirements.txt +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/pydoc/config.yml +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/src/haystack_integrations/components/connectors/__init__.py +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/tests/__init__.py +0 -0
- {langfuse_haystack-2.2.1 → langfuse_haystack-3.0.0}/tests/test_langfuse_connector.py +0 -0
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [integrations/langfuse-v2.3.0] - 2025-08-25
|
|
4
|
+
|
|
5
|
+
### 🐛 Bug Fixes
|
|
6
|
+
|
|
7
|
+
- Avoid mixed Langfuse traces in async envs (#2207)
|
|
8
|
+
|
|
9
|
+
### 🧪 Testing
|
|
10
|
+
|
|
11
|
+
- Make tests successfully run from forks (#2203)
|
|
12
|
+
|
|
13
|
+
### 🧹 Chores
|
|
14
|
+
|
|
15
|
+
- Standardize readmes - part 2 (#2205)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## [integrations/langfuse-v2.2.1] - 2025-08-07
|
|
19
|
+
|
|
20
|
+
### 🚀 Features
|
|
21
|
+
|
|
22
|
+
- Add AmazonBedrockChatGenerator to supported chat generators in Langfuse (#2164)
|
|
23
|
+
|
|
24
|
+
|
|
3
25
|
## [integrations/langfuse-v2.2.0] - 2025-07-03
|
|
4
26
|
|
|
5
27
|
### 🚀 Features
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langfuse-haystack
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: Langfuse integration for Haystack
|
|
5
|
+
Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse#readme
|
|
6
|
+
Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
|
|
7
|
+
Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse
|
|
8
|
+
Author-email: deepset GmbH <info@deepset.ai>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE.txt
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: haystack-ai>=2.15.1
|
|
22
|
+
Requires-Dist: langfuse<4.0.0,>=3.0.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# langfuse-haystack
|
|
26
|
+
|
|
27
|
+
[](https://pypi.org/project/langfuse-haystack)
|
|
28
|
+
[](https://pypi.org/project/langfuse-haystack)
|
|
29
|
+
|
|
30
|
+
- [Integration page](https://haystack.deepset.ai/integrations/langfuse)
|
|
31
|
+
- [Changelog](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/langfuse/CHANGELOG.md)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Contributing
|
|
36
|
+
|
|
37
|
+
Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
|
|
38
|
+
|
|
39
|
+
To run integration tests locally, you need to export the following environment variables:
|
|
40
|
+
|
|
41
|
+
- `LANGFUSE_SECRET_KEY`
|
|
42
|
+
- `LANGFUSE_PUBLIC_KEY`
|
|
43
|
+
- `OPENAI_API_KEY`
|
|
44
|
+
- `ANTHROPIC_API_KEY`
|
|
45
|
+
- `COHERE_API_KEY`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# langfuse-haystack
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/langfuse-haystack)
|
|
4
|
+
[](https://pypi.org/project/langfuse-haystack)
|
|
5
|
+
|
|
6
|
+
- [Integration page](https://haystack.deepset.ai/integrations/langfuse)
|
|
7
|
+
- [Changelog](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/langfuse/CHANGELOG.md)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Contributing
|
|
12
|
+
|
|
13
|
+
Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
|
|
14
|
+
|
|
15
|
+
To run integration tests locally, you need to export the following environment variables:
|
|
16
|
+
|
|
17
|
+
- `LANGFUSE_SECRET_KEY`
|
|
18
|
+
- `LANGFUSE_PUBLIC_KEY`
|
|
19
|
+
- `OPENAI_API_KEY`
|
|
20
|
+
- `ANTHROPIC_API_KEY`
|
|
21
|
+
- `COHERE_API_KEY`.
|
|
@@ -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.15.1", "langfuse>=
|
|
25
|
+
dependencies = ["haystack-ai>=2.15.1", "langfuse>=3.0.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"
|
|
@@ -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
|
|
|
@@ -61,25 +58,31 @@ _COMPONENT_TYPE_KEY = "haystack.component.type"
|
|
|
61
58
|
_COMPONENT_OUTPUT_KEY = "haystack.component.output"
|
|
62
59
|
_COMPONENT_INPUT_KEY = "haystack.component.input"
|
|
63
60
|
|
|
64
|
-
#
|
|
65
|
-
#
|
|
61
|
+
# External session metadata for trace correlation (Haystack system)
|
|
62
|
+
# Stores trace_id, user_id, session_id, tags, version for root trace creation
|
|
66
63
|
tracing_context_var: ContextVar[Dict[Any, Any]] = ContextVar("tracing_context")
|
|
67
64
|
|
|
65
|
+
# Internal span execution hierarchy for our tracer
|
|
66
|
+
# Manages parent-child relationships and prevents cross-request span interleaving
|
|
67
|
+
span_stack_var: ContextVar[Optional[List["LangfuseSpan"]]] = ContextVar("span_stack", default=None)
|
|
68
|
+
|
|
68
69
|
|
|
69
70
|
class LangfuseSpan(Span):
|
|
70
71
|
"""
|
|
71
72
|
Internal class representing a bridge between the Haystack span tracing API and Langfuse.
|
|
72
73
|
"""
|
|
73
74
|
|
|
74
|
-
def __init__(self,
|
|
75
|
+
def __init__(self, context_manager: AbstractContextManager) -> None:
|
|
75
76
|
"""
|
|
76
77
|
Initialize a LangfuseSpan instance.
|
|
77
78
|
|
|
78
|
-
: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`.
|
|
79
82
|
"""
|
|
80
|
-
self._span =
|
|
81
|
-
# locally cache tags
|
|
83
|
+
self._span = context_manager.__enter__()
|
|
82
84
|
self._data: Dict[str, Any] = {}
|
|
85
|
+
self._context_manager = context_manager
|
|
83
86
|
|
|
84
87
|
def set_tag(self, key: str, value: Any) -> None:
|
|
85
88
|
"""
|
|
@@ -121,7 +124,7 @@ class LangfuseSpan(Span):
|
|
|
121
124
|
|
|
122
125
|
self._data[key] = value
|
|
123
126
|
|
|
124
|
-
def raw_span(self) ->
|
|
127
|
+
def raw_span(self) -> LangfuseClientSpan:
|
|
125
128
|
"""
|
|
126
129
|
Return the underlying span instance.
|
|
127
130
|
|
|
@@ -265,24 +268,39 @@ class DefaultSpanHandler(SpanHandler):
|
|
|
265
268
|
)
|
|
266
269
|
raise RuntimeError(message)
|
|
267
270
|
|
|
271
|
+
# Get external tracing context for root trace creation (correlation metadata)
|
|
268
272
|
tracing_ctx = tracing_context_var.get({})
|
|
269
273
|
if not context.parent_span:
|
|
270
274
|
# Create a new trace when there's no parent span
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
public=context.public,
|
|
275
|
-
id=tracing_ctx.get("trace_id"),
|
|
276
|
-
user_id=tracing_ctx.get("user_id"),
|
|
277
|
-
session_id=tracing_ctx.get("session_id"),
|
|
278
|
-
tags=tracing_ctx.get("tags"),
|
|
279
|
-
version=tracing_ctx.get("version"),
|
|
280
|
-
)
|
|
275
|
+
span_context_manager = self.tracer.start_as_current_span(
|
|
276
|
+
name=context.trace_name,
|
|
277
|
+
version=tracing_ctx.get("version"),
|
|
281
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
|
+
}
|
|
293
|
+
|
|
294
|
+
# Filter out None values and apply trace attributes
|
|
295
|
+
trace_attrs = {k: v for k, v in trace_metadata.items() if v is not None}
|
|
296
|
+
if trace_attrs:
|
|
297
|
+
span._span.update_trace(**trace_attrs)
|
|
298
|
+
|
|
299
|
+
return span
|
|
282
300
|
elif context.component_type in _ALL_SUPPORTED_GENERATORS:
|
|
283
|
-
return LangfuseSpan(
|
|
301
|
+
return LangfuseSpan(self.tracer.start_as_current_observation(name=context.name, as_type="generation"))
|
|
284
302
|
else:
|
|
285
|
-
return LangfuseSpan(
|
|
303
|
+
return LangfuseSpan(self.tracer.start_as_current_span(name=context.name))
|
|
286
304
|
|
|
287
305
|
def handle(self, span: LangfuseSpan, component_type: Optional[str]) -> None:
|
|
288
306
|
# If the span is at the pipeline level, we add input and output keys to the span
|
|
@@ -290,8 +308,7 @@ class DefaultSpanHandler(SpanHandler):
|
|
|
290
308
|
if at_pipeline_level:
|
|
291
309
|
coerced_input = tracing_utils.coerce_tag_value(span.get_data().get(_PIPELINE_INPUT_KEY))
|
|
292
310
|
coerced_output = tracing_utils.coerce_tag_value(span.get_data().get(_PIPELINE_OUTPUT_KEY))
|
|
293
|
-
span.raw_span().
|
|
294
|
-
|
|
311
|
+
span.raw_span().update_trace(input=coerced_input, output=coerced_output)
|
|
295
312
|
# special case for ToolInvoker (to update the span name to be: `original_component_name - [tool_names]`)
|
|
296
313
|
if component_type == "ToolInvoker":
|
|
297
314
|
tool_names: List[str] = []
|
|
@@ -360,6 +377,7 @@ class LangfuseTracer(Tracer):
|
|
|
360
377
|
"before importing Haystack."
|
|
361
378
|
)
|
|
362
379
|
self._tracer = tracer
|
|
380
|
+
# Keep _context as deprecated shim to avoid AttributeError if anyone uses it
|
|
363
381
|
self._context: List[LangfuseSpan] = []
|
|
364
382
|
self._name = name
|
|
365
383
|
self._public = public
|
|
@@ -391,7 +409,12 @@ class LangfuseTracer(Tracer):
|
|
|
391
409
|
# Create span using the handler
|
|
392
410
|
span = self._span_handler.create_span(span_context)
|
|
393
411
|
|
|
394
|
-
|
|
412
|
+
# Build new span hierarchy: copy existing stack, add new span, save for restoration
|
|
413
|
+
prev_stack = span_stack_var.get()
|
|
414
|
+
new_stack = (prev_stack or []).copy()
|
|
415
|
+
new_stack.append(span)
|
|
416
|
+
token = span_stack_var.set(new_stack)
|
|
417
|
+
|
|
395
418
|
span.set_tags(tags)
|
|
396
419
|
|
|
397
420
|
try:
|
|
@@ -404,7 +427,11 @@ class LangfuseTracer(Tracer):
|
|
|
404
427
|
|
|
405
428
|
# End span (may fail if span data is corrupted)
|
|
406
429
|
raw_span = span.raw_span()
|
|
407
|
-
|
|
430
|
+
# In v3, we need to properly exit context managers
|
|
431
|
+
if span._context_manager is not None:
|
|
432
|
+
span._context_manager.__exit__(None, None, None)
|
|
433
|
+
elif hasattr(raw_span, "end"):
|
|
434
|
+
# Only call end() if it's not a context manager
|
|
408
435
|
raw_span.end()
|
|
409
436
|
except Exception as cleanup_error:
|
|
410
437
|
# Log cleanup errors but don't let them corrupt context
|
|
@@ -414,10 +441,8 @@ class LangfuseTracer(Tracer):
|
|
|
414
441
|
cleanup_error=cleanup_error,
|
|
415
442
|
)
|
|
416
443
|
finally:
|
|
417
|
-
#
|
|
418
|
-
|
|
419
|
-
if self._context and self._context[-1] == span:
|
|
420
|
-
self._context.pop()
|
|
444
|
+
# Restore previous span stack using saved token - ensures proper cleanup
|
|
445
|
+
span_stack_var.reset(token)
|
|
421
446
|
|
|
422
447
|
if self.enforce_flush:
|
|
423
448
|
self.flush()
|
|
@@ -431,7 +456,9 @@ class LangfuseTracer(Tracer):
|
|
|
431
456
|
|
|
432
457
|
:return: The current span if available, else None.
|
|
433
458
|
"""
|
|
434
|
-
|
|
459
|
+
# Get top of span stack (most recent span) from context-local storage
|
|
460
|
+
stack = span_stack_var.get()
|
|
461
|
+
return stack[-1] if stack else None
|
|
435
462
|
|
|
436
463
|
def get_trace_url(self) -> str:
|
|
437
464
|
"""
|
|
@@ -445,4 +472,4 @@ class LangfuseTracer(Tracer):
|
|
|
445
472
|
Return the trace ID.
|
|
446
473
|
:return: The trace ID.
|
|
447
474
|
"""
|
|
448
|
-
return self._tracer.
|
|
475
|
+
return self._tracer.get_current_trace_id()
|