langfuse-haystack 0.5.0__tar.gz → 0.6.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.5.0 → langfuse_haystack-0.6.0}/CHANGELOG.md +10 -0
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/PKG-INFO +2 -3
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/pyproject.toml +3 -1
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/src/haystack_integrations/tracing/langfuse/tracer.py +75 -44
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/tests/test_tracer.py +70 -33
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/tests/test_tracing.py +17 -14
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/.gitignore +0 -0
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/LICENSE.txt +0 -0
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/README.md +0 -0
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/example/basic_rag.py +0 -0
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/example/chat.py +0 -0
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/example/requirements.txt +0 -0
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/pydoc/config.yml +0 -0
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +0 -0
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/tests/__init__.py +0 -0
- {langfuse_haystack-0.5.0 → langfuse_haystack-0.6.0}/tests/test_langfuse_span.py +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [integrations/langfuse-v0.5.0] - 2024-10-01
|
|
4
|
+
|
|
5
|
+
### ⚙️ Miscellaneous Tasks
|
|
6
|
+
|
|
7
|
+
- Update ruff linting scripts and settings (#1105)
|
|
8
|
+
|
|
9
|
+
### Langfuse
|
|
10
|
+
|
|
11
|
+
- Add invocation_context to identify traces (#1089)
|
|
12
|
+
|
|
3
13
|
## [integrations/langfuse-v0.4.0] - 2024-09-17
|
|
4
14
|
|
|
5
15
|
### 🚀 Features
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: langfuse-haystack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
7
7
|
Author-email: deepset GmbH <info@deepset.ai>
|
|
8
|
-
License
|
|
9
|
-
License-File: LICENSE.txt
|
|
8
|
+
License: Apache-2.0
|
|
10
9
|
Classifier: Development Status :: 4 - Beta
|
|
11
10
|
Classifier: Programming Language :: Python
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.8
|
|
@@ -42,6 +42,7 @@ root = "../.."
|
|
|
42
42
|
git_describe_command = 'git describe --tags --match="integrations/langfuse-v[0-9]*"'
|
|
43
43
|
|
|
44
44
|
[tool.hatch.envs.default]
|
|
45
|
+
installer = "uv"
|
|
45
46
|
dependencies = [
|
|
46
47
|
"coverage[toml]>=6.5",
|
|
47
48
|
"pytest",
|
|
@@ -64,8 +65,9 @@ docs = ["pydoc-markdown pydoc/config.yml"]
|
|
|
64
65
|
python = ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
|
65
66
|
|
|
66
67
|
[tool.hatch.envs.lint]
|
|
68
|
+
installer = "uv"
|
|
67
69
|
detached = true
|
|
68
|
-
dependencies = ["black>=23.1.0", "mypy>=1.0.0", "ruff>=0.0.243"]
|
|
70
|
+
dependencies = ["pip", "black>=23.1.0", "mypy>=1.0.0", "ruff>=0.0.243"]
|
|
69
71
|
|
|
70
72
|
[tool.hatch.envs.lint.scripts]
|
|
71
73
|
typing = "mypy --install-types --non-interactive --explicit-package-bases {args:src/ tests}"
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import os
|
|
3
|
-
from
|
|
3
|
+
from contextvars import ContextVar
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Dict, Iterator, List, Optional, Union
|
|
4
6
|
|
|
7
|
+
from haystack import logging
|
|
5
8
|
from haystack.components.generators.openai_utils import _convert_message_to_openai_format
|
|
6
9
|
from haystack.dataclasses import ChatMessage
|
|
7
10
|
from haystack.tracing import Span, Tracer, tracer
|
|
@@ -9,6 +12,8 @@ from haystack.tracing import utils as tracing_utils
|
|
|
9
12
|
|
|
10
13
|
import langfuse
|
|
11
14
|
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
12
17
|
HAYSTACK_LANGFUSE_ENFORCE_FLUSH_ENV_VAR = "HAYSTACK_LANGFUSE_ENFORCE_FLUSH"
|
|
13
18
|
_SUPPORTED_GENERATORS = [
|
|
14
19
|
"AzureOpenAIGenerator",
|
|
@@ -28,6 +33,17 @@ _SUPPORTED_CHAT_GENERATORS = [
|
|
|
28
33
|
]
|
|
29
34
|
_ALL_SUPPORTED_GENERATORS = _SUPPORTED_GENERATORS + _SUPPORTED_CHAT_GENERATORS
|
|
30
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", default={})
|
|
46
|
+
|
|
31
47
|
|
|
32
48
|
class LangfuseSpan(Span):
|
|
33
49
|
"""
|
|
@@ -82,7 +98,7 @@ class LangfuseSpan(Span):
|
|
|
82
98
|
|
|
83
99
|
self._data[key] = value
|
|
84
100
|
|
|
85
|
-
def raw_span(self) ->
|
|
101
|
+
def raw_span(self) -> "Union[langfuse.client.StatefulSpanClient, langfuse.client.StatefulTraceClient]":
|
|
86
102
|
"""
|
|
87
103
|
Return the underlying span instance.
|
|
88
104
|
|
|
@@ -111,75 +127,90 @@ class LangfuseTracer(Tracer):
|
|
|
111
127
|
and only accessible to the Langfuse account owner.
|
|
112
128
|
"""
|
|
113
129
|
self._tracer = tracer
|
|
114
|
-
self._context:
|
|
130
|
+
self._context: List[LangfuseSpan] = []
|
|
115
131
|
self._name = name
|
|
116
132
|
self._public = public
|
|
117
133
|
self.enforce_flush = os.getenv(HAYSTACK_LANGFUSE_ENFORCE_FLUSH_ENV_VAR, "true").lower() == "true"
|
|
118
134
|
|
|
119
135
|
@contextlib.contextmanager
|
|
120
|
-
def trace(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
:param operation_name: The name of the operation.
|
|
124
|
-
:param tags: A dictionary of tags to attach to the span.
|
|
125
|
-
:return: A context manager yielding the span.
|
|
126
|
-
"""
|
|
136
|
+
def trace(
|
|
137
|
+
self, operation_name: str, tags: Optional[Dict[str, Any]] = None, parent_span: Optional[Span] = None
|
|
138
|
+
) -> Iterator[Span]:
|
|
127
139
|
tags = tags or {}
|
|
128
|
-
span_name = tags.get(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
span_name = tags.get(_COMPONENT_NAME_KEY, operation_name)
|
|
141
|
+
|
|
142
|
+
# Create new span depending whether there's a parent span or not
|
|
143
|
+
if not parent_span:
|
|
144
|
+
if operation_name != _PIPELINE_RUN_KEY:
|
|
145
|
+
logger.warning(
|
|
146
|
+
"Creating a new trace without a parent span is not recommended for operation '{operation_name}'.",
|
|
147
|
+
operation_name=operation_name,
|
|
148
|
+
)
|
|
149
|
+
# Create a new trace if no parent span is provided
|
|
150
|
+
span = LangfuseSpan(
|
|
151
|
+
self._tracer.trace(
|
|
152
|
+
name=self._name,
|
|
153
|
+
public=self._public,
|
|
154
|
+
id=tracing_context_var.get().get("trace_id"),
|
|
155
|
+
user_id=tracing_context_var.get().get("user_id"),
|
|
156
|
+
session_id=tracing_context_var.get().get("session_id"),
|
|
157
|
+
tags=tracing_context_var.get().get("tags"),
|
|
158
|
+
version=tracing_context_var.get().get("version"),
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
elif tags.get(_COMPONENT_TYPE_KEY) in _ALL_SUPPORTED_GENERATORS:
|
|
162
|
+
span = LangfuseSpan(parent_span.raw_span().generation(name=span_name))
|
|
132
163
|
else:
|
|
133
|
-
span = LangfuseSpan(
|
|
164
|
+
span = LangfuseSpan(parent_span.raw_span().span(name=span_name))
|
|
134
165
|
|
|
135
166
|
self._context.append(span)
|
|
136
167
|
span.set_tags(tags)
|
|
137
168
|
|
|
138
169
|
yield span
|
|
139
170
|
|
|
140
|
-
|
|
141
|
-
|
|
171
|
+
# Update span metadata based on component type
|
|
172
|
+
if tags.get(_COMPONENT_TYPE_KEY) in _SUPPORTED_GENERATORS:
|
|
173
|
+
# Haystack returns one meta dict for each message, but the 'usage' value
|
|
174
|
+
# is always the same, let's just pick the first item
|
|
175
|
+
meta = span._data.get(_COMPONENT_OUTPUT_KEY, {}).get("meta")
|
|
142
176
|
if meta:
|
|
143
|
-
# Haystack returns one meta dict for each message, but the 'usage' value
|
|
144
|
-
# is always the same, let's just pick the first item
|
|
145
177
|
m = meta[0]
|
|
146
178
|
span._span.update(usage=m.get("usage") or None, model=m.get("model"))
|
|
147
|
-
elif tags.get(
|
|
148
|
-
replies = span._data.get(
|
|
179
|
+
elif tags.get(_COMPONENT_TYPE_KEY) in _SUPPORTED_CHAT_GENERATORS:
|
|
180
|
+
replies = span._data.get(_COMPONENT_OUTPUT_KEY, {}).get("replies")
|
|
149
181
|
if replies:
|
|
150
182
|
meta = replies[0].meta
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
183
|
+
completion_start_time = meta.get("completion_start_time")
|
|
184
|
+
if completion_start_time:
|
|
185
|
+
try:
|
|
186
|
+
completion_start_time = datetime.fromisoformat(completion_start_time)
|
|
187
|
+
except ValueError:
|
|
188
|
+
logger.error(f"Failed to parse completion_start_time: {completion_start_time}")
|
|
189
|
+
completion_start_time = None
|
|
190
|
+
span._span.update(
|
|
191
|
+
usage=meta.get("usage") or None,
|
|
192
|
+
model=meta.get("model"),
|
|
193
|
+
completion_start_time=completion_start_time,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
raw_span = span.raw_span()
|
|
197
|
+
if isinstance(raw_span, langfuse.client.StatefulSpanClient):
|
|
198
|
+
raw_span.end()
|
|
161
199
|
self._context.pop()
|
|
162
200
|
|
|
163
|
-
if
|
|
164
|
-
|
|
165
|
-
self._context.pop()
|
|
166
|
-
|
|
167
|
-
if self.enforce_flush:
|
|
168
|
-
self.flush()
|
|
201
|
+
if self.enforce_flush:
|
|
202
|
+
self.flush()
|
|
169
203
|
|
|
170
204
|
def flush(self):
|
|
171
205
|
self._tracer.flush()
|
|
172
206
|
|
|
173
|
-
def current_span(self) -> Span:
|
|
207
|
+
def current_span(self) -> Optional[Span]:
|
|
174
208
|
"""
|
|
175
|
-
Return the
|
|
209
|
+
Return the current active span.
|
|
176
210
|
|
|
177
|
-
:return: The
|
|
211
|
+
:return: The current span if available, else None.
|
|
178
212
|
"""
|
|
179
|
-
if
|
|
180
|
-
# The root span has to be a trace
|
|
181
|
-
self._context.append(LangfuseSpan(self._tracer.trace(name=self._name, public=self._public)))
|
|
182
|
-
return self._context[-1]
|
|
213
|
+
return self._context[-1] if self._context else None
|
|
183
214
|
|
|
184
215
|
def get_trace_url(self) -> str:
|
|
185
216
|
"""
|
|
@@ -1,9 +1,43 @@
|
|
|
1
|
-
import
|
|
1
|
+
import datetime
|
|
2
2
|
from unittest.mock import MagicMock, Mock, patch
|
|
3
3
|
|
|
4
|
+
from haystack.dataclasses import ChatMessage
|
|
4
5
|
from haystack_integrations.tracing.langfuse.tracer import LangfuseTracer
|
|
5
6
|
|
|
6
7
|
|
|
8
|
+
class MockSpan:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self._data = {}
|
|
11
|
+
self._span = self
|
|
12
|
+
self.operation_name = "operation_name"
|
|
13
|
+
|
|
14
|
+
def raw_span(self):
|
|
15
|
+
return self
|
|
16
|
+
|
|
17
|
+
def span(self, name=None):
|
|
18
|
+
# assert correct operation name passed to the span
|
|
19
|
+
assert name == "operation_name"
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
def update(self, **kwargs):
|
|
23
|
+
self._data.update(kwargs)
|
|
24
|
+
|
|
25
|
+
def generation(self, name=None):
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def end(self):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MockTracer:
|
|
33
|
+
|
|
34
|
+
def trace(self, name, **kwargs):
|
|
35
|
+
return MockSpan()
|
|
36
|
+
|
|
37
|
+
def flush(self):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
7
41
|
class TestLangfuseTracer:
|
|
8
42
|
|
|
9
43
|
# LangfuseTracer can be initialized with a Langfuse instance, a name and a boolean value for public.
|
|
@@ -35,7 +69,7 @@ class TestLangfuseTracer:
|
|
|
35
69
|
tracer = LangfuseTracer(tracer=mock_tracer, name="Haystack", public=False)
|
|
36
70
|
|
|
37
71
|
with tracer.trace("operation_name", tags={"tag1": "value1", "tag2": "value2"}) as span:
|
|
38
|
-
assert len(tracer._context) ==
|
|
72
|
+
assert len(tracer._context) == 1, "The trace span should have been added to the the root context span"
|
|
39
73
|
assert span.raw_span().operation_name == "operation_name"
|
|
40
74
|
assert span.raw_span().metadata == {"tag1": "value1", "tag2": "value2"}
|
|
41
75
|
|
|
@@ -45,37 +79,6 @@ class TestLangfuseTracer:
|
|
|
45
79
|
|
|
46
80
|
# check that update method is called on the span instance with the provided key value pairs
|
|
47
81
|
def test_update_span_with_pipeline_input_output_data(self):
|
|
48
|
-
class MockTracer:
|
|
49
|
-
|
|
50
|
-
def trace(self, name, **kwargs):
|
|
51
|
-
return MockSpan()
|
|
52
|
-
|
|
53
|
-
def flush(self):
|
|
54
|
-
pass
|
|
55
|
-
|
|
56
|
-
class MockSpan:
|
|
57
|
-
def __init__(self):
|
|
58
|
-
self._data = {}
|
|
59
|
-
self._span = self
|
|
60
|
-
self.operation_name = "operation_name"
|
|
61
|
-
|
|
62
|
-
def raw_span(self):
|
|
63
|
-
return self
|
|
64
|
-
|
|
65
|
-
def span(self, name=None):
|
|
66
|
-
# assert correct operation name passed to the span
|
|
67
|
-
assert name == "operation_name"
|
|
68
|
-
return self
|
|
69
|
-
|
|
70
|
-
def update(self, **kwargs):
|
|
71
|
-
self._data.update(kwargs)
|
|
72
|
-
|
|
73
|
-
def generation(self, name=None):
|
|
74
|
-
return self
|
|
75
|
-
|
|
76
|
-
def end(self):
|
|
77
|
-
pass
|
|
78
|
-
|
|
79
82
|
tracer = LangfuseTracer(tracer=MockTracer(), name="Haystack", public=False)
|
|
80
83
|
with tracer.trace(operation_name="operation_name", tags={"haystack.pipeline.input_data": "hello"}) as span:
|
|
81
84
|
assert span.raw_span()._data["metadata"] == {"haystack.pipeline.input_data": "hello"}
|
|
@@ -83,6 +86,40 @@ class TestLangfuseTracer:
|
|
|
83
86
|
with tracer.trace(operation_name="operation_name", tags={"haystack.pipeline.output_data": "bye"}) as span:
|
|
84
87
|
assert span.raw_span()._data["metadata"] == {"haystack.pipeline.output_data": "bye"}
|
|
85
88
|
|
|
89
|
+
def test_trace_generation(self):
|
|
90
|
+
tracer = LangfuseTracer(tracer=MockTracer(), name="Haystack", public=False)
|
|
91
|
+
tags = {
|
|
92
|
+
"haystack.component.type": "OpenAIChatGenerator",
|
|
93
|
+
"haystack.component.output": {
|
|
94
|
+
"replies": [
|
|
95
|
+
ChatMessage.from_assistant(
|
|
96
|
+
"", meta={"completion_start_time": "2021-07-27T16:02:08.012345", "model": "test_model"}
|
|
97
|
+
)
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
with tracer.trace(operation_name="operation_name", tags=tags) as span:
|
|
102
|
+
...
|
|
103
|
+
assert span.raw_span()._data["usage"] is None
|
|
104
|
+
assert span.raw_span()._data["model"] == "test_model"
|
|
105
|
+
assert span.raw_span()._data["completion_start_time"] == datetime.datetime(2021, 7, 27, 16, 2, 8, 12345)
|
|
106
|
+
|
|
107
|
+
def test_trace_generation_invalid_start_time(self):
|
|
108
|
+
tracer = LangfuseTracer(tracer=MockTracer(), name="Haystack", public=False)
|
|
109
|
+
tags = {
|
|
110
|
+
"haystack.component.type": "OpenAIChatGenerator",
|
|
111
|
+
"haystack.component.output": {
|
|
112
|
+
"replies": [
|
|
113
|
+
ChatMessage.from_assistant("", meta={"completion_start_time": "foobar", "model": "test_model"}),
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
with tracer.trace(operation_name="operation_name", tags=tags) as span:
|
|
118
|
+
...
|
|
119
|
+
assert span.raw_span()._data["usage"] is None
|
|
120
|
+
assert span.raw_span()._data["model"] == "test_model"
|
|
121
|
+
assert span.raw_span()._data["completion_start_time"] is None
|
|
122
|
+
|
|
86
123
|
def test_update_span_gets_flushed_by_default(self):
|
|
87
124
|
tracer_mock = Mock()
|
|
88
125
|
|
|
@@ -52,25 +52,28 @@ def test_tracing_integration(llm_class, env_var, expected_trace):
|
|
|
52
52
|
assert "Berlin" in response["llm"]["replies"][0].content
|
|
53
53
|
assert response["tracer"]["trace_url"]
|
|
54
54
|
|
|
55
|
-
# add a random delay between 1 and 3 seconds to make sure the trace is flushed
|
|
56
|
-
# and that the trace is available in Langfuse when we fetch it below
|
|
57
|
-
time.sleep(random.uniform(1, 3))
|
|
58
|
-
|
|
59
|
-
url = "https://cloud.langfuse.com/api/public/traces/"
|
|
60
55
|
trace_url = response["tracer"]["trace_url"]
|
|
61
56
|
uuid = os.path.basename(urlparse(trace_url).path)
|
|
57
|
+
url = f"https://cloud.langfuse.com/api/public/traces/{uuid}"
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
# Poll the Langfuse API a bit as the trace might not be ready right away
|
|
60
|
+
attempts = 5
|
|
61
|
+
delay = 1
|
|
62
|
+
while attempts >= 0:
|
|
63
|
+
res = requests.get(
|
|
64
|
+
url, auth=HTTPBasicAuth(os.environ["LANGFUSE_PUBLIC_KEY"], os.environ["LANGFUSE_SECRET_KEY"])
|
|
66
65
|
)
|
|
67
|
-
|
|
66
|
+
if attempts > 0 and res.status_code != 200:
|
|
67
|
+
attempts -= 1
|
|
68
|
+
time.sleep(delay)
|
|
69
|
+
delay *= 2
|
|
70
|
+
continue
|
|
71
|
+
assert res.status_code == 200, f"Failed to retrieve data from Langfuse API: {res.status_code}"
|
|
68
72
|
|
|
69
73
|
# check if the trace contains the expected LLM name
|
|
70
|
-
assert expected_trace in str(
|
|
74
|
+
assert expected_trace in str(res.content)
|
|
71
75
|
# check if the trace contains the expected generation span
|
|
72
|
-
assert "GENERATION" in str(
|
|
76
|
+
assert "GENERATION" in str(res.content)
|
|
73
77
|
# check if the trace contains the expected user_id
|
|
74
|
-
assert "user_42" in str(
|
|
75
|
-
|
|
76
|
-
pytest.fail(f"Failed to retrieve data from Langfuse API: {e}")
|
|
78
|
+
assert "user_42" in str(res.content)
|
|
79
|
+
break
|
|
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
|