arize-phoenix 3.2.1__py3-none-any.whl → 3.4.0__py3-none-any.whl
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.
Potentially problematic release.
This version of arize-phoenix might be problematic. Click here for more details.
- {arize_phoenix-3.2.1.dist-info → arize_phoenix-3.4.0.dist-info}/METADATA +5 -3
- {arize_phoenix-3.2.1.dist-info → arize_phoenix-3.4.0.dist-info}/RECORD +10 -10
- phoenix/__init__.py +28 -0
- phoenix/core/traces.py +138 -139
- phoenix/server/api/types/Span.py +3 -4
- phoenix/server/static/index.js +130 -126
- phoenix/version.py +1 -1
- {arize_phoenix-3.2.1.dist-info → arize_phoenix-3.4.0.dist-info}/WHEEL +0 -0
- {arize_phoenix-3.2.1.dist-info → arize_phoenix-3.4.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-3.2.1.dist-info → arize_phoenix-3.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: arize-phoenix
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: ML Observability in your notebook
|
|
5
5
|
Project-URL: Documentation, https://docs.arize.com/phoenix/
|
|
6
6
|
Project-URL: Issues, https://github.com/Arize-ai/phoenix/issues
|
|
@@ -61,6 +61,8 @@ Requires-Dist: pytest-lazy-fixture; extra == 'dev'
|
|
|
61
61
|
Requires-Dist: pytest==7.4.4; extra == 'dev'
|
|
62
62
|
Requires-Dist: ruff==0.1.5; extra == 'dev'
|
|
63
63
|
Requires-Dist: strawberry-graphql[debug-server]==0.208.2; extra == 'dev'
|
|
64
|
+
Provides-Extra: evals
|
|
65
|
+
Requires-Dist: arize-phoenix-evals>=0.0.3; extra == 'evals'
|
|
64
66
|
Provides-Extra: experimental
|
|
65
67
|
Requires-Dist: tenacity; extra == 'experimental'
|
|
66
68
|
Provides-Extra: llama-index
|
|
@@ -303,12 +305,12 @@ df = download_benchmark_dataset(
|
|
|
303
305
|
df = df.sample(100)
|
|
304
306
|
df = df.rename(
|
|
305
307
|
columns={
|
|
306
|
-
"query_text": "
|
|
308
|
+
"query_text": "input",
|
|
307
309
|
"document_text": "reference",
|
|
308
310
|
},
|
|
309
311
|
)
|
|
310
312
|
model = OpenAIModel(
|
|
311
|
-
|
|
313
|
+
model="gpt-4",
|
|
312
314
|
temperature=0.0,
|
|
313
315
|
)
|
|
314
316
|
rails =list(RAG_RELEVANCY_PROMPT_RAILS_MAP.values())
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
phoenix/__init__.py,sha256=
|
|
1
|
+
phoenix/__init__.py,sha256=mrgR7rvpc7LnBeyLzCX9wmRP4kJSJdfKDbj2zDiDjG8,2311
|
|
2
2
|
phoenix/config.py,sha256=RT4UR5VH8JUAKQhfmlkpxq2kmBSA8G8MODrC3GXWyaI,3507
|
|
3
3
|
phoenix/datetime_utils.py,sha256=D955QLrkgrrSdUM6NyqbCeAu2SMsjhR5rHVQEsVUdng,2773
|
|
4
4
|
phoenix/exceptions.py,sha256=X5k9ipUDfwSCwZB-H5zFJLas86Gf9tAx0W4l5TZxp5k,108
|
|
5
5
|
phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
6
6
|
phoenix/services.py,sha256=f6AeyKTuOpy9RCcTCjVH3gx5nYZhbTMFOuv1WSUOB5o,4992
|
|
7
|
-
phoenix/version.py,sha256=
|
|
7
|
+
phoenix/version.py,sha256=SaDAYHfdUx0zZwEMJs8CthhSY2X4I0doARTPgX2vOwM,22
|
|
8
8
|
phoenix/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
phoenix/core/embedding_dimension.py,sha256=zKGbcvwOXgLf-yrJBpQyKtd-LEOPRKHnUToyAU8Owis,87
|
|
10
10
|
phoenix/core/evals.py,sha256=gJyqQzpud5YjtoY8h4pgXvHDsdubGfqmEewLuZHPPmQ,10224
|
|
11
11
|
phoenix/core/model.py,sha256=vQ6RxpUPlncezJvur5u6xBN0Lkrk2gW0cTyb-qqaSqA,4713
|
|
12
12
|
phoenix/core/model_schema.py,sha256=rR9VdhL_oXxbprDTPQJBXs5hw5sMPQmzx__m6Kwsxug,50394
|
|
13
13
|
phoenix/core/model_schema_adapter.py,sha256=3GkyzqUST4fYi-Bgs8qAam5hwMCdQRZTDLjZ9Bnzdm4,8268
|
|
14
|
-
phoenix/core/traces.py,sha256=
|
|
14
|
+
phoenix/core/traces.py,sha256=IHaRP-SK0iKuGpq8hpt-0fio3LhqjTmvtfPzo_X2tS8,13798
|
|
15
15
|
phoenix/datasets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
16
|
phoenix/datasets/dataset.py,sha256=scKVZ7zc6Dpc_ntt-pWhzY-KWqOJEwKePuyNnKSVTGE,30515
|
|
17
17
|
phoenix/datasets/errors.py,sha256=cGp9vxnw4SewFoWBV3ZGMkhE0Kh73lPIv3Ppz_H_RoA,8261
|
|
@@ -109,7 +109,7 @@ phoenix/server/api/types/Retrieval.py,sha256=OhMK2ncjoyp5h1yjKhjlKpoTbQrMHuxmgSF
|
|
|
109
109
|
phoenix/server/api/types/ScalarDriftMetricEnum.py,sha256=IUAcRPpgL41WdoIgK6cNk2Te38SspXGyEs-S1fY23_A,232
|
|
110
110
|
phoenix/server/api/types/Segments.py,sha256=zogJI9MdmctBL7J-fDSR_8tUJLvuISlVYgCLnTaigKE,2937
|
|
111
111
|
phoenix/server/api/types/SortDir.py,sha256=OUpXhlCzCxPoXSDkJJygEs9Rw9pMymfaZUG5zPTrw4Y,152
|
|
112
|
-
phoenix/server/api/types/Span.py,sha256=
|
|
112
|
+
phoenix/server/api/types/Span.py,sha256=ao8LusgrdXVLc39jGtrL-9RuaUbPOzI98EsQNb6xpiE,11610
|
|
113
113
|
phoenix/server/api/types/TimeSeries.py,sha256=QbLfxHnwYsMsirpq4tx9us6ha7YtAVzK4m8mAL3fMt0,5200
|
|
114
114
|
phoenix/server/api/types/UMAPPoints.py,sha256=8l9RJXi308qty4MdHb2pBbiU6ZuLbrRRxXNbPhXoxKI,1639
|
|
115
115
|
phoenix/server/api/types/ValidationResult.py,sha256=pHwdYk4J7SJ5xhlWWHg_6qWkfk4rjOx-bSkGHvkDE3Q,142
|
|
@@ -127,7 +127,7 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
|
|
|
127
127
|
phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
|
|
128
128
|
phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
|
|
129
129
|
phoenix/server/static/index.css,sha256=KKGpx4iwF91VGRm0YN-4cn8oC-oIqC6HecoPf0x3ZM8,1885
|
|
130
|
-
phoenix/server/static/index.js,sha256=
|
|
130
|
+
phoenix/server/static/index.js,sha256=73BthyJKaIMt5o5VThZoX7kGJclvLdghL9Dn80wEdUE,3144672
|
|
131
131
|
phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
|
|
132
132
|
phoenix/server/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
133
133
|
phoenix/server/templates/index.html,sha256=DlfcGoq1V5C2QkJWqP1j4Nu6_kPfsOzOrtzYF3ogghE,1900
|
|
@@ -167,8 +167,8 @@ phoenix/trace/v1/evaluation_pb2.pyi,sha256=cCbbx06gwQmaH14s3J1X25TtaARh-k1abbxQd
|
|
|
167
167
|
phoenix/utilities/__init__.py,sha256=3TVirVnjIGyaCFuJCqeZO4tjlzQ_chZgYM0itIwsEpE,656
|
|
168
168
|
phoenix/utilities/error_handling.py,sha256=7b5rpGFj9EWZ8yrZK1IHvxB89suWk3lggDayUQcvZds,1946
|
|
169
169
|
phoenix/utilities/logging.py,sha256=lDXd6EGaamBNcQxL4vP1au9-i_SXe0OraUDiJOcszSw,222
|
|
170
|
-
arize_phoenix-3.
|
|
171
|
-
arize_phoenix-3.
|
|
172
|
-
arize_phoenix-3.
|
|
173
|
-
arize_phoenix-3.
|
|
174
|
-
arize_phoenix-3.
|
|
170
|
+
arize_phoenix-3.4.0.dist-info/METADATA,sha256=GILlx8yqFVDq1YOx3KHlqPMfYKGMUnE7ndwRhIE_nG4,28853
|
|
171
|
+
arize_phoenix-3.4.0.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
|
|
172
|
+
arize_phoenix-3.4.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
|
|
173
|
+
arize_phoenix-3.4.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
|
|
174
|
+
arize_phoenix-3.4.0.dist-info/RECORD,,
|
phoenix/__init__.py
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from importlib.abc import Loader, MetaPathFinder
|
|
3
|
+
from importlib.machinery import ModuleSpec
|
|
4
|
+
from types import ModuleType
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
1
7
|
from .datasets.dataset import Dataset
|
|
2
8
|
from .datasets.fixtures import ExampleDatasets, load_example
|
|
3
9
|
from .datasets.schema import EmbeddingColumnNames, RetrievalEmbeddingColumnNames, Schema
|
|
@@ -41,4 +47,26 @@ __all__ = [
|
|
|
41
47
|
"NotebookEnvironment",
|
|
42
48
|
"log_evaluations",
|
|
43
49
|
"Client",
|
|
50
|
+
"evals",
|
|
44
51
|
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PhoenixEvalsFinder(MetaPathFinder):
|
|
55
|
+
def find_spec(self, fullname: Any, path: Any, target: Any = None) -> Optional[ModuleSpec]:
|
|
56
|
+
if fullname == "phoenix.evals":
|
|
57
|
+
return ModuleSpec(fullname, PhoenixEvalsLoader())
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PhoenixEvalsLoader(Loader):
|
|
62
|
+
def create_module(self, spec: ModuleSpec) -> None:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def exec_module(self, module: ModuleType) -> None:
|
|
66
|
+
raise ImportError(
|
|
67
|
+
"The optional `phoenix.evals` package is not installed. "
|
|
68
|
+
"Please install `phoenix` with the `evals` extra: `pip install 'arize-phoenix[evals]'`."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
sys.meta_path.append(PhoenixEvalsFinder())
|
phoenix/core/traces.py
CHANGED
|
@@ -3,14 +3,13 @@ from collections import defaultdict
|
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
4
|
from queue import SimpleQueue
|
|
5
5
|
from threading import RLock, Thread
|
|
6
|
-
from types import MethodType
|
|
6
|
+
from types import MappingProxyType, MethodType
|
|
7
7
|
from typing import (
|
|
8
8
|
Any,
|
|
9
9
|
DefaultDict,
|
|
10
10
|
Dict,
|
|
11
11
|
Iterable,
|
|
12
12
|
Iterator,
|
|
13
|
-
List,
|
|
14
13
|
Optional,
|
|
15
14
|
Set,
|
|
16
15
|
SupportsFloat,
|
|
@@ -72,6 +71,10 @@ class ReadableSpan(ObjectProxy): # type: ignore
|
|
|
72
71
|
@property
|
|
73
72
|
def span(self) -> Span:
|
|
74
73
|
span = decode(self._self_otlp_span)
|
|
74
|
+
# FIXME: Our legacy files have the __computed__ attributes which interferes
|
|
75
|
+
# with our ability to add more computations. As a workaround, we discard the computed
|
|
76
|
+
# attribute if it exists.
|
|
77
|
+
span.attributes.pop(COMPUTED_PREFIX[:-1], None)
|
|
75
78
|
span.attributes.update(
|
|
76
79
|
cast(phoenix.trace.schemas.SpanAttributes, self._self_computed_values)
|
|
77
80
|
)
|
|
@@ -95,6 +98,12 @@ class ReadableSpan(ObjectProxy): # type: ignore
|
|
|
95
98
|
raise KeyError(f"{key} is not a computed value")
|
|
96
99
|
self._self_computed_values[key] = value
|
|
97
100
|
|
|
101
|
+
def __eq__(self, other: Any) -> bool:
|
|
102
|
+
return self is other
|
|
103
|
+
|
|
104
|
+
def __hash__(self) -> int:
|
|
105
|
+
return id(self)
|
|
106
|
+
|
|
98
107
|
|
|
99
108
|
ParentSpanID: TypeAlias = SpanID
|
|
100
109
|
ChildSpanID: TypeAlias = SpanID
|
|
@@ -108,22 +117,19 @@ class Traces:
|
|
|
108
117
|
self._lock = RLock()
|
|
109
118
|
self._spans: Dict[SpanID, ReadableSpan] = {}
|
|
110
119
|
self._parent_span_ids: Dict[SpanID, ParentSpanID] = {}
|
|
111
|
-
self._traces: DefaultDict[TraceID,
|
|
112
|
-
self.
|
|
113
|
-
self._orphan_spans: DefaultDict[ParentSpanID, List[otlp.Span]] = defaultdict(list)
|
|
120
|
+
self._traces: DefaultDict[TraceID, Set[ReadableSpan]] = defaultdict(set)
|
|
121
|
+
self._child_spans: DefaultDict[SpanID, Set[ReadableSpan]] = defaultdict(set)
|
|
114
122
|
self._num_documents: DefaultDict[SpanID, int] = defaultdict(int)
|
|
115
|
-
self.
|
|
116
|
-
key=lambda
|
|
123
|
+
self._start_time_sorted_spans: SortedKeyList[ReadableSpan] = SortedKeyList(
|
|
124
|
+
key=lambda span: span.start_time,
|
|
117
125
|
)
|
|
118
|
-
self.
|
|
119
|
-
key=lambda
|
|
126
|
+
self._start_time_sorted_root_spans: SortedKeyList[ReadableSpan] = SortedKeyList(
|
|
127
|
+
key=lambda span: span.start_time,
|
|
120
128
|
)
|
|
121
|
-
self.
|
|
122
|
-
key=lambda
|
|
129
|
+
self._latency_sorted_root_spans: SortedKeyList[ReadableSpan] = SortedKeyList(
|
|
130
|
+
key=lambda span: span[ComputedAttributes.LATENCY_MS.value],
|
|
123
131
|
)
|
|
124
132
|
self._root_span_latency_ms_sketch = DDSketch()
|
|
125
|
-
self._min_start_time: Optional[datetime] = None
|
|
126
|
-
self._max_start_time: Optional[datetime] = None
|
|
127
133
|
self._token_count_total: int = 0
|
|
128
134
|
self._last_updated_at: Optional[datetime] = None
|
|
129
135
|
self._start_consumer()
|
|
@@ -136,10 +142,9 @@ class Traces:
|
|
|
136
142
|
# make a copy because source data can mutate during iteration
|
|
137
143
|
if not (trace := self._traces.get(trace_id)):
|
|
138
144
|
return
|
|
139
|
-
|
|
140
|
-
for
|
|
141
|
-
|
|
142
|
-
yield span
|
|
145
|
+
spans = tuple(trace)
|
|
146
|
+
for span in spans:
|
|
147
|
+
yield span.span
|
|
143
148
|
|
|
144
149
|
def get_spans(
|
|
145
150
|
self,
|
|
@@ -157,32 +162,34 @@ class Traces:
|
|
|
157
162
|
start_time = start_time or min_start_time
|
|
158
163
|
stop_time = stop_time or max_stop_time
|
|
159
164
|
if span_ids is not None:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
self._start_time_sorted_root_span_ids
|
|
170
|
-
if root_spans_only
|
|
171
|
-
else self._start_time_sorted_span_ids
|
|
172
|
-
)
|
|
173
|
-
with self._lock:
|
|
174
|
-
# make a copy because source data can mutate during iteration
|
|
175
|
-
span_ids = tuple(
|
|
176
|
-
sorted_span_ids.irange_key(
|
|
177
|
-
start_time.astimezone(timezone.utc),
|
|
178
|
-
stop_time.astimezone(timezone.utc),
|
|
179
|
-
inclusive=(True, False),
|
|
180
|
-
reverse=True, # most recent spans first
|
|
165
|
+
with self._lock:
|
|
166
|
+
spans = tuple(
|
|
167
|
+
span
|
|
168
|
+
for span_id in span_ids
|
|
169
|
+
if (
|
|
170
|
+
(span := self._spans.get(span_id))
|
|
171
|
+
and start_time <= span.start_time < stop_time
|
|
172
|
+
and (not root_spans_only or span.parent_id is None)
|
|
173
|
+
)
|
|
181
174
|
)
|
|
175
|
+
else:
|
|
176
|
+
sorted_spans = (
|
|
177
|
+
self._start_time_sorted_root_spans
|
|
178
|
+
if root_spans_only
|
|
179
|
+
else self._start_time_sorted_spans
|
|
182
180
|
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
# make a copy because source data can mutate during iteration
|
|
182
|
+
with self._lock:
|
|
183
|
+
spans = tuple(
|
|
184
|
+
sorted_spans.irange_key(
|
|
185
|
+
start_time.astimezone(timezone.utc),
|
|
186
|
+
stop_time.astimezone(timezone.utc),
|
|
187
|
+
inclusive=(True, False),
|
|
188
|
+
reverse=True, # most recent spans first
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
for span in spans:
|
|
192
|
+
yield span.span
|
|
186
193
|
|
|
187
194
|
def get_num_documents(self, span_id: SpanID) -> int:
|
|
188
195
|
with self._lock:
|
|
@@ -194,11 +201,11 @@ class Traces:
|
|
|
194
201
|
latency value as percent of the total count of root spans. E.g., for
|
|
195
202
|
a latency value at the 75th percentile, the result is roughly 75.
|
|
196
203
|
"""
|
|
197
|
-
|
|
198
|
-
if not (n := len(
|
|
204
|
+
root_spans = self._latency_sorted_root_spans
|
|
205
|
+
if not (n := len(root_spans)):
|
|
199
206
|
return None
|
|
200
207
|
with self._lock:
|
|
201
|
-
rank = cast(int,
|
|
208
|
+
rank = cast(int, root_spans.bisect_key_left(latency_ms))
|
|
202
209
|
return rank / n * 100
|
|
203
210
|
|
|
204
211
|
def root_span_latency_ms_quantiles(self, *probabilities: float) -> Iterator[Optional[float]]:
|
|
@@ -210,15 +217,19 @@ class Traces:
|
|
|
210
217
|
)
|
|
211
218
|
yield from values
|
|
212
219
|
|
|
213
|
-
def
|
|
220
|
+
def get_descendant_spans(self, span_id: SpanID) -> Iterator[Span]:
|
|
221
|
+
for span in self._get_descendant_spans(span_id):
|
|
222
|
+
yield span.span
|
|
223
|
+
|
|
224
|
+
def _get_descendant_spans(self, span_id: SpanID) -> Iterator[ReadableSpan]:
|
|
214
225
|
with self._lock:
|
|
215
226
|
# make a copy because source data can mutate during iteration
|
|
216
|
-
if not (
|
|
227
|
+
if not (child_spans := self._child_spans.get(span_id)):
|
|
217
228
|
return
|
|
218
|
-
|
|
219
|
-
for
|
|
220
|
-
yield
|
|
221
|
-
yield from self.
|
|
229
|
+
spans = tuple(child_spans)
|
|
230
|
+
for child_span in spans:
|
|
231
|
+
yield child_span
|
|
232
|
+
yield from self._get_descendant_spans(child_span.context.span_id)
|
|
222
233
|
|
|
223
234
|
@property
|
|
224
235
|
def last_updated_at(self) -> Optional[datetime]:
|
|
@@ -226,7 +237,7 @@ class Traces:
|
|
|
226
237
|
|
|
227
238
|
@property
|
|
228
239
|
def span_count(self) -> int:
|
|
229
|
-
"""Total number of spans
|
|
240
|
+
"""Total number of spans"""
|
|
230
241
|
return len(self._spans)
|
|
231
242
|
|
|
232
243
|
@property
|
|
@@ -235,7 +246,14 @@ class Traces:
|
|
|
235
246
|
|
|
236
247
|
@property
|
|
237
248
|
def right_open_time_range(self) -> Tuple[Optional[datetime], Optional[datetime]]:
|
|
238
|
-
|
|
249
|
+
if not self._start_time_sorted_spans:
|
|
250
|
+
return None, None
|
|
251
|
+
with self._lock:
|
|
252
|
+
first_span = self._start_time_sorted_spans[0]
|
|
253
|
+
last_span = self._start_time_sorted_spans[-1]
|
|
254
|
+
min_start_time = first_span.start_time
|
|
255
|
+
max_start_time = last_span.start_time
|
|
256
|
+
return right_open_time_range(min_start_time, max_start_time)
|
|
239
257
|
|
|
240
258
|
def __getitem__(self, span_id: SpanID) -> Optional[Span]:
|
|
241
259
|
with self._lock:
|
|
@@ -257,97 +275,67 @@ class Traces:
|
|
|
257
275
|
with self._lock:
|
|
258
276
|
self._process_span(item)
|
|
259
277
|
|
|
260
|
-
def _process_span(self,
|
|
261
|
-
|
|
262
|
-
span_id =
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
# Reject updates if span has ended.
|
|
278
|
+
def _process_span(self, otlp_span: otlp.Span) -> None:
|
|
279
|
+
span = ReadableSpan(otlp_span)
|
|
280
|
+
span_id = span.context.span_id
|
|
281
|
+
if span_id in self._spans:
|
|
282
|
+
# Update is not allowed.
|
|
266
283
|
return
|
|
267
|
-
|
|
284
|
+
|
|
285
|
+
parent_span_id = span.parent_id
|
|
286
|
+
is_root_span = parent_span_id is None
|
|
268
287
|
if not is_root_span:
|
|
269
|
-
parent_span_id
|
|
270
|
-
if parent_span_id not in self._spans:
|
|
271
|
-
# Span can't be processed before its parent.
|
|
272
|
-
self._orphan_spans[parent_span_id].append(span)
|
|
273
|
-
return
|
|
274
|
-
self._child_span_ids[parent_span_id].add(span_id)
|
|
288
|
+
self._child_spans[parent_span_id].add(span)
|
|
275
289
|
self._parent_span_ids[span_id] = parent_span_id
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
self.
|
|
293
|
-
self.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
(LLM_TOKEN_COUNT_TOTAL, ComputedAttributes.CUMULATIVE_LLM_TOKEN_COUNT_TOTAL.value),
|
|
309
|
-
(LLM_TOKEN_COUNT_PROMPT, ComputedAttributes.CUMULATIVE_LLM_TOKEN_COUNT_PROMPT.value),
|
|
310
|
-
(
|
|
311
|
-
LLM_TOKEN_COUNT_COMPLETION,
|
|
312
|
-
ComputedAttributes.CUMULATIVE_LLM_TOKEN_COUNT_COMPLETION.value,
|
|
313
|
-
),
|
|
314
|
-
(
|
|
315
|
-
ComputedAttributes.ERROR_COUNT.value,
|
|
316
|
-
ComputedAttributes.CUMULATIVE_ERROR_COUNT.value,
|
|
317
|
-
),
|
|
290
|
+
|
|
291
|
+
# Add computed attributes to span
|
|
292
|
+
start_time = span.start_time
|
|
293
|
+
end_time = span.end_time
|
|
294
|
+
span[ComputedAttributes.LATENCY_MS.value] = latency = (
|
|
295
|
+
end_time - start_time
|
|
296
|
+
).total_seconds() * 1000
|
|
297
|
+
if is_root_span:
|
|
298
|
+
self._root_span_latency_ms_sketch.add(latency)
|
|
299
|
+
span[ComputedAttributes.ERROR_COUNT.value] = int(span.status_code is SpanStatusCode.ERROR)
|
|
300
|
+
|
|
301
|
+
# Store the new span (after adding computed attributes)
|
|
302
|
+
self._spans[span_id] = span
|
|
303
|
+
self._traces[span.context.trace_id].add(span)
|
|
304
|
+
self._start_time_sorted_spans.add(span)
|
|
305
|
+
if is_root_span:
|
|
306
|
+
self._start_time_sorted_root_spans.add(span)
|
|
307
|
+
self._latency_sorted_root_spans.add(span)
|
|
308
|
+
self._propagate_cumulative_values(span)
|
|
309
|
+
self._update_cached_statistics(span)
|
|
310
|
+
|
|
311
|
+
# Update last updated timestamp, letting users know
|
|
312
|
+
# when they should refresh the page.
|
|
313
|
+
self._last_updated_at = datetime.now(timezone.utc)
|
|
314
|
+
|
|
315
|
+
def _update_cached_statistics(self, span: ReadableSpan) -> None:
|
|
316
|
+
# Update statistics for quick access later
|
|
317
|
+
span_id = span.context.span_id
|
|
318
|
+
if token_count_update := span.attributes.get(SpanAttributes.LLM_TOKEN_COUNT_TOTAL):
|
|
319
|
+
self._token_count_total += token_count_update
|
|
320
|
+
if num_documents_update := len(
|
|
321
|
+
span.attributes.get(SpanAttributes.RETRIEVAL_DOCUMENTS) or ()
|
|
318
322
|
):
|
|
319
|
-
existing_value = (existing_span[attribute_name] or 0) if existing_span else 0
|
|
320
|
-
new_value = new_span[attribute_name] or 0
|
|
321
|
-
if not (difference := new_value - existing_value):
|
|
322
|
-
continue
|
|
323
|
-
existing_cumulative_value = (
|
|
324
|
-
(existing_span[cumulative_attribute_name] or 0) if existing_span else 0
|
|
325
|
-
)
|
|
326
|
-
new_span[cumulative_attribute_name] = difference + existing_cumulative_value
|
|
327
|
-
self._add_value_to_span_ancestors(
|
|
328
|
-
span_id,
|
|
329
|
-
cumulative_attribute_name,
|
|
330
|
-
difference,
|
|
331
|
-
)
|
|
332
|
-
# Update token count total
|
|
333
|
-
if existing_span:
|
|
334
|
-
self._token_count_total -= existing_span[LLM_TOKEN_COUNT_TOTAL] or 0
|
|
335
|
-
self._token_count_total += new_span[LLM_TOKEN_COUNT_TOTAL] or 0
|
|
336
|
-
# Update number of documents
|
|
337
|
-
num_documents_update = len(
|
|
338
|
-
new_span.attributes.get(SpanAttributes.RETRIEVAL_DOCUMENTS) or ()
|
|
339
|
-
)
|
|
340
|
-
if existing_span:
|
|
341
|
-
num_documents_update -= len(
|
|
342
|
-
existing_span.attributes.get(SpanAttributes.RETRIEVAL_DOCUMENTS) or ()
|
|
343
|
-
)
|
|
344
|
-
if num_documents_update:
|
|
345
323
|
self._num_documents[span_id] += num_documents_update
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
324
|
+
|
|
325
|
+
def _propagate_cumulative_values(self, span: ReadableSpan) -> None:
|
|
326
|
+
child_spans: Iterable[ReadableSpan] = self._child_spans.get(span.context.span_id) or ()
|
|
327
|
+
for cumulative_attribute, attribute in _CUMULATIVE_ATTRIBUTES.items():
|
|
328
|
+
span[cumulative_attribute] = span[attribute] or 0
|
|
329
|
+
for child_span in child_spans:
|
|
330
|
+
span[cumulative_attribute] += child_span[cumulative_attribute] or 0
|
|
331
|
+
self._update_ancestors(span)
|
|
332
|
+
|
|
333
|
+
def _update_ancestors(self, span: ReadableSpan) -> None:
|
|
334
|
+
# Add cumulative values to each of the span's ancestors.
|
|
335
|
+
span_id = span.context.span_id
|
|
336
|
+
for attribute in _CUMULATIVE_ATTRIBUTES.keys():
|
|
337
|
+
value = span[attribute] or 0
|
|
338
|
+
self._add_value_to_span_ancestors(span_id, attribute, value)
|
|
351
339
|
|
|
352
340
|
def _add_value_to_span_ancestors(
|
|
353
341
|
self,
|
|
@@ -356,7 +344,18 @@ class Traces:
|
|
|
356
344
|
value: float,
|
|
357
345
|
) -> None:
|
|
358
346
|
while parent_span_id := self._parent_span_ids.get(span_id):
|
|
359
|
-
parent_span
|
|
347
|
+
if not (parent_span := self._spans.get(parent_span_id)):
|
|
348
|
+
return
|
|
360
349
|
cumulative_value = parent_span[attribute_name] or 0
|
|
361
350
|
parent_span[attribute_name] = cumulative_value + value
|
|
362
351
|
span_id = parent_span_id
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
_CUMULATIVE_ATTRIBUTES = MappingProxyType(
|
|
355
|
+
{
|
|
356
|
+
ComputedAttributes.CUMULATIVE_LLM_TOKEN_COUNT_TOTAL.value: LLM_TOKEN_COUNT_TOTAL,
|
|
357
|
+
ComputedAttributes.CUMULATIVE_LLM_TOKEN_COUNT_PROMPT.value: LLM_TOKEN_COUNT_PROMPT,
|
|
358
|
+
ComputedAttributes.CUMULATIVE_LLM_TOKEN_COUNT_COMPLETION.value: LLM_TOKEN_COUNT_COMPLETION,
|
|
359
|
+
ComputedAttributes.CUMULATIVE_ERROR_COUNT.value: ComputedAttributes.ERROR_COUNT.value,
|
|
360
|
+
}
|
|
361
|
+
)
|
phoenix/server/api/types/Span.py
CHANGED
|
@@ -95,6 +95,7 @@ class SpanEvent:
|
|
|
95
95
|
class Span:
|
|
96
96
|
name: str
|
|
97
97
|
status_code: SpanStatusCode
|
|
98
|
+
status_message: str
|
|
98
99
|
start_time: datetime
|
|
99
100
|
end_time: Optional[datetime]
|
|
100
101
|
latency_ms: Optional[float]
|
|
@@ -212,10 +213,7 @@ class Span:
|
|
|
212
213
|
if (traces := info.context.traces) is None:
|
|
213
214
|
return []
|
|
214
215
|
return [
|
|
215
|
-
to_gql_span(
|
|
216
|
-
for span_id in traces.get_descendant_span_ids(
|
|
217
|
-
cast(SpanID, self.context.span_id),
|
|
218
|
-
)
|
|
216
|
+
to_gql_span(span) for span in traces.get_descendant_spans(SpanID(self.context.span_id))
|
|
219
217
|
]
|
|
220
218
|
|
|
221
219
|
|
|
@@ -228,6 +226,7 @@ def to_gql_span(span: trace_schema.Span) -> "Span":
|
|
|
228
226
|
return Span(
|
|
229
227
|
name=span.name,
|
|
230
228
|
status_code=SpanStatusCode(span.status_code),
|
|
229
|
+
status_message=span.status_message,
|
|
231
230
|
parent_id=cast(Optional[ID], span.parent_id),
|
|
232
231
|
span_kind=SpanKind(span.span_kind),
|
|
233
232
|
start_time=span.start_time,
|