langfuse-haystack 0.6.4__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.6.4 → langfuse_haystack-0.8.0}/CHANGELOG.md +18 -0
- {langfuse_haystack-0.6.4 → langfuse_haystack-0.8.0}/PKG-INFO +1 -1
- {langfuse_haystack-0.6.4 → langfuse_haystack-0.8.0}/pyproject.toml +7 -1
- {langfuse_haystack-0.6.4 → langfuse_haystack-0.8.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +41 -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.6.4 → langfuse_haystack-0.8.0}/tests/test_tracing.py +111 -1
- langfuse_haystack-0.6.4/src/haystack_integrations/tracing/langfuse/__init__.py +0 -6
- langfuse_haystack-0.6.4/src/haystack_integrations/tracing/langfuse/tracer.py +0 -227
- {langfuse_haystack-0.6.4 → langfuse_haystack-0.8.0}/.gitignore +0 -0
- {langfuse_haystack-0.6.4 → langfuse_haystack-0.8.0}/LICENSE.txt +0 -0
- {langfuse_haystack-0.6.4 → langfuse_haystack-0.8.0}/README.md +0 -0
- {langfuse_haystack-0.6.4 → langfuse_haystack-0.8.0}/example/basic_rag.py +0 -0
- {langfuse_haystack-0.6.4 → langfuse_haystack-0.8.0}/example/chat.py +0 -0
- {langfuse_haystack-0.6.4 → langfuse_haystack-0.8.0}/example/requirements.txt +0 -0
- {langfuse_haystack-0.6.4 → langfuse_haystack-0.8.0}/pydoc/config.yml +0 -0
- {langfuse_haystack-0.6.4 → langfuse_haystack-0.8.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
- {langfuse_haystack-0.6.4 → langfuse_haystack-0.8.0}/tests/__init__.py +0 -0
- {langfuse_haystack-0.6.4 → langfuse_haystack-0.8.0}/tests/test_langfuse_span.py +0 -0
- {langfuse_haystack-0.6.4 → langfuse_haystack-0.8.0}/tests/test_tracer.py +0 -0
|
@@ -1,5 +1,23 @@
|
|
|
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
|
+
|
|
14
|
+
## [integrations/langfuse-v0.6.4] - 2025-01-17
|
|
15
|
+
|
|
16
|
+
### 🚀 Features
|
|
17
|
+
|
|
18
|
+
- Add LangfuseConnector secure key management and serialization (#1287)
|
|
19
|
+
|
|
20
|
+
|
|
3
21
|
## [integrations/langfuse-v0.6.3] - 2025-01-15
|
|
4
22
|
|
|
5
23
|
### 🌀 Miscellaneous
|
|
@@ -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
|
|
@@ -67,7 +67,12 @@ python = ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
|
|
67
67
|
[tool.hatch.envs.lint]
|
|
68
68
|
installer = "uv"
|
|
69
69
|
detached = true
|
|
70
|
-
dependencies = [
|
|
70
|
+
dependencies = [
|
|
71
|
+
"pip",
|
|
72
|
+
"black>=23.1.0",
|
|
73
|
+
"mypy>=1.0.0",
|
|
74
|
+
"ruff>=0.0.243",
|
|
75
|
+
]
|
|
71
76
|
|
|
72
77
|
[tool.hatch.envs.lint.scripts]
|
|
73
78
|
typing = "mypy --install-types --non-interactive --explicit-package-bases {args:src/ tests}"
|
|
@@ -167,6 +172,7 @@ module = [
|
|
|
167
172
|
"haystack_integrations.*",
|
|
168
173
|
"pytest.*",
|
|
169
174
|
"numpy.*",
|
|
175
|
+
"httpx.*",
|
|
170
176
|
]
|
|
171
177
|
ignore_missing_imports = true
|
|
172
178
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from typing import Any, Dict, Optional
|
|
2
2
|
|
|
3
|
+
import httpx
|
|
3
4
|
from haystack import component, default_from_dict, default_to_dict, logging, tracing
|
|
4
5
|
from haystack.utils import Secret, deserialize_secrets_inplace
|
|
6
|
+
from haystack.utils.base_serialization import deserialize_class_instance, serialize_class_instance
|
|
5
7
|
|
|
6
|
-
from haystack_integrations.tracing.langfuse import LangfuseTracer
|
|
8
|
+
from haystack_integrations.tracing.langfuse import LangfuseTracer, SpanHandler
|
|
7
9
|
from langfuse import Langfuse
|
|
8
10
|
|
|
9
11
|
logger = logging.getLogger(__name__)
|
|
@@ -93,6 +95,24 @@ class LangfuseConnector:
|
|
|
93
95
|
print(response["tracer"]["trace_url"])
|
|
94
96
|
```
|
|
95
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
|
+
|
|
96
116
|
"""
|
|
97
117
|
|
|
98
118
|
def __init__(
|
|
@@ -101,6 +121,8 @@ class LangfuseConnector:
|
|
|
101
121
|
public: bool = False,
|
|
102
122
|
public_key: Optional[Secret] = Secret.from_env_var("LANGFUSE_PUBLIC_KEY"), # noqa: B008
|
|
103
123
|
secret_key: Optional[Secret] = Secret.from_env_var("LANGFUSE_SECRET_KEY"), # noqa: B008
|
|
124
|
+
httpx_client: Optional[httpx.Client] = None,
|
|
125
|
+
span_handler: Optional[SpanHandler] = None,
|
|
104
126
|
):
|
|
105
127
|
"""
|
|
106
128
|
Initialize the LangfuseConnector component.
|
|
@@ -112,18 +134,28 @@ class LangfuseConnector:
|
|
|
112
134
|
only accessible to the Langfuse account owner. The default is `False`.
|
|
113
135
|
:param public_key: The Langfuse public key. Defaults to reading from LANGFUSE_PUBLIC_KEY environment variable.
|
|
114
136
|
:param secret_key: The Langfuse secret key. Defaults to reading from LANGFUSE_SECRET_KEY environment variable.
|
|
137
|
+
:param httpx_client: Optional custom httpx.Client instance to use for Langfuse API calls. Note that when
|
|
138
|
+
deserializing a pipeline from YAML, any custom client is discarded and Langfuse will create its own default
|
|
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.
|
|
115
144
|
"""
|
|
116
145
|
self.name = name
|
|
117
146
|
self.public = public
|
|
118
147
|
self.secret_key = secret_key
|
|
119
148
|
self.public_key = public_key
|
|
149
|
+
self.span_handler = span_handler
|
|
120
150
|
self.tracer = LangfuseTracer(
|
|
121
151
|
tracer=Langfuse(
|
|
122
152
|
secret_key=secret_key.resolve_value() if secret_key else None,
|
|
123
153
|
public_key=public_key.resolve_value() if public_key else None,
|
|
154
|
+
httpx_client=httpx_client,
|
|
124
155
|
),
|
|
125
156
|
name=name,
|
|
126
157
|
public=public,
|
|
158
|
+
span_handler=span_handler,
|
|
127
159
|
)
|
|
128
160
|
tracing.enable_tracing(self.tracer)
|
|
129
161
|
|
|
@@ -152,12 +184,15 @@ class LangfuseConnector:
|
|
|
152
184
|
|
|
153
185
|
:returns: The serialized component as a dictionary.
|
|
154
186
|
"""
|
|
187
|
+
span_handler = serialize_class_instance(self.span_handler) if self.span_handler else None
|
|
155
188
|
return default_to_dict(
|
|
156
189
|
self,
|
|
157
190
|
name=self.name,
|
|
158
191
|
public=self.public,
|
|
159
192
|
secret_key=self.secret_key.to_dict() if self.secret_key else None,
|
|
160
193
|
public_key=self.public_key.to_dict() if self.public_key else None,
|
|
194
|
+
# Note: httpx_client is not serialized as it's not serializable
|
|
195
|
+
span_handler=span_handler,
|
|
161
196
|
)
|
|
162
197
|
|
|
163
198
|
@classmethod
|
|
@@ -168,5 +203,9 @@ class LangfuseConnector:
|
|
|
168
203
|
:param data: The dictionary representation of this component.
|
|
169
204
|
:returns: The deserialized component instance.
|
|
170
205
|
"""
|
|
171
|
-
|
|
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
|
+
)
|
|
172
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
|
|
@@ -10,10 +11,13 @@ from haystack.components.generators.chat import OpenAIChatGenerator
|
|
|
10
11
|
from haystack.dataclasses import ChatMessage
|
|
11
12
|
from haystack.utils import Secret
|
|
12
13
|
from requests.auth import HTTPBasicAuth
|
|
14
|
+
import httpx
|
|
13
15
|
|
|
14
16
|
from haystack_integrations.components.connectors.langfuse import LangfuseConnector
|
|
15
17
|
from haystack_integrations.components.generators.anthropic import AnthropicChatGenerator
|
|
16
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
|
|
17
21
|
|
|
18
22
|
# don't remove (or move) this env var setting from here, it's needed to turn tracing on
|
|
19
23
|
os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
|
|
@@ -49,6 +53,27 @@ def pipeline_with_secrets(llm_class, expected_trace):
|
|
|
49
53
|
return pipe
|
|
50
54
|
|
|
51
55
|
|
|
56
|
+
@pytest.fixture
|
|
57
|
+
def pipeline_with_custom_client(llm_class, expected_trace):
|
|
58
|
+
"""Pipeline factory using custom httpx client for Langfuse"""
|
|
59
|
+
pipe = Pipeline()
|
|
60
|
+
custom_client = httpx.Client(timeout=30.0) # Custom timeout of 30 seconds
|
|
61
|
+
pipe.add_component(
|
|
62
|
+
"tracer",
|
|
63
|
+
LangfuseConnector(
|
|
64
|
+
name=f"Chat example - {expected_trace}",
|
|
65
|
+
public=True,
|
|
66
|
+
secret_key=Secret.from_env_var("LANGFUSE_SECRET_KEY"),
|
|
67
|
+
public_key=Secret.from_env_var("LANGFUSE_PUBLIC_KEY"),
|
|
68
|
+
httpx_client=custom_client,
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
pipe.add_component("prompt_builder", ChatPromptBuilder())
|
|
72
|
+
pipe.add_component("llm", llm_class())
|
|
73
|
+
pipe.connect("prompt_builder.prompt", "llm.messages")
|
|
74
|
+
return pipe
|
|
75
|
+
|
|
76
|
+
|
|
52
77
|
@pytest.mark.integration
|
|
53
78
|
@pytest.mark.parametrize(
|
|
54
79
|
"llm_class, env_var, expected_trace",
|
|
@@ -58,7 +83,9 @@ def pipeline_with_secrets(llm_class, expected_trace):
|
|
|
58
83
|
(CohereChatGenerator, "COHERE_API_KEY", "Cohere"),
|
|
59
84
|
],
|
|
60
85
|
)
|
|
61
|
-
@pytest.mark.parametrize(
|
|
86
|
+
@pytest.mark.parametrize(
|
|
87
|
+
"pipeline_fixture", ["pipeline_with_env_vars", "pipeline_with_secrets", "pipeline_with_custom_client"]
|
|
88
|
+
)
|
|
62
89
|
def test_tracing_integration(llm_class, env_var, expected_trace, pipeline_fixture, request):
|
|
63
90
|
if not all([os.environ.get("LANGFUSE_SECRET_KEY"), os.environ.get("LANGFUSE_PUBLIC_KEY"), os.environ.get(env_var)]):
|
|
64
91
|
pytest.skip(f"Missing required environment variables: LANGFUSE_SECRET_KEY, LANGFUSE_PUBLIC_KEY, or {env_var}")
|
|
@@ -145,3 +172,86 @@ def test_pipeline_serialization(monkeypatch):
|
|
|
145
172
|
|
|
146
173
|
# Verify pipeline is the same
|
|
147
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,227 +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
|
-
if isinstance(raw_span, langfuse.client.StatefulSpanClient):
|
|
205
|
-
raw_span.end()
|
|
206
|
-
self._context.pop()
|
|
207
|
-
|
|
208
|
-
if self.enforce_flush:
|
|
209
|
-
self.flush()
|
|
210
|
-
|
|
211
|
-
def flush(self):
|
|
212
|
-
self._tracer.flush()
|
|
213
|
-
|
|
214
|
-
def current_span(self) -> Optional[Span]:
|
|
215
|
-
"""
|
|
216
|
-
Return the current active span.
|
|
217
|
-
|
|
218
|
-
:return: The current span if available, else None.
|
|
219
|
-
"""
|
|
220
|
-
return self._context[-1] if self._context else None
|
|
221
|
-
|
|
222
|
-
def get_trace_url(self) -> str:
|
|
223
|
-
"""
|
|
224
|
-
Return the URL to the tracing data.
|
|
225
|
-
:return: The URL to the tracing data.
|
|
226
|
-
"""
|
|
227
|
-
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
|