langfuse-haystack 0.7.0__tar.gz → 0.8.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-0.7.0 → langfuse_haystack-0.8.0}/CHANGELOG.md +11 -0
- {langfuse_haystack-0.7.0 → langfuse_haystack-0.8.0}/PKG-INFO +1 -1
- {langfuse_haystack-0.7.0 → langfuse_haystack-0.8.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +34 -2
- langfuse_haystack-0.8.0/src/haystack_integrations/tracing/langfuse/__init__.py +6 -0
- langfuse_haystack-0.8.0/src/haystack_integrations/tracing/langfuse/tracer.py +382 -0
- {langfuse_haystack-0.7.0 → langfuse_haystack-0.8.0}/tests/test_tracing.py +86 -0
- langfuse_haystack-0.7.0/src/haystack_integrations/tracing/langfuse/__init__.py +0 -6
- langfuse_haystack-0.7.0/src/haystack_integrations/tracing/langfuse/tracer.py +0 -231
- {langfuse_haystack-0.7.0 → langfuse_haystack-0.8.0}/.gitignore +0 -0
- {langfuse_haystack-0.7.0 → langfuse_haystack-0.8.0}/LICENSE.txt +0 -0
- {langfuse_haystack-0.7.0 → langfuse_haystack-0.8.0}/README.md +0 -0
- {langfuse_haystack-0.7.0 → langfuse_haystack-0.8.0}/example/basic_rag.py +0 -0
- {langfuse_haystack-0.7.0 → langfuse_haystack-0.8.0}/example/chat.py +0 -0
- {langfuse_haystack-0.7.0 → langfuse_haystack-0.8.0}/example/requirements.txt +0 -0
- {langfuse_haystack-0.7.0 → langfuse_haystack-0.8.0}/pydoc/config.yml +0 -0
- {langfuse_haystack-0.7.0 → langfuse_haystack-0.8.0}/pyproject.toml +0 -0
- {langfuse_haystack-0.7.0 → langfuse_haystack-0.8.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
- {langfuse_haystack-0.7.0 → langfuse_haystack-0.8.0}/tests/__init__.py +0 -0
- {langfuse_haystack-0.7.0 → langfuse_haystack-0.8.0}/tests/test_langfuse_span.py +0 -0
- {langfuse_haystack-0.7.0 → langfuse_haystack-0.8.0}/tests/test_tracer.py +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [integrations/langfuse-v0.7.0] - 2025-01-21
|
|
4
|
+
|
|
5
|
+
### 🚀 Features
|
|
6
|
+
|
|
7
|
+
- LangfuseConnector - add httpx.Client init param (#1308)
|
|
8
|
+
|
|
9
|
+
### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- End langfuse generation spans properly (#1301)
|
|
12
|
+
|
|
13
|
+
|
|
3
14
|
## [integrations/langfuse-v0.6.4] - 2025-01-17
|
|
4
15
|
|
|
5
16
|
### 🚀 Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langfuse-haystack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse#readme
|
|
5
5
|
Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
|
|
6
6
|
Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse
|
|
@@ -3,8 +3,9 @@ from typing import Any, Dict, Optional
|
|
|
3
3
|
import httpx
|
|
4
4
|
from haystack import component, default_from_dict, default_to_dict, logging, tracing
|
|
5
5
|
from haystack.utils import Secret, deserialize_secrets_inplace
|
|
6
|
+
from haystack.utils.base_serialization import deserialize_class_instance, serialize_class_instance
|
|
6
7
|
|
|
7
|
-
from haystack_integrations.tracing.langfuse import LangfuseTracer
|
|
8
|
+
from haystack_integrations.tracing.langfuse import LangfuseTracer, SpanHandler
|
|
8
9
|
from langfuse import Langfuse
|
|
9
10
|
|
|
10
11
|
logger = logging.getLogger(__name__)
|
|
@@ -94,6 +95,24 @@ class LangfuseConnector:
|
|
|
94
95
|
print(response["tracer"]["trace_url"])
|
|
95
96
|
```
|
|
96
97
|
|
|
98
|
+
For advanced use cases, you can also customize how spans are created and processed by
|
|
99
|
+
providing a custom SpanHandler. This allows you to add custom metrics, set warning levels,
|
|
100
|
+
or attach additional metadata to your Langfuse traces:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from haystack_integrations.tracing.langfuse import DefaultSpanHandler, LangfuseSpan
|
|
104
|
+
from typing import Optional
|
|
105
|
+
|
|
106
|
+
class CustomSpanHandler(DefaultSpanHandler):
|
|
107
|
+
|
|
108
|
+
def handle(self, span: LangfuseSpan, component_type: Optional[str]) -> None:
|
|
109
|
+
# Custom span handling logic, customize Langfuse spans however it fits you
|
|
110
|
+
# see DefaultSpanHandler for how we create and process spans by default
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
connector = LangfuseConnector(span_handler=CustomSpanHandler())
|
|
114
|
+
```
|
|
115
|
+
|
|
97
116
|
"""
|
|
98
117
|
|
|
99
118
|
def __init__(
|
|
@@ -103,6 +122,7 @@ class LangfuseConnector:
|
|
|
103
122
|
public_key: Optional[Secret] = Secret.from_env_var("LANGFUSE_PUBLIC_KEY"), # noqa: B008
|
|
104
123
|
secret_key: Optional[Secret] = Secret.from_env_var("LANGFUSE_SECRET_KEY"), # noqa: B008
|
|
105
124
|
httpx_client: Optional[httpx.Client] = None,
|
|
125
|
+
span_handler: Optional[SpanHandler] = None,
|
|
106
126
|
):
|
|
107
127
|
"""
|
|
108
128
|
Initialize the LangfuseConnector component.
|
|
@@ -117,11 +137,16 @@ class LangfuseConnector:
|
|
|
117
137
|
:param httpx_client: Optional custom httpx.Client instance to use for Langfuse API calls. Note that when
|
|
118
138
|
deserializing a pipeline from YAML, any custom client is discarded and Langfuse will create its own default
|
|
119
139
|
client, since HTTPX clients cannot be serialized.
|
|
140
|
+
:param span_handler: Optional custom handler for processing spans. If None, uses DefaultSpanHandler.
|
|
141
|
+
The span handler controls how spans are created and processed, allowing customization of span types
|
|
142
|
+
based on component types and additional processing after spans are yielded. See SpanHandler class for
|
|
143
|
+
details on implementing custom handlers.
|
|
120
144
|
"""
|
|
121
145
|
self.name = name
|
|
122
146
|
self.public = public
|
|
123
147
|
self.secret_key = secret_key
|
|
124
148
|
self.public_key = public_key
|
|
149
|
+
self.span_handler = span_handler
|
|
125
150
|
self.tracer = LangfuseTracer(
|
|
126
151
|
tracer=Langfuse(
|
|
127
152
|
secret_key=secret_key.resolve_value() if secret_key else None,
|
|
@@ -130,6 +155,7 @@ class LangfuseConnector:
|
|
|
130
155
|
),
|
|
131
156
|
name=name,
|
|
132
157
|
public=public,
|
|
158
|
+
span_handler=span_handler,
|
|
133
159
|
)
|
|
134
160
|
tracing.enable_tracing(self.tracer)
|
|
135
161
|
|
|
@@ -158,6 +184,7 @@ class LangfuseConnector:
|
|
|
158
184
|
|
|
159
185
|
:returns: The serialized component as a dictionary.
|
|
160
186
|
"""
|
|
187
|
+
span_handler = serialize_class_instance(self.span_handler) if self.span_handler else None
|
|
161
188
|
return default_to_dict(
|
|
162
189
|
self,
|
|
163
190
|
name=self.name,
|
|
@@ -165,6 +192,7 @@ class LangfuseConnector:
|
|
|
165
192
|
secret_key=self.secret_key.to_dict() if self.secret_key else None,
|
|
166
193
|
public_key=self.public_key.to_dict() if self.public_key else None,
|
|
167
194
|
# Note: httpx_client is not serialized as it's not serializable
|
|
195
|
+
span_handler=span_handler,
|
|
168
196
|
)
|
|
169
197
|
|
|
170
198
|
@classmethod
|
|
@@ -175,5 +203,9 @@ class LangfuseConnector:
|
|
|
175
203
|
:param data: The dictionary representation of this component.
|
|
176
204
|
:returns: The deserialized component instance.
|
|
177
205
|
"""
|
|
178
|
-
|
|
206
|
+
init_params = data["init_parameters"]
|
|
207
|
+
deserialize_secrets_inplace(init_params, keys=["secret_key", "public_key"])
|
|
208
|
+
init_params["span_handler"] = (
|
|
209
|
+
deserialize_class_instance(init_params["span_handler"]) if init_params["span_handler"] else None
|
|
210
|
+
)
|
|
179
211
|
return default_from_dict(cls, data)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
from .tracer import DefaultSpanHandler, LangfuseSpan, LangfuseTracer, SpanContext, SpanHandler
|
|
5
|
+
|
|
6
|
+
__all__ = ["DefaultSpanHandler", "LangfuseSpan", "LangfuseTracer", "SpanContext", "SpanHandler"]
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import os
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from contextvars import ContextVar
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any, Dict, Iterator, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
from haystack import default_from_dict, default_to_dict, logging
|
|
10
|
+
from haystack.dataclasses import ChatMessage
|
|
11
|
+
from haystack.tracing import Span, Tracer
|
|
12
|
+
from haystack.tracing import tracer as proxy_tracer
|
|
13
|
+
from haystack.tracing import utils as tracing_utils
|
|
14
|
+
from typing_extensions import TypeAlias
|
|
15
|
+
|
|
16
|
+
import langfuse
|
|
17
|
+
from langfuse.client import StatefulGenerationClient, StatefulSpanClient, StatefulTraceClient
|
|
18
|
+
|
|
19
|
+
# Type alias for Langfuse stateful clients
|
|
20
|
+
LangfuseStatefulClient: TypeAlias = Union[StatefulTraceClient, StatefulSpanClient, StatefulGenerationClient]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
HAYSTACK_LANGFUSE_ENFORCE_FLUSH_ENV_VAR = "HAYSTACK_LANGFUSE_ENFORCE_FLUSH"
|
|
26
|
+
_SUPPORTED_GENERATORS = [
|
|
27
|
+
"AzureOpenAIGenerator",
|
|
28
|
+
"OpenAIGenerator",
|
|
29
|
+
"AnthropicGenerator",
|
|
30
|
+
"HuggingFaceAPIGenerator",
|
|
31
|
+
"HuggingFaceLocalGenerator",
|
|
32
|
+
"CohereGenerator",
|
|
33
|
+
]
|
|
34
|
+
_SUPPORTED_CHAT_GENERATORS = [
|
|
35
|
+
"AzureOpenAIChatGenerator",
|
|
36
|
+
"OpenAIChatGenerator",
|
|
37
|
+
"AnthropicChatGenerator",
|
|
38
|
+
"HuggingFaceAPIChatGenerator",
|
|
39
|
+
"HuggingFaceLocalChatGenerator",
|
|
40
|
+
"CohereChatGenerator",
|
|
41
|
+
]
|
|
42
|
+
_ALL_SUPPORTED_GENERATORS = _SUPPORTED_GENERATORS + _SUPPORTED_CHAT_GENERATORS
|
|
43
|
+
|
|
44
|
+
# These are the keys used by Haystack for traces and span.
|
|
45
|
+
# We keep them here to avoid making typos when using them.
|
|
46
|
+
_PIPELINE_RUN_KEY = "haystack.pipeline.run"
|
|
47
|
+
_COMPONENT_NAME_KEY = "haystack.component.name"
|
|
48
|
+
_COMPONENT_TYPE_KEY = "haystack.component.type"
|
|
49
|
+
_COMPONENT_OUTPUT_KEY = "haystack.component.output"
|
|
50
|
+
|
|
51
|
+
# Context var used to keep track of tracing related info.
|
|
52
|
+
# This mainly useful for parents spans.
|
|
53
|
+
tracing_context_var: ContextVar[Dict[Any, Any]] = ContextVar("tracing_context")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class LangfuseSpan(Span):
|
|
57
|
+
"""
|
|
58
|
+
Internal class representing a bridge between the Haystack span tracing API and Langfuse.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, span: LangfuseStatefulClient) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Initialize a LangfuseSpan instance.
|
|
64
|
+
|
|
65
|
+
:param span: The span instance managed by Langfuse.
|
|
66
|
+
"""
|
|
67
|
+
self._span = span
|
|
68
|
+
# locally cache tags
|
|
69
|
+
self._data: Dict[str, Any] = {}
|
|
70
|
+
|
|
71
|
+
def set_tag(self, key: str, value: Any) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Set a generic tag for this span.
|
|
74
|
+
|
|
75
|
+
:param key: The tag key.
|
|
76
|
+
:param value: The tag value.
|
|
77
|
+
"""
|
|
78
|
+
coerced_value = tracing_utils.coerce_tag_value(value)
|
|
79
|
+
self._span.update(metadata={key: coerced_value})
|
|
80
|
+
self._data[key] = value
|
|
81
|
+
|
|
82
|
+
def set_content_tag(self, key: str, value: Any) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Set a content-specific tag for this span.
|
|
85
|
+
|
|
86
|
+
:param key: The content tag key.
|
|
87
|
+
:param value: The content tag value.
|
|
88
|
+
"""
|
|
89
|
+
if not proxy_tracer.is_content_tracing_enabled:
|
|
90
|
+
return
|
|
91
|
+
if key.endswith(".input"):
|
|
92
|
+
if "messages" in value:
|
|
93
|
+
messages = [m.to_openai_dict_format() for m in value["messages"]]
|
|
94
|
+
self._span.update(input=messages)
|
|
95
|
+
else:
|
|
96
|
+
self._span.update(input=value)
|
|
97
|
+
elif key.endswith(".output"):
|
|
98
|
+
if "replies" in value:
|
|
99
|
+
if all(isinstance(r, ChatMessage) for r in value["replies"]):
|
|
100
|
+
replies = [m.to_openai_dict_format() for m in value["replies"]]
|
|
101
|
+
else:
|
|
102
|
+
replies = value["replies"]
|
|
103
|
+
self._span.update(output=replies)
|
|
104
|
+
else:
|
|
105
|
+
self._span.update(output=value)
|
|
106
|
+
|
|
107
|
+
self._data[key] = value
|
|
108
|
+
|
|
109
|
+
def raw_span(self) -> LangfuseStatefulClient:
|
|
110
|
+
"""
|
|
111
|
+
Return the underlying span instance.
|
|
112
|
+
|
|
113
|
+
:return: The Langfuse span instance.
|
|
114
|
+
"""
|
|
115
|
+
return self._span
|
|
116
|
+
|
|
117
|
+
def get_correlation_data_for_logs(self) -> Dict[str, Any]:
|
|
118
|
+
return {}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class SpanContext:
|
|
123
|
+
"""
|
|
124
|
+
Context for creating spans in Langfuse.
|
|
125
|
+
|
|
126
|
+
Encapsulates the information needed to create and configure a span in Langfuse tracing.
|
|
127
|
+
Used by SpanHandler to determine the span type (trace, generation, or default) and its configuration.
|
|
128
|
+
|
|
129
|
+
:param name: The name of the span to create. For components, this is typically the component name.
|
|
130
|
+
:param operation_name: The operation being traced (e.g. "haystack.pipeline.run"). Used to determine
|
|
131
|
+
if a new trace should be created without warning.
|
|
132
|
+
:param component_type: The type of component creating the span (e.g. "OpenAIChatGenerator").
|
|
133
|
+
Can be used to determine the type of span to create.
|
|
134
|
+
:param tags: Additional metadata to attach to the span. Contains component input/output data
|
|
135
|
+
and other trace information.
|
|
136
|
+
:param parent_span: The parent span if this is a child span. If None, a new trace will be created.
|
|
137
|
+
:param trace_name: The name to use for the trace when creating a parent span. Defaults to "Haystack".
|
|
138
|
+
:param public: Whether traces should be publicly accessible. Defaults to False.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
name: str
|
|
142
|
+
operation_name: str
|
|
143
|
+
component_type: Optional[str]
|
|
144
|
+
tags: Dict[str, Any]
|
|
145
|
+
parent_span: Optional[Span]
|
|
146
|
+
trace_name: str = "Haystack"
|
|
147
|
+
public: bool = False
|
|
148
|
+
|
|
149
|
+
def __post_init__(self) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Validate the span context attributes.
|
|
152
|
+
|
|
153
|
+
:raises ValueError: If name, operation_name or trace_name are empty
|
|
154
|
+
:raises TypeError: If tags is not a dictionary
|
|
155
|
+
"""
|
|
156
|
+
if not self.name:
|
|
157
|
+
msg = "Span name cannot be empty"
|
|
158
|
+
raise ValueError(msg)
|
|
159
|
+
if not self.operation_name:
|
|
160
|
+
msg = "Operation name cannot be empty"
|
|
161
|
+
raise ValueError(msg)
|
|
162
|
+
if not self.trace_name:
|
|
163
|
+
msg = "Trace name cannot be empty"
|
|
164
|
+
raise ValueError(msg)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class SpanHandler(ABC):
|
|
168
|
+
"""
|
|
169
|
+
Abstract base class for customizing how Langfuse spans are created and processed.
|
|
170
|
+
|
|
171
|
+
This class defines two key extension points:
|
|
172
|
+
1. create_span: Controls what type of span to create (default or generation)
|
|
173
|
+
2. handle: Processes the span after component execution (adding metadata, metrics, etc.)
|
|
174
|
+
|
|
175
|
+
To implement a custom handler:
|
|
176
|
+
- Extend this class or DefaultSpanHandler
|
|
177
|
+
- Override create_span and handle methods. It is more common to override handle.
|
|
178
|
+
- Pass your handler to LangfuseConnector init method
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def __init__(self):
|
|
182
|
+
self.tracer: Optional[langfuse.Langfuse] = None
|
|
183
|
+
|
|
184
|
+
def init_tracer(self, tracer: langfuse.Langfuse) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Initialize with Langfuse tracer. Called internally by LangfuseTracer.
|
|
187
|
+
|
|
188
|
+
:param tracer: The Langfuse client instance to use for creating spans
|
|
189
|
+
"""
|
|
190
|
+
self.tracer = tracer
|
|
191
|
+
|
|
192
|
+
@abstractmethod
|
|
193
|
+
def create_span(self, context: SpanContext) -> LangfuseSpan:
|
|
194
|
+
"""
|
|
195
|
+
Create a span of appropriate type based on the context.
|
|
196
|
+
|
|
197
|
+
This method determines what kind of span to create:
|
|
198
|
+
- A new trace if there's no parent span
|
|
199
|
+
- A generation span for LLM components
|
|
200
|
+
- A default span for other components
|
|
201
|
+
|
|
202
|
+
:param context: The context containing all information needed to create the span
|
|
203
|
+
:returns: A new LangfuseSpan instance configured according to the context
|
|
204
|
+
"""
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
@abstractmethod
|
|
208
|
+
def handle(self, span: LangfuseSpan, component_type: Optional[str]) -> None:
|
|
209
|
+
"""
|
|
210
|
+
Process a span after component execution by attaching metadata and metrics.
|
|
211
|
+
|
|
212
|
+
This method is called after the component yields its span, allowing you to:
|
|
213
|
+
- Extract and attach token usage statistics
|
|
214
|
+
- Add model information
|
|
215
|
+
- Record timing data (e.g., time-to-first-token)
|
|
216
|
+
- Set log levels for quality monitoring
|
|
217
|
+
- Add custom metrics and observations
|
|
218
|
+
|
|
219
|
+
:param span: The span that was yielded by the component
|
|
220
|
+
:param component_type: The type of component that created the span, used to determine
|
|
221
|
+
what metadata to extract and how to process it
|
|
222
|
+
"""
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
@classmethod
|
|
226
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SpanHandler":
|
|
227
|
+
return default_from_dict(cls, data)
|
|
228
|
+
|
|
229
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
230
|
+
return default_to_dict(self)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class DefaultSpanHandler(SpanHandler):
|
|
234
|
+
"""DefaultSpanHandler provides the default Langfuse tracing behavior for Haystack."""
|
|
235
|
+
|
|
236
|
+
def create_span(self, context: SpanContext) -> LangfuseSpan:
|
|
237
|
+
message = "Tracer is not initialized"
|
|
238
|
+
if self.tracer is None:
|
|
239
|
+
raise RuntimeError(message)
|
|
240
|
+
tracing_ctx = tracing_context_var.get({})
|
|
241
|
+
if not context.parent_span:
|
|
242
|
+
if context.operation_name != _PIPELINE_RUN_KEY:
|
|
243
|
+
logger.warning(
|
|
244
|
+
"Creating a new trace without a parent span is not recommended for operation '{operation_name}'.",
|
|
245
|
+
operation_name=context.operation_name,
|
|
246
|
+
)
|
|
247
|
+
# Create a new trace when there's no parent span
|
|
248
|
+
return LangfuseSpan(
|
|
249
|
+
self.tracer.trace(
|
|
250
|
+
name=context.trace_name,
|
|
251
|
+
public=context.public,
|
|
252
|
+
id=tracing_ctx.get("trace_id"),
|
|
253
|
+
user_id=tracing_ctx.get("user_id"),
|
|
254
|
+
session_id=tracing_ctx.get("session_id"),
|
|
255
|
+
tags=tracing_ctx.get("tags"),
|
|
256
|
+
version=tracing_ctx.get("version"),
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
elif context.component_type in _ALL_SUPPORTED_GENERATORS:
|
|
260
|
+
return LangfuseSpan(context.parent_span.raw_span().generation(name=context.name))
|
|
261
|
+
else:
|
|
262
|
+
return LangfuseSpan(context.parent_span.raw_span().span(name=context.name))
|
|
263
|
+
|
|
264
|
+
def handle(self, span: LangfuseSpan, component_type: Optional[str]) -> None:
|
|
265
|
+
if component_type in _SUPPORTED_GENERATORS:
|
|
266
|
+
meta = span._data.get(_COMPONENT_OUTPUT_KEY, {}).get("meta")
|
|
267
|
+
if meta:
|
|
268
|
+
m = meta[0]
|
|
269
|
+
span._span.update(usage=m.get("usage") or None, model=m.get("model"))
|
|
270
|
+
elif component_type in _SUPPORTED_CHAT_GENERATORS:
|
|
271
|
+
replies = span._data.get(_COMPONENT_OUTPUT_KEY, {}).get("replies")
|
|
272
|
+
if replies:
|
|
273
|
+
meta = replies[0].meta
|
|
274
|
+
completion_start_time = meta.get("completion_start_time")
|
|
275
|
+
if completion_start_time:
|
|
276
|
+
try:
|
|
277
|
+
completion_start_time = datetime.fromisoformat(completion_start_time)
|
|
278
|
+
except ValueError:
|
|
279
|
+
logger.error(f"Failed to parse completion_start_time: {completion_start_time}")
|
|
280
|
+
completion_start_time = None
|
|
281
|
+
span._span.update(
|
|
282
|
+
usage=meta.get("usage") or None,
|
|
283
|
+
model=meta.get("model"),
|
|
284
|
+
completion_start_time=completion_start_time,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class LangfuseTracer(Tracer):
|
|
289
|
+
"""
|
|
290
|
+
Internal class representing a bridge between the Haystack tracer and Langfuse.
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
def __init__(
|
|
294
|
+
self,
|
|
295
|
+
tracer: langfuse.Langfuse,
|
|
296
|
+
name: str = "Haystack",
|
|
297
|
+
public: bool = False,
|
|
298
|
+
span_handler: Optional[SpanHandler] = None,
|
|
299
|
+
) -> None:
|
|
300
|
+
"""
|
|
301
|
+
Initialize a LangfuseTracer instance.
|
|
302
|
+
|
|
303
|
+
:param tracer: The Langfuse tracer instance.
|
|
304
|
+
:param name: The name of the pipeline or component. This name will be used to identify the tracing run on the
|
|
305
|
+
Langfuse dashboard.
|
|
306
|
+
:param public: Whether the tracing data should be public or private. If set to `True`, the tracing data will
|
|
307
|
+
be publicly accessible to anyone with the tracing URL. If set to `False`, the tracing data will be private
|
|
308
|
+
and only accessible to the Langfuse account owner.
|
|
309
|
+
:param span_handler: Custom handler for processing spans. If None, uses DefaultSpanHandler.
|
|
310
|
+
"""
|
|
311
|
+
if not proxy_tracer.is_content_tracing_enabled:
|
|
312
|
+
logger.warning(
|
|
313
|
+
"Traces will not be logged to Langfuse because Haystack tracing is disabled. "
|
|
314
|
+
"To enable, set the HAYSTACK_CONTENT_TRACING_ENABLED environment variable to true "
|
|
315
|
+
"before importing Haystack."
|
|
316
|
+
)
|
|
317
|
+
self._tracer = tracer
|
|
318
|
+
self._context: List[LangfuseSpan] = []
|
|
319
|
+
self._name = name
|
|
320
|
+
self._public = public
|
|
321
|
+
self.enforce_flush = os.getenv(HAYSTACK_LANGFUSE_ENFORCE_FLUSH_ENV_VAR, "true").lower() == "true"
|
|
322
|
+
self._span_handler = span_handler or DefaultSpanHandler()
|
|
323
|
+
self._span_handler.init_tracer(tracer)
|
|
324
|
+
|
|
325
|
+
@contextlib.contextmanager
|
|
326
|
+
def trace(
|
|
327
|
+
self, operation_name: str, tags: Optional[Dict[str, Any]] = None, parent_span: Optional[Span] = None
|
|
328
|
+
) -> Iterator[Span]:
|
|
329
|
+
tags = tags or {}
|
|
330
|
+
span_name = tags.get(_COMPONENT_NAME_KEY, operation_name)
|
|
331
|
+
component_type = tags.get(_COMPONENT_TYPE_KEY)
|
|
332
|
+
|
|
333
|
+
# Create span using the handler
|
|
334
|
+
span = self._span_handler.create_span(
|
|
335
|
+
SpanContext(
|
|
336
|
+
name=span_name,
|
|
337
|
+
operation_name=operation_name,
|
|
338
|
+
component_type=component_type,
|
|
339
|
+
tags=tags,
|
|
340
|
+
parent_span=parent_span,
|
|
341
|
+
trace_name=self._name,
|
|
342
|
+
public=self._public,
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
self._context.append(span)
|
|
347
|
+
span.set_tags(tags)
|
|
348
|
+
|
|
349
|
+
yield span
|
|
350
|
+
|
|
351
|
+
# Let the span handler process the span
|
|
352
|
+
self._span_handler.handle(span, component_type)
|
|
353
|
+
|
|
354
|
+
raw_span = span.raw_span()
|
|
355
|
+
|
|
356
|
+
# In this section, we finalize both regular spans and generation spans created using the LangfuseSpan class.
|
|
357
|
+
# It's important to end() these spans to ensure they are properly closed and all relevant data is recorded.
|
|
358
|
+
# Note that we do not call end() on the main trace span itself, as its lifecycle is managed differently.
|
|
359
|
+
if isinstance(raw_span, (StatefulSpanClient, StatefulGenerationClient)):
|
|
360
|
+
raw_span.end()
|
|
361
|
+
self._context.pop()
|
|
362
|
+
|
|
363
|
+
if self.enforce_flush:
|
|
364
|
+
self.flush()
|
|
365
|
+
|
|
366
|
+
def flush(self):
|
|
367
|
+
self._tracer.flush()
|
|
368
|
+
|
|
369
|
+
def current_span(self) -> Optional[Span]:
|
|
370
|
+
"""
|
|
371
|
+
Return the current active span.
|
|
372
|
+
|
|
373
|
+
:return: The current span if available, else None.
|
|
374
|
+
"""
|
|
375
|
+
return self._context[-1] if self._context else None
|
|
376
|
+
|
|
377
|
+
def get_trace_url(self) -> str:
|
|
378
|
+
"""
|
|
379
|
+
Return the URL to the tracing data.
|
|
380
|
+
:return: The URL to the tracing data.
|
|
381
|
+
"""
|
|
382
|
+
return self._tracer.get_trace_url()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import time
|
|
3
3
|
from urllib.parse import urlparse
|
|
4
|
+
from typing import Optional
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
import requests
|
|
@@ -15,6 +16,8 @@ import httpx
|
|
|
15
16
|
from haystack_integrations.components.connectors.langfuse import LangfuseConnector
|
|
16
17
|
from haystack_integrations.components.generators.anthropic import AnthropicChatGenerator
|
|
17
18
|
from haystack_integrations.components.generators.cohere import CohereChatGenerator
|
|
19
|
+
from haystack_integrations.tracing.langfuse import LangfuseSpan, DefaultSpanHandler
|
|
20
|
+
from haystack_integrations.tracing.langfuse.tracer import _COMPONENT_OUTPUT_KEY
|
|
18
21
|
|
|
19
22
|
# don't remove (or move) this env var setting from here, it's needed to turn tracing on
|
|
20
23
|
os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
|
|
@@ -169,3 +172,86 @@ def test_pipeline_serialization(monkeypatch):
|
|
|
169
172
|
|
|
170
173
|
# Verify pipeline is the same
|
|
171
174
|
assert new_pipe == pipe
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class QualityCheckSpanHandler(DefaultSpanHandler):
|
|
178
|
+
"""Extends default handler to add quality checks with warning levels."""
|
|
179
|
+
|
|
180
|
+
def handle(self, span: LangfuseSpan, component_type: Optional[str]) -> None:
|
|
181
|
+
# First do the default handling (model, usage, etc.)
|
|
182
|
+
super().handle(span, component_type)
|
|
183
|
+
|
|
184
|
+
# Then add our custom quality checks
|
|
185
|
+
if component_type == "OpenAIChatGenerator":
|
|
186
|
+
output = span._data.get(_COMPONENT_OUTPUT_KEY, {})
|
|
187
|
+
replies = output.get("replies", [])
|
|
188
|
+
|
|
189
|
+
if not replies:
|
|
190
|
+
span._span.update(level="ERROR", status_message="No response received")
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
reply = replies[0]
|
|
194
|
+
if "error" in reply.meta:
|
|
195
|
+
span._span.update(level="ERROR", status_message=f"OpenAI error: {reply.meta['error']}")
|
|
196
|
+
elif len(reply.text) > 10:
|
|
197
|
+
span._span.update(level="WARNING", status_message="Response too long (> 10 chars)")
|
|
198
|
+
else:
|
|
199
|
+
span._span.update(level="DEFAULT", status_message="Success")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@pytest.mark.integration
|
|
203
|
+
def test_custom_span_handler():
|
|
204
|
+
"""Test that custom span handler properly sets Langfuse levels."""
|
|
205
|
+
if not all(
|
|
206
|
+
[os.environ.get("LANGFUSE_SECRET_KEY"), os.environ.get("LANGFUSE_PUBLIC_KEY"), os.environ.get("OPENAI_API_KEY")]
|
|
207
|
+
):
|
|
208
|
+
pytest.skip("Missing required environment variables")
|
|
209
|
+
|
|
210
|
+
pipe = Pipeline()
|
|
211
|
+
pipe.add_component(
|
|
212
|
+
"tracer",
|
|
213
|
+
LangfuseConnector(
|
|
214
|
+
name="Quality Check Example",
|
|
215
|
+
public=True,
|
|
216
|
+
span_handler=QualityCheckSpanHandler(),
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
pipe.add_component("prompt_builder", ChatPromptBuilder())
|
|
220
|
+
pipe.add_component("llm", OpenAIChatGenerator())
|
|
221
|
+
pipe.connect("prompt_builder.prompt", "llm.messages")
|
|
222
|
+
|
|
223
|
+
# Test short response
|
|
224
|
+
messages = [
|
|
225
|
+
ChatMessage.from_system("Respond with exactly 3 words."),
|
|
226
|
+
ChatMessage.from_user("What is Berlin?"),
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
response = pipe.run(
|
|
230
|
+
data={
|
|
231
|
+
"prompt_builder": {"template_variables": {}, "template": messages},
|
|
232
|
+
"tracer": {"invocation_context": {"user_id": "test_user"}},
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
trace_url = response["tracer"]["trace_url"]
|
|
237
|
+
uuid = os.path.basename(urlparse(trace_url).path)
|
|
238
|
+
url = f"https://cloud.langfuse.com/api/public/traces/{uuid}"
|
|
239
|
+
|
|
240
|
+
# Poll the Langfuse API
|
|
241
|
+
attempts = 5
|
|
242
|
+
delay = 1
|
|
243
|
+
while attempts >= 0:
|
|
244
|
+
res = requests.get(
|
|
245
|
+
url, auth=HTTPBasicAuth(os.environ["LANGFUSE_PUBLIC_KEY"], os.environ["LANGFUSE_SECRET_KEY"])
|
|
246
|
+
)
|
|
247
|
+
if attempts > 0 and res.status_code != 200:
|
|
248
|
+
attempts -= 1
|
|
249
|
+
time.sleep(delay)
|
|
250
|
+
delay *= 2
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
assert res.status_code == 200
|
|
254
|
+
content = str(res.content)
|
|
255
|
+
assert "WARNING" in content
|
|
256
|
+
assert "Response too long" in content
|
|
257
|
+
break
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import contextlib
|
|
2
|
-
import os
|
|
3
|
-
from contextvars import ContextVar
|
|
4
|
-
from datetime import datetime
|
|
5
|
-
from typing import Any, Dict, Iterator, List, Optional, Union
|
|
6
|
-
|
|
7
|
-
from haystack import logging
|
|
8
|
-
from haystack.dataclasses import ChatMessage
|
|
9
|
-
from haystack.tracing import Span, Tracer
|
|
10
|
-
from haystack.tracing import tracer as proxy_tracer
|
|
11
|
-
from haystack.tracing import utils as tracing_utils
|
|
12
|
-
|
|
13
|
-
import langfuse
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
|
-
|
|
17
|
-
HAYSTACK_LANGFUSE_ENFORCE_FLUSH_ENV_VAR = "HAYSTACK_LANGFUSE_ENFORCE_FLUSH"
|
|
18
|
-
_SUPPORTED_GENERATORS = [
|
|
19
|
-
"AzureOpenAIGenerator",
|
|
20
|
-
"OpenAIGenerator",
|
|
21
|
-
"AnthropicGenerator",
|
|
22
|
-
"HuggingFaceAPIGenerator",
|
|
23
|
-
"HuggingFaceLocalGenerator",
|
|
24
|
-
"CohereGenerator",
|
|
25
|
-
]
|
|
26
|
-
_SUPPORTED_CHAT_GENERATORS = [
|
|
27
|
-
"AzureOpenAIChatGenerator",
|
|
28
|
-
"OpenAIChatGenerator",
|
|
29
|
-
"AnthropicChatGenerator",
|
|
30
|
-
"HuggingFaceAPIChatGenerator",
|
|
31
|
-
"HuggingFaceLocalChatGenerator",
|
|
32
|
-
"CohereChatGenerator",
|
|
33
|
-
]
|
|
34
|
-
_ALL_SUPPORTED_GENERATORS = _SUPPORTED_GENERATORS + _SUPPORTED_CHAT_GENERATORS
|
|
35
|
-
|
|
36
|
-
# These are the keys used by Haystack for traces and span.
|
|
37
|
-
# We keep them here to avoid making typos when using them.
|
|
38
|
-
_PIPELINE_RUN_KEY = "haystack.pipeline.run"
|
|
39
|
-
_COMPONENT_NAME_KEY = "haystack.component.name"
|
|
40
|
-
_COMPONENT_TYPE_KEY = "haystack.component.type"
|
|
41
|
-
_COMPONENT_OUTPUT_KEY = "haystack.component.output"
|
|
42
|
-
|
|
43
|
-
# Context var used to keep track of tracing related info.
|
|
44
|
-
# This mainly useful for parents spans.
|
|
45
|
-
tracing_context_var: ContextVar[Dict[Any, Any]] = ContextVar("tracing_context")
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class LangfuseSpan(Span):
|
|
49
|
-
"""
|
|
50
|
-
Internal class representing a bridge between the Haystack span tracing API and Langfuse.
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
def __init__(self, span: "Union[langfuse.client.StatefulSpanClient, langfuse.client.StatefulTraceClient]") -> None:
|
|
54
|
-
"""
|
|
55
|
-
Initialize a LangfuseSpan instance.
|
|
56
|
-
|
|
57
|
-
:param span: The span instance managed by Langfuse.
|
|
58
|
-
"""
|
|
59
|
-
self._span = span
|
|
60
|
-
# locally cache tags
|
|
61
|
-
self._data: Dict[str, Any] = {}
|
|
62
|
-
|
|
63
|
-
def set_tag(self, key: str, value: Any) -> None:
|
|
64
|
-
"""
|
|
65
|
-
Set a generic tag for this span.
|
|
66
|
-
|
|
67
|
-
:param key: The tag key.
|
|
68
|
-
:param value: The tag value.
|
|
69
|
-
"""
|
|
70
|
-
coerced_value = tracing_utils.coerce_tag_value(value)
|
|
71
|
-
self._span.update(metadata={key: coerced_value})
|
|
72
|
-
self._data[key] = value
|
|
73
|
-
|
|
74
|
-
def set_content_tag(self, key: str, value: Any) -> None:
|
|
75
|
-
"""
|
|
76
|
-
Set a content-specific tag for this span.
|
|
77
|
-
|
|
78
|
-
:param key: The content tag key.
|
|
79
|
-
:param value: The content tag value.
|
|
80
|
-
"""
|
|
81
|
-
if not proxy_tracer.is_content_tracing_enabled:
|
|
82
|
-
return
|
|
83
|
-
if key.endswith(".input"):
|
|
84
|
-
if "messages" in value:
|
|
85
|
-
messages = [m.to_openai_dict_format() for m in value["messages"]]
|
|
86
|
-
self._span.update(input=messages)
|
|
87
|
-
else:
|
|
88
|
-
self._span.update(input=value)
|
|
89
|
-
elif key.endswith(".output"):
|
|
90
|
-
if "replies" in value:
|
|
91
|
-
if all(isinstance(r, ChatMessage) for r in value["replies"]):
|
|
92
|
-
replies = [m.to_openai_dict_format() for m in value["replies"]]
|
|
93
|
-
else:
|
|
94
|
-
replies = value["replies"]
|
|
95
|
-
self._span.update(output=replies)
|
|
96
|
-
else:
|
|
97
|
-
self._span.update(output=value)
|
|
98
|
-
|
|
99
|
-
self._data[key] = value
|
|
100
|
-
|
|
101
|
-
def raw_span(self) -> "Union[langfuse.client.StatefulSpanClient, langfuse.client.StatefulTraceClient]":
|
|
102
|
-
"""
|
|
103
|
-
Return the underlying span instance.
|
|
104
|
-
|
|
105
|
-
:return: The Langfuse span instance.
|
|
106
|
-
"""
|
|
107
|
-
return self._span
|
|
108
|
-
|
|
109
|
-
def get_correlation_data_for_logs(self) -> Dict[str, Any]:
|
|
110
|
-
return {}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
class LangfuseTracer(Tracer):
|
|
114
|
-
"""
|
|
115
|
-
Internal class representing a bridge between the Haystack tracer and Langfuse.
|
|
116
|
-
"""
|
|
117
|
-
|
|
118
|
-
def __init__(self, tracer: "langfuse.Langfuse", name: str = "Haystack", public: bool = False) -> None:
|
|
119
|
-
"""
|
|
120
|
-
Initialize a LangfuseTracer instance.
|
|
121
|
-
|
|
122
|
-
:param tracer: The Langfuse tracer instance.
|
|
123
|
-
:param name: The name of the pipeline or component. This name will be used to identify the tracing run on the
|
|
124
|
-
Langfuse dashboard.
|
|
125
|
-
:param public: Whether the tracing data should be public or private. If set to `True`, the tracing data will
|
|
126
|
-
be publicly accessible to anyone with the tracing URL. If set to `False`, the tracing data will be private
|
|
127
|
-
and only accessible to the Langfuse account owner.
|
|
128
|
-
"""
|
|
129
|
-
if not proxy_tracer.is_content_tracing_enabled:
|
|
130
|
-
logger.warning(
|
|
131
|
-
"Traces will not be logged to Langfuse because Haystack tracing is disabled. "
|
|
132
|
-
"To enable, set the HAYSTACK_CONTENT_TRACING_ENABLED environment variable to true "
|
|
133
|
-
"before importing Haystack."
|
|
134
|
-
)
|
|
135
|
-
self._tracer = tracer
|
|
136
|
-
self._context: List[LangfuseSpan] = []
|
|
137
|
-
self._name = name
|
|
138
|
-
self._public = public
|
|
139
|
-
self.enforce_flush = os.getenv(HAYSTACK_LANGFUSE_ENFORCE_FLUSH_ENV_VAR, "true").lower() == "true"
|
|
140
|
-
|
|
141
|
-
@contextlib.contextmanager
|
|
142
|
-
def trace(
|
|
143
|
-
self, operation_name: str, tags: Optional[Dict[str, Any]] = None, parent_span: Optional[Span] = None
|
|
144
|
-
) -> Iterator[Span]:
|
|
145
|
-
tags = tags or {}
|
|
146
|
-
span_name = tags.get(_COMPONENT_NAME_KEY, operation_name)
|
|
147
|
-
|
|
148
|
-
# Create new span depending whether there's a parent span or not
|
|
149
|
-
if not parent_span:
|
|
150
|
-
if operation_name != _PIPELINE_RUN_KEY:
|
|
151
|
-
logger.warning(
|
|
152
|
-
"Creating a new trace without a parent span is not recommended for operation '{operation_name}'.",
|
|
153
|
-
operation_name=operation_name,
|
|
154
|
-
)
|
|
155
|
-
# Create a new trace if no parent span is provided
|
|
156
|
-
context = tracing_context_var.get({})
|
|
157
|
-
span = LangfuseSpan(
|
|
158
|
-
self._tracer.trace(
|
|
159
|
-
name=self._name,
|
|
160
|
-
public=self._public,
|
|
161
|
-
id=context.get("trace_id"),
|
|
162
|
-
user_id=context.get("user_id"),
|
|
163
|
-
session_id=context.get("session_id"),
|
|
164
|
-
tags=context.get("tags"),
|
|
165
|
-
version=context.get("version"),
|
|
166
|
-
)
|
|
167
|
-
)
|
|
168
|
-
elif tags.get(_COMPONENT_TYPE_KEY) in _ALL_SUPPORTED_GENERATORS:
|
|
169
|
-
span = LangfuseSpan(parent_span.raw_span().generation(name=span_name))
|
|
170
|
-
else:
|
|
171
|
-
span = LangfuseSpan(parent_span.raw_span().span(name=span_name))
|
|
172
|
-
|
|
173
|
-
self._context.append(span)
|
|
174
|
-
span.set_tags(tags)
|
|
175
|
-
|
|
176
|
-
yield span
|
|
177
|
-
|
|
178
|
-
# Update span metadata based on component type
|
|
179
|
-
if tags.get(_COMPONENT_TYPE_KEY) in _SUPPORTED_GENERATORS:
|
|
180
|
-
# Haystack returns one meta dict for each message, but the 'usage' value
|
|
181
|
-
# is always the same, let's just pick the first item
|
|
182
|
-
meta = span._data.get(_COMPONENT_OUTPUT_KEY, {}).get("meta")
|
|
183
|
-
if meta:
|
|
184
|
-
m = meta[0]
|
|
185
|
-
span._span.update(usage=m.get("usage") or None, model=m.get("model"))
|
|
186
|
-
elif tags.get(_COMPONENT_TYPE_KEY) in _SUPPORTED_CHAT_GENERATORS:
|
|
187
|
-
replies = span._data.get(_COMPONENT_OUTPUT_KEY, {}).get("replies")
|
|
188
|
-
if replies:
|
|
189
|
-
meta = replies[0].meta
|
|
190
|
-
completion_start_time = meta.get("completion_start_time")
|
|
191
|
-
if completion_start_time:
|
|
192
|
-
try:
|
|
193
|
-
completion_start_time = datetime.fromisoformat(completion_start_time)
|
|
194
|
-
except ValueError:
|
|
195
|
-
logger.error(f"Failed to parse completion_start_time: {completion_start_time}")
|
|
196
|
-
completion_start_time = None
|
|
197
|
-
span._span.update(
|
|
198
|
-
usage=meta.get("usage") or None,
|
|
199
|
-
model=meta.get("model"),
|
|
200
|
-
completion_start_time=completion_start_time,
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
raw_span = span.raw_span()
|
|
204
|
-
|
|
205
|
-
# In this section, we finalize both regular spans and generation spans created using the LangfuseSpan class.
|
|
206
|
-
# It's important to end() these spans to ensure they are properly closed and all relevant data is recorded.
|
|
207
|
-
# Note that we do not call end() on the main trace span itself, as its lifecycle is managed differently.
|
|
208
|
-
if isinstance(raw_span, (langfuse.client.StatefulSpanClient, langfuse.client.StatefulGenerationClient)):
|
|
209
|
-
raw_span.end()
|
|
210
|
-
self._context.pop()
|
|
211
|
-
|
|
212
|
-
if self.enforce_flush:
|
|
213
|
-
self.flush()
|
|
214
|
-
|
|
215
|
-
def flush(self):
|
|
216
|
-
self._tracer.flush()
|
|
217
|
-
|
|
218
|
-
def current_span(self) -> Optional[Span]:
|
|
219
|
-
"""
|
|
220
|
-
Return the current active span.
|
|
221
|
-
|
|
222
|
-
:return: The current span if available, else None.
|
|
223
|
-
"""
|
|
224
|
-
return self._context[-1] if self._context else None
|
|
225
|
-
|
|
226
|
-
def get_trace_url(self) -> str:
|
|
227
|
-
"""
|
|
228
|
-
Return the URL to the tracing data.
|
|
229
|
-
:return: The URL to the tracing data.
|
|
230
|
-
"""
|
|
231
|
-
return self._tracer.get_trace_url()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|