arize-phoenix 8.1.0__py3-none-any.whl → 8.2.1__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-8.1.0.dist-info → arize_phoenix-8.2.1.dist-info}/METADATA +1 -1
- {arize_phoenix-8.1.0.dist-info → arize_phoenix-8.2.1.dist-info}/RECORD +36 -34
- phoenix/db/models.py +151 -11
- phoenix/server/api/context.py +4 -0
- phoenix/server/api/dataloaders/__init__.py +4 -0
- phoenix/server/api/dataloaders/span_by_id.py +29 -0
- phoenix/server/api/dataloaders/span_descendants.py +24 -15
- phoenix/server/api/dataloaders/span_fields.py +76 -0
- phoenix/server/api/dataloaders/trace_root_spans.py +9 -10
- phoenix/server/api/mutations/chat_mutations.py +10 -7
- phoenix/server/api/queries.py +2 -2
- phoenix/server/api/subscriptions.py +3 -3
- phoenix/server/api/types/Annotation.py +4 -1
- phoenix/server/api/types/DatasetExample.py +2 -2
- phoenix/server/api/types/Project.py +8 -10
- phoenix/server/api/types/ProjectSession.py +2 -2
- phoenix/server/api/types/Span.py +377 -120
- phoenix/server/api/types/SpanIOValue.py +39 -6
- phoenix/server/api/types/Trace.py +17 -15
- phoenix/server/app.py +4 -0
- phoenix/server/prometheus.py +113 -7
- phoenix/server/static/.vite/manifest.json +36 -36
- phoenix/server/static/assets/{components-B-qgPyHv.js → components-CgcYOKnv.js} +175 -170
- phoenix/server/static/assets/{index-D4KO1IcF.js → index-B_Nkd6Rh.js} +2 -2
- phoenix/server/static/assets/{pages-DdcuL3Rh.js → pages-Cz-JsoAE.js} +327 -327
- phoenix/server/static/assets/{vendor-DQp7CrDA.js → vendor-Cqfydjep.js} +117 -117
- phoenix/server/static/assets/{vendor-arizeai-C1nEIEQq.js → vendor-arizeai-WnerlUPN.js} +1 -1
- phoenix/server/static/assets/{vendor-codemirror-BZXYUIkP.js → vendor-codemirror-D-ZZKLFq.js} +1 -1
- phoenix/server/static/assets/{vendor-recharts-BUFpwCVD.js → vendor-recharts-KY97ZPfK.js} +1 -1
- phoenix/server/static/assets/{vendor-shiki-C8L-c9jT.js → vendor-shiki-D5K9GnFn.js} +1 -1
- phoenix/trace/attributes.py +7 -2
- phoenix/version.py +1 -1
- {arize_phoenix-8.1.0.dist-info → arize_phoenix-8.2.1.dist-info}/WHEEL +0 -0
- {arize_phoenix-8.1.0.dist-info → arize_phoenix-8.2.1.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-8.1.0.dist-info → arize_phoenix-8.2.1.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-8.1.0.dist-info → arize_phoenix-8.2.1.dist-info}/licenses/LICENSE +0 -0
phoenix/server/api/types/Span.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from
|
|
2
|
+
from asyncio import gather
|
|
3
|
+
from collections.abc import Mapping
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from enum import Enum
|
|
5
|
-
from typing import TYPE_CHECKING, Any, Optional, cast
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Iterable, Optional, cast
|
|
6
7
|
|
|
7
8
|
import numpy as np
|
|
8
9
|
import strawberry
|
|
@@ -10,7 +11,7 @@ from openinference.semconv.trace import SpanAttributes
|
|
|
10
11
|
from strawberry import ID, UNSET
|
|
11
12
|
from strawberry.relay import Node, NodeID
|
|
12
13
|
from strawberry.types import Info
|
|
13
|
-
from typing_extensions import Annotated
|
|
14
|
+
from typing_extensions import Annotated, TypeAlias
|
|
14
15
|
|
|
15
16
|
import phoenix.trace.schemas as trace_schema
|
|
16
17
|
from phoenix.db import models
|
|
@@ -31,7 +32,7 @@ from phoenix.server.api.types.GenerativeProvider import GenerativeProvider
|
|
|
31
32
|
from phoenix.server.api.types.MimeType import MimeType
|
|
32
33
|
from phoenix.server.api.types.SortDir import SortDir
|
|
33
34
|
from phoenix.server.api.types.SpanAnnotation import SpanAnnotation, to_gql_span_annotation
|
|
34
|
-
from phoenix.server.api.types.SpanIOValue import SpanIOValue
|
|
35
|
+
from phoenix.server.api.types.SpanIOValue import SpanIOValue, truncate_value
|
|
35
36
|
from phoenix.trace.attributes import get_attribute_value
|
|
36
37
|
|
|
37
38
|
if TYPE_CHECKING:
|
|
@@ -102,50 +103,346 @@ class SpanEvent:
|
|
|
102
103
|
class SpanAsExampleRevision(ExampleRevision): ...
|
|
103
104
|
|
|
104
105
|
|
|
106
|
+
SpanRowId: TypeAlias = int
|
|
107
|
+
|
|
108
|
+
|
|
105
109
|
@strawberry.type
|
|
106
110
|
class Span(Node):
|
|
107
|
-
|
|
108
|
-
db_span: strawberry.Private[models.Span]
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
span_rowid: NodeID[SpanRowId]
|
|
112
|
+
db_span: strawberry.Private[models.Span] = UNSET
|
|
113
|
+
|
|
114
|
+
def __post_init__(self) -> None:
|
|
115
|
+
if self.db_span and self.span_rowid != self.db_span.id:
|
|
116
|
+
raise ValueError("Span ID mismatch")
|
|
117
|
+
|
|
118
|
+
@strawberry.field
|
|
119
|
+
async def name(
|
|
120
|
+
self,
|
|
121
|
+
info: Info[Context, None],
|
|
122
|
+
) -> str:
|
|
123
|
+
if self.db_span:
|
|
124
|
+
return self.db_span.name
|
|
125
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
126
|
+
(self.span_rowid, models.Span.name),
|
|
127
|
+
)
|
|
128
|
+
return str(value)
|
|
129
|
+
|
|
130
|
+
@strawberry.field
|
|
131
|
+
async def status_code(
|
|
132
|
+
self,
|
|
133
|
+
info: Info[Context, None],
|
|
134
|
+
) -> SpanStatusCode:
|
|
135
|
+
if self.db_span:
|
|
136
|
+
value = self.db_span.status_code
|
|
137
|
+
else:
|
|
138
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
139
|
+
(self.span_rowid, models.Span.status_code),
|
|
140
|
+
)
|
|
141
|
+
return SpanStatusCode(value)
|
|
142
|
+
|
|
143
|
+
@strawberry.field
|
|
144
|
+
async def status_message(
|
|
145
|
+
self,
|
|
146
|
+
info: Info[Context, None],
|
|
147
|
+
) -> str:
|
|
148
|
+
if self.db_span:
|
|
149
|
+
return self.db_span.status_message
|
|
150
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
151
|
+
(self.span_rowid, models.Span.status_message),
|
|
152
|
+
)
|
|
153
|
+
return str(value)
|
|
154
|
+
|
|
155
|
+
@strawberry.field
|
|
156
|
+
async def start_time(
|
|
157
|
+
self,
|
|
158
|
+
info: Info[Context, None],
|
|
159
|
+
) -> datetime:
|
|
160
|
+
if self.db_span:
|
|
161
|
+
return self.db_span.start_time
|
|
162
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
163
|
+
(self.span_rowid, models.Span.start_time),
|
|
164
|
+
)
|
|
165
|
+
return cast(datetime, value)
|
|
166
|
+
|
|
167
|
+
@strawberry.field
|
|
168
|
+
async def end_time(
|
|
169
|
+
self,
|
|
170
|
+
info: Info[Context, None],
|
|
171
|
+
) -> Optional[datetime]:
|
|
172
|
+
if self.db_span:
|
|
173
|
+
return self.db_span.end_time
|
|
174
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
175
|
+
(self.span_rowid, models.Span.end_time),
|
|
176
|
+
)
|
|
177
|
+
return cast(datetime, value)
|
|
178
|
+
|
|
179
|
+
@strawberry.field
|
|
180
|
+
async def latency_ms(
|
|
181
|
+
self,
|
|
182
|
+
info: Info[Context, None],
|
|
183
|
+
) -> Optional[float]:
|
|
184
|
+
if self.db_span:
|
|
185
|
+
return self.db_span.latency_ms
|
|
186
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
187
|
+
(self.span_rowid, models.Span.latency_ms),
|
|
188
|
+
)
|
|
189
|
+
return cast(float, value)
|
|
190
|
+
|
|
191
|
+
@strawberry.field(
|
|
192
|
+
description="the parent span ID. If null, it is a root span",
|
|
193
|
+
) # type: ignore
|
|
194
|
+
async def parent_id(
|
|
195
|
+
self,
|
|
196
|
+
info: Info[Context, None],
|
|
197
|
+
) -> Optional[ID]:
|
|
198
|
+
if self.db_span:
|
|
199
|
+
value = self.db_span.parent_id
|
|
200
|
+
else:
|
|
201
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
202
|
+
(self.span_rowid, models.Span.parent_id),
|
|
203
|
+
)
|
|
204
|
+
return None if value is None else ID(value)
|
|
205
|
+
|
|
206
|
+
@strawberry.field
|
|
207
|
+
async def span_kind(
|
|
208
|
+
self,
|
|
209
|
+
info: Info[Context, None],
|
|
210
|
+
) -> SpanKind:
|
|
211
|
+
if self.db_span:
|
|
212
|
+
value = self.db_span.span_kind
|
|
213
|
+
else:
|
|
214
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
215
|
+
(self.span_rowid, models.Span.span_kind),
|
|
216
|
+
)
|
|
217
|
+
return SpanKind(value)
|
|
218
|
+
|
|
219
|
+
@strawberry.field
|
|
220
|
+
async def context(
|
|
221
|
+
self,
|
|
222
|
+
info: Info[Context, None],
|
|
223
|
+
) -> SpanContext:
|
|
224
|
+
if self.db_span:
|
|
225
|
+
trace_id = self.db_span.trace.trace_id
|
|
226
|
+
span_id = self.db_span.span_id
|
|
227
|
+
else:
|
|
228
|
+
span_id, trace_id = await gather(
|
|
229
|
+
info.context.data_loaders.span_fields.load(
|
|
230
|
+
(self.span_rowid, models.Span.span_id),
|
|
231
|
+
),
|
|
232
|
+
info.context.data_loaders.span_fields.load(
|
|
233
|
+
(self.span_rowid, models.Trace.trace_id),
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
return SpanContext(trace_id=ID(trace_id), span_id=ID(span_id))
|
|
237
|
+
|
|
238
|
+
@strawberry.field(
|
|
121
239
|
description="Span attributes as a JSON string",
|
|
122
|
-
)
|
|
123
|
-
|
|
240
|
+
) # type: ignore
|
|
241
|
+
async def attributes(
|
|
242
|
+
self,
|
|
243
|
+
info: Info[Context, None],
|
|
244
|
+
) -> str:
|
|
245
|
+
if self.db_span:
|
|
246
|
+
value = self.db_span.attributes
|
|
247
|
+
else:
|
|
248
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
249
|
+
(self.span_rowid, models.Span.attributes),
|
|
250
|
+
)
|
|
251
|
+
return json.dumps(_hide_embedding_vectors(value), cls=_JSONEncoder)
|
|
252
|
+
|
|
253
|
+
@strawberry.field(
|
|
124
254
|
description="Metadata as a JSON string",
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
255
|
+
) # type: ignore
|
|
256
|
+
async def metadata(
|
|
257
|
+
self,
|
|
258
|
+
info: Info[Context, None],
|
|
259
|
+
) -> Optional[str]:
|
|
260
|
+
if self.db_span:
|
|
261
|
+
value = self.db_span.metadata_
|
|
262
|
+
else:
|
|
263
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
264
|
+
(self.span_rowid, models.Span.metadata_),
|
|
265
|
+
)
|
|
266
|
+
return _convert_metadata_to_string(value)
|
|
267
|
+
|
|
268
|
+
@strawberry.field
|
|
269
|
+
async def num_documents(
|
|
270
|
+
self,
|
|
271
|
+
info: Info[Context, None],
|
|
272
|
+
) -> Optional[int]:
|
|
273
|
+
if self.db_span:
|
|
274
|
+
return self.db_span.num_documents
|
|
275
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
276
|
+
(self.span_rowid, models.Span.num_documents),
|
|
277
|
+
)
|
|
278
|
+
return cast(int, value)
|
|
279
|
+
|
|
280
|
+
@strawberry.field
|
|
281
|
+
async def token_count_total(
|
|
282
|
+
self,
|
|
283
|
+
info: Info[Context, None],
|
|
284
|
+
) -> Optional[int]:
|
|
285
|
+
if self.db_span:
|
|
286
|
+
return self.db_span.llm_token_count_total
|
|
287
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
288
|
+
(self.span_rowid, models.Span.llm_token_count_total),
|
|
289
|
+
)
|
|
290
|
+
return cast(Optional[int], value)
|
|
291
|
+
|
|
292
|
+
@strawberry.field
|
|
293
|
+
async def token_count_prompt(
|
|
294
|
+
self,
|
|
295
|
+
info: Info[Context, None],
|
|
296
|
+
) -> Optional[int]:
|
|
297
|
+
if self.db_span:
|
|
298
|
+
return self.db_span.llm_token_count_prompt
|
|
299
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
300
|
+
(self.span_rowid, models.Span.llm_token_count_prompt),
|
|
301
|
+
)
|
|
302
|
+
return cast(Optional[int], value)
|
|
303
|
+
|
|
304
|
+
@strawberry.field
|
|
305
|
+
async def token_count_completion(
|
|
306
|
+
self,
|
|
307
|
+
info: Info[Context, None],
|
|
308
|
+
) -> Optional[int]:
|
|
309
|
+
if self.db_span:
|
|
310
|
+
return self.db_span.llm_token_count_completion
|
|
311
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
312
|
+
(self.span_rowid, models.Span.llm_token_count_completion),
|
|
313
|
+
)
|
|
314
|
+
return cast(Optional[int], value)
|
|
315
|
+
|
|
316
|
+
@strawberry.field
|
|
317
|
+
async def input(
|
|
318
|
+
self,
|
|
319
|
+
info: Info[Context, None],
|
|
320
|
+
) -> Optional[SpanIOValue]:
|
|
321
|
+
if self.db_span:
|
|
322
|
+
mime_type = self.db_span.input_mime_type
|
|
323
|
+
input_value = self.db_span.input_value
|
|
324
|
+
return SpanIOValue(
|
|
325
|
+
cached_value=input_value,
|
|
326
|
+
mime_type=MimeType(mime_type),
|
|
327
|
+
)
|
|
328
|
+
mime_type, input_value_first_101_chars = await gather(
|
|
329
|
+
info.context.data_loaders.span_fields.load(
|
|
330
|
+
(self.span_rowid, models.Span.input_mime_type),
|
|
331
|
+
),
|
|
332
|
+
info.context.data_loaders.span_fields.load(
|
|
333
|
+
(self.span_rowid, models.Span.input_value_first_101_chars),
|
|
334
|
+
),
|
|
335
|
+
)
|
|
336
|
+
if not input_value_first_101_chars:
|
|
337
|
+
return None
|
|
338
|
+
return SpanIOValue(
|
|
339
|
+
span_rowid=self.span_rowid,
|
|
340
|
+
attr=models.Span.input_value,
|
|
341
|
+
truncated_value=truncate_value(input_value_first_101_chars),
|
|
342
|
+
mime_type=MimeType(mime_type),
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
@strawberry.field
|
|
346
|
+
async def output(
|
|
347
|
+
self,
|
|
348
|
+
info: Info[Context, None],
|
|
349
|
+
) -> Optional[SpanIOValue]:
|
|
350
|
+
if self.db_span:
|
|
351
|
+
mime_type = self.db_span.output_mime_type
|
|
352
|
+
output_value = self.db_span.output_value
|
|
353
|
+
return SpanIOValue(
|
|
354
|
+
cached_value=output_value,
|
|
355
|
+
mime_type=MimeType(mime_type),
|
|
356
|
+
)
|
|
357
|
+
mime_type, output_value_first_101_chars = await gather(
|
|
358
|
+
info.context.data_loaders.span_fields.load(
|
|
359
|
+
(self.span_rowid, models.Span.output_mime_type),
|
|
360
|
+
),
|
|
361
|
+
info.context.data_loaders.span_fields.load(
|
|
362
|
+
(self.span_rowid, models.Span.output_value_first_101_chars),
|
|
363
|
+
),
|
|
364
|
+
)
|
|
365
|
+
if not output_value_first_101_chars:
|
|
366
|
+
return None
|
|
367
|
+
return SpanIOValue(
|
|
368
|
+
span_rowid=self.span_rowid,
|
|
369
|
+
attr=models.Span.output_value,
|
|
370
|
+
truncated_value=truncate_value(output_value_first_101_chars),
|
|
371
|
+
mime_type=MimeType(mime_type),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
@strawberry.field
|
|
375
|
+
async def events(
|
|
376
|
+
self,
|
|
377
|
+
info: Info[Context, None],
|
|
378
|
+
) -> list[SpanEvent]:
|
|
379
|
+
if self.db_span:
|
|
380
|
+
return [SpanEvent.from_dict(event) for event in self.db_span.events]
|
|
381
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
382
|
+
(self.span_rowid, models.Span.events),
|
|
383
|
+
)
|
|
384
|
+
return [SpanEvent.from_dict(event) for event in value]
|
|
385
|
+
|
|
386
|
+
@strawberry.field(
|
|
387
|
+
description="Cumulative (prompt plus completion) token count from self "
|
|
388
|
+
"and all descendant spans (children, grandchildren, etc.)",
|
|
389
|
+
) # type: ignore
|
|
390
|
+
async def cumulative_token_count_total(
|
|
391
|
+
self,
|
|
392
|
+
info: Info[Context, None],
|
|
393
|
+
) -> Optional[int]:
|
|
394
|
+
if self.db_span:
|
|
395
|
+
return self.db_span.cumulative_llm_token_count_total
|
|
396
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
397
|
+
(self.span_rowid, models.Span.cumulative_llm_token_count_total),
|
|
398
|
+
)
|
|
399
|
+
return cast(Optional[int], value)
|
|
400
|
+
|
|
401
|
+
@strawberry.field(
|
|
402
|
+
description="Cumulative (prompt) token count from self and all descendant "
|
|
403
|
+
"spans (children, grandchildren, etc.)",
|
|
404
|
+
) # type: ignore
|
|
405
|
+
async def cumulative_token_count_prompt(
|
|
406
|
+
self,
|
|
407
|
+
info: Info[Context, None],
|
|
408
|
+
) -> Optional[int]:
|
|
409
|
+
if self.db_span:
|
|
410
|
+
return self.db_span.cumulative_llm_token_count_prompt
|
|
411
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
412
|
+
(self.span_rowid, models.Span.cumulative_llm_token_count_prompt),
|
|
413
|
+
)
|
|
414
|
+
return cast(Optional[int], value)
|
|
415
|
+
|
|
416
|
+
@strawberry.field(
|
|
417
|
+
description="Cumulative (completion) token count from self and all descendant "
|
|
418
|
+
"spans (children, grandchildren, etc.)",
|
|
419
|
+
) # type: ignore
|
|
420
|
+
async def cumulative_token_count_completion(
|
|
421
|
+
self,
|
|
422
|
+
info: Info[Context, None],
|
|
423
|
+
) -> Optional[int]:
|
|
424
|
+
if self.db_span:
|
|
425
|
+
return self.db_span.cumulative_llm_token_count_completion
|
|
426
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
427
|
+
(self.span_rowid, models.Span.cumulative_llm_token_count_completion),
|
|
428
|
+
)
|
|
429
|
+
return cast(Optional[int], value)
|
|
430
|
+
|
|
431
|
+
@strawberry.field(
|
|
432
|
+
description="Propagated status code that percolates up error status codes from "
|
|
143
433
|
"descendant spans (children, grandchildren, etc.)",
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
)
|
|
434
|
+
) # type: ignore
|
|
435
|
+
async def propagated_status_code(
|
|
436
|
+
self,
|
|
437
|
+
info: Info[Context, None],
|
|
438
|
+
) -> SpanStatusCode:
|
|
439
|
+
if self.db_span:
|
|
440
|
+
value = self.db_span.cumulative_error_count
|
|
441
|
+
else:
|
|
442
|
+
value = await info.context.data_loaders.span_fields.load(
|
|
443
|
+
(self.span_rowid, models.Span.cumulative_error_count),
|
|
444
|
+
)
|
|
445
|
+
return SpanStatusCode.ERROR if value else SpanStatusCode.OK
|
|
149
446
|
|
|
150
447
|
@strawberry.field(
|
|
151
448
|
description=(
|
|
@@ -158,7 +455,7 @@ class Span(Node):
|
|
|
158
455
|
info: Info[Context, None],
|
|
159
456
|
sort: Optional[SpanAnnotationSort] = UNSET,
|
|
160
457
|
) -> list[SpanAnnotation]:
|
|
161
|
-
span_id = self.
|
|
458
|
+
span_id = self.span_rowid
|
|
162
459
|
annotations = await info.context.data_loaders.span_annotations.load(span_id)
|
|
163
460
|
sort_key = SpanAnnotationColumn.name.value
|
|
164
461
|
sort_descending = False
|
|
@@ -178,8 +475,11 @@ class Span(Node):
|
|
|
178
475
|
"a list, and each evaluation is identified by its document's (zero-based) "
|
|
179
476
|
"index in that list."
|
|
180
477
|
) # type: ignore
|
|
181
|
-
async def document_evaluations(
|
|
182
|
-
|
|
478
|
+
async def document_evaluations(
|
|
479
|
+
self,
|
|
480
|
+
info: Info[Context, None],
|
|
481
|
+
) -> list[DocumentEvaluation]:
|
|
482
|
+
return await info.context.data_loaders.document_evaluations.load(self.span_rowid)
|
|
183
483
|
|
|
184
484
|
@strawberry.field(
|
|
185
485
|
description="Retrieval metrics: NDCG@K, Precision@K, Reciprocal Rank, etc.",
|
|
@@ -189,10 +489,17 @@ class Span(Node):
|
|
|
189
489
|
info: Info[Context, None],
|
|
190
490
|
evaluation_name: Optional[str] = UNSET,
|
|
191
491
|
) -> list[DocumentRetrievalMetrics]:
|
|
192
|
-
|
|
492
|
+
num_documents = (
|
|
493
|
+
self.db_span.num_documents
|
|
494
|
+
if self.db_span
|
|
495
|
+
else await info.context.data_loaders.span_fields.load(
|
|
496
|
+
(self.span_rowid, models.Span.num_documents),
|
|
497
|
+
)
|
|
498
|
+
)
|
|
499
|
+
if not num_documents:
|
|
193
500
|
return []
|
|
194
501
|
return await info.context.data_loaders.document_retrieval_metrics.load(
|
|
195
|
-
(self.
|
|
502
|
+
(self.span_rowid, evaluation_name or None, num_documents),
|
|
196
503
|
)
|
|
197
504
|
|
|
198
505
|
@strawberry.field(
|
|
@@ -202,15 +509,21 @@ class Span(Node):
|
|
|
202
509
|
self,
|
|
203
510
|
info: Info[Context, None],
|
|
204
511
|
) -> list["Span"]:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return [to_gql_span(span) for span in spans]
|
|
512
|
+
ids: Iterable[int] = await info.context.data_loaders.span_descendants.load(self.span_rowid)
|
|
513
|
+
return [Span(span_rowid=id_) for id_ in ids]
|
|
208
514
|
|
|
209
515
|
@strawberry.field(
|
|
210
516
|
description="The span's attributes translated into an example revision for a dataset",
|
|
211
517
|
) # type: ignore
|
|
212
|
-
async def as_example_revision(
|
|
213
|
-
|
|
518
|
+
async def as_example_revision(
|
|
519
|
+
self,
|
|
520
|
+
info: Info[Context, None],
|
|
521
|
+
) -> SpanAsExampleRevision:
|
|
522
|
+
span = (
|
|
523
|
+
self.db_span
|
|
524
|
+
if self.db_span
|
|
525
|
+
else await info.context.data_loaders.span_by_id.load(self.span_rowid)
|
|
526
|
+
)
|
|
214
527
|
|
|
215
528
|
# Fetch annotations associated with this span
|
|
216
529
|
span_annotations = await self.span_annotations(info)
|
|
@@ -244,21 +557,27 @@ class Span(Node):
|
|
|
244
557
|
]: # use lazy types to avoid circular import: https://strawberry.rocks/docs/types/lazy
|
|
245
558
|
from phoenix.server.api.types.Project import to_gql_project
|
|
246
559
|
|
|
247
|
-
span_id = self.
|
|
560
|
+
span_id = self.span_rowid
|
|
248
561
|
project = await info.context.data_loaders.span_projects.load(span_id)
|
|
249
562
|
return to_gql_project(project)
|
|
250
563
|
|
|
251
564
|
@strawberry.field(description="Indicates if the span is contained in any dataset") # type: ignore
|
|
252
|
-
async def contained_in_dataset(
|
|
253
|
-
|
|
565
|
+
async def contained_in_dataset(
|
|
566
|
+
self,
|
|
567
|
+
info: Info[Context, None],
|
|
568
|
+
) -> bool:
|
|
569
|
+
examples = await info.context.data_loaders.span_dataset_examples.load(self.span_rowid)
|
|
254
570
|
return bool(examples)
|
|
255
571
|
|
|
256
572
|
@strawberry.field(description="Invocation parameters for the span") # type: ignore
|
|
257
|
-
async def invocation_parameters(
|
|
573
|
+
async def invocation_parameters(
|
|
574
|
+
self,
|
|
575
|
+
info: Info[Context, None],
|
|
576
|
+
) -> list[InvocationParameter]:
|
|
258
577
|
from phoenix.server.api.helpers.playground_clients import OpenAIStreamingClient
|
|
259
578
|
from phoenix.server.api.helpers.playground_registry import PLAYGROUND_CLIENT_REGISTRY
|
|
260
579
|
|
|
261
|
-
db_span = self.
|
|
580
|
+
db_span: models.Span = await info.context.data_loaders.span_by_id.load(self.span_rowid)
|
|
262
581
|
attributes = db_span.attributes
|
|
263
582
|
llm_provider = GenerativeProvider.get_model_provider_from_attributes(attributes)
|
|
264
583
|
if llm_provider is None:
|
|
@@ -288,68 +607,6 @@ class Span(Node):
|
|
|
288
607
|
]
|
|
289
608
|
|
|
290
609
|
|
|
291
|
-
def to_gql_span(span: models.Span) -> Span:
|
|
292
|
-
events: list[SpanEvent] = list(map(SpanEvent.from_dict, span.events))
|
|
293
|
-
input_value = get_attribute_value(span.attributes, INPUT_VALUE)
|
|
294
|
-
if input_value is not None:
|
|
295
|
-
input_value = str(input_value)
|
|
296
|
-
assert input_value is None or isinstance(input_value, str)
|
|
297
|
-
output_value = get_attribute_value(span.attributes, OUTPUT_VALUE)
|
|
298
|
-
if output_value is not None:
|
|
299
|
-
output_value = str(output_value)
|
|
300
|
-
assert output_value is None or isinstance(output_value, str)
|
|
301
|
-
retrieval_documents = get_attribute_value(span.attributes, RETRIEVAL_DOCUMENTS)
|
|
302
|
-
num_documents = len(retrieval_documents) if isinstance(retrieval_documents, Sized) else None
|
|
303
|
-
return Span(
|
|
304
|
-
id_attr=span.id,
|
|
305
|
-
db_span=span,
|
|
306
|
-
name=span.name,
|
|
307
|
-
status_code=SpanStatusCode(span.status_code),
|
|
308
|
-
status_message=span.status_message,
|
|
309
|
-
parent_id=cast(Optional[ID], span.parent_id),
|
|
310
|
-
span_kind=SpanKind(span.span_kind),
|
|
311
|
-
start_time=span.start_time,
|
|
312
|
-
end_time=span.end_time,
|
|
313
|
-
latency_ms=span.latency_ms,
|
|
314
|
-
context=SpanContext(
|
|
315
|
-
trace_id=cast(ID, span.trace.trace_id),
|
|
316
|
-
span_id=cast(ID, span.span_id),
|
|
317
|
-
),
|
|
318
|
-
attributes=json.dumps(_hide_embedding_vectors(span.attributes), cls=_JSONEncoder),
|
|
319
|
-
metadata=_convert_metadata_to_string(get_attribute_value(span.attributes, METADATA)),
|
|
320
|
-
num_documents=num_documents,
|
|
321
|
-
token_count_total=span.llm_token_count_total,
|
|
322
|
-
token_count_prompt=span.llm_token_count_prompt,
|
|
323
|
-
token_count_completion=span.llm_token_count_completion,
|
|
324
|
-
cumulative_token_count_total=span.cumulative_llm_token_count_prompt
|
|
325
|
-
+ span.cumulative_llm_token_count_completion,
|
|
326
|
-
cumulative_token_count_prompt=span.cumulative_llm_token_count_prompt,
|
|
327
|
-
cumulative_token_count_completion=span.cumulative_llm_token_count_completion,
|
|
328
|
-
propagated_status_code=(
|
|
329
|
-
SpanStatusCode.ERROR
|
|
330
|
-
if span.cumulative_error_count
|
|
331
|
-
else SpanStatusCode(span.status_code)
|
|
332
|
-
),
|
|
333
|
-
events=events,
|
|
334
|
-
input=(
|
|
335
|
-
SpanIOValue(
|
|
336
|
-
mime_type=MimeType(get_attribute_value(span.attributes, INPUT_MIME_TYPE)),
|
|
337
|
-
value=input_value,
|
|
338
|
-
)
|
|
339
|
-
if input_value is not None
|
|
340
|
-
else None
|
|
341
|
-
),
|
|
342
|
-
output=(
|
|
343
|
-
SpanIOValue(
|
|
344
|
-
mime_type=MimeType(get_attribute_value(span.attributes, OUTPUT_MIME_TYPE)),
|
|
345
|
-
value=output_value,
|
|
346
|
-
)
|
|
347
|
-
if output_value is not None
|
|
348
|
-
else None
|
|
349
|
-
),
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
|
|
353
610
|
def _hide_embedding_vectors(attributes: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
354
611
|
if not (
|
|
355
612
|
isinstance(em := attributes.get("embedding"), dict)
|
|
@@ -1,15 +1,48 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
1
3
|
import strawberry
|
|
4
|
+
from sqlalchemy.orm import QueryableAttribute
|
|
5
|
+
from strawberry import UNSET, Info
|
|
6
|
+
from typing_extensions import TypeAlias
|
|
2
7
|
|
|
8
|
+
from phoenix.server.api.context import Context
|
|
3
9
|
from phoenix.server.api.types.MimeType import MimeType
|
|
4
10
|
|
|
11
|
+
SpanRowId: TypeAlias = int
|
|
12
|
+
|
|
5
13
|
|
|
6
14
|
@strawberry.type
|
|
7
15
|
class SpanIOValue:
|
|
16
|
+
span_rowid: strawberry.Private[SpanRowId] = UNSET
|
|
17
|
+
attr: strawberry.Private[QueryableAttribute[Any]] = UNSET
|
|
18
|
+
cached_value: strawberry.Private[str] = UNSET
|
|
8
19
|
mime_type: MimeType
|
|
9
|
-
|
|
20
|
+
truncated_value: str = strawberry.field(
|
|
21
|
+
default=UNSET,
|
|
22
|
+
description="Truncated value up to 100 characters, appending '...' if truncated.",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def __post_init__(self) -> None:
|
|
26
|
+
if self.cached_value is not UNSET:
|
|
27
|
+
self.truncated_value = truncate_value(self.cached_value)
|
|
28
|
+
elif self.span_rowid is UNSET or self.attr is UNSET or self.truncated_value is UNSET:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
"SpanIOValue must be initialized with either 'cached_value' or "
|
|
31
|
+
"'truncated_value' and 'id_' and 'attr'."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@strawberry.field
|
|
35
|
+
async def value(
|
|
36
|
+
self,
|
|
37
|
+
info: Info[Context, None],
|
|
38
|
+
) -> str:
|
|
39
|
+
if self.cached_value is not UNSET:
|
|
40
|
+
return self.cached_value
|
|
41
|
+
if not self.truncated_value:
|
|
42
|
+
return ""
|
|
43
|
+
io_value = await info.context.data_loaders.span_fields.load((self.span_rowid, self.attr))
|
|
44
|
+
return "" if io_value is None else str(io_value)
|
|
45
|
+
|
|
10
46
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
) # type: ignore
|
|
14
|
-
def truncated_value(self, chars: int = 100) -> str:
|
|
15
|
-
return f"{self.value[: max(0, chars - 3)]}..." if len(self.value) > chars else self.value
|
|
47
|
+
def truncate_value(value: str, chars: int = 100) -> str:
|
|
48
|
+
return f"{value[: max(0, chars - 3)]}..." if len(value) > chars else value
|