arize-phoenix 8.0.1__py3-none-any.whl → 8.2.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.

Files changed (38) hide show
  1. {arize_phoenix-8.0.1.dist-info → arize_phoenix-8.2.0.dist-info}/METADATA +1 -2
  2. {arize_phoenix-8.0.1.dist-info → arize_phoenix-8.2.0.dist-info}/RECORD +37 -36
  3. phoenix/config.py +32 -6
  4. phoenix/db/models.py +151 -11
  5. phoenix/server/api/context.py +4 -0
  6. phoenix/server/api/dataloaders/__init__.py +4 -0
  7. phoenix/server/api/dataloaders/span_by_id.py +29 -0
  8. phoenix/server/api/dataloaders/span_descendants.py +24 -15
  9. phoenix/server/api/dataloaders/span_fields.py +76 -0
  10. phoenix/server/api/dataloaders/trace_root_spans.py +9 -10
  11. phoenix/server/api/mutations/chat_mutations.py +10 -7
  12. phoenix/server/api/queries.py +2 -2
  13. phoenix/server/api/subscriptions.py +3 -3
  14. phoenix/server/api/types/Annotation.py +4 -1
  15. phoenix/server/api/types/DatasetExample.py +2 -2
  16. phoenix/server/api/types/Project.py +8 -10
  17. phoenix/server/api/types/ProjectSession.py +2 -2
  18. phoenix/server/api/types/Span.py +377 -120
  19. phoenix/server/api/types/SpanIOValue.py +39 -6
  20. phoenix/server/api/types/Trace.py +17 -15
  21. phoenix/server/app.py +4 -0
  22. phoenix/server/prometheus.py +113 -7
  23. phoenix/server/static/.vite/manifest.json +36 -36
  24. phoenix/server/static/assets/{components-B-qgPyHv.js → components-C48uRczp.js} +1 -1
  25. phoenix/server/static/assets/{index-D4KO1IcF.js → index-5klIR86Z.js} +2 -2
  26. phoenix/server/static/assets/{pages-DdcuL3Rh.js → pages-sERWyBWu.js} +326 -326
  27. phoenix/server/static/assets/{vendor-DQp7CrDA.js → vendor-Cqfydjep.js} +117 -117
  28. phoenix/server/static/assets/{vendor-arizeai-C1nEIEQq.js → vendor-arizeai-WnerlUPN.js} +1 -1
  29. phoenix/server/static/assets/{vendor-codemirror-BZXYUIkP.js → vendor-codemirror-D-ZZKLFq.js} +1 -1
  30. phoenix/server/static/assets/{vendor-recharts-BUFpwCVD.js → vendor-recharts-KY97ZPfK.js} +1 -1
  31. phoenix/server/static/assets/{vendor-shiki-C8L-c9jT.js → vendor-shiki-D5K9GnFn.js} +1 -1
  32. phoenix/trace/attributes.py +7 -2
  33. phoenix/version.py +1 -1
  34. phoenix/server/api/helpers/jsonschema.py +0 -135
  35. {arize_phoenix-8.0.1.dist-info → arize_phoenix-8.2.0.dist-info}/WHEEL +0 -0
  36. {arize_phoenix-8.0.1.dist-info → arize_phoenix-8.2.0.dist-info}/entry_points.txt +0 -0
  37. {arize_phoenix-8.0.1.dist-info → arize_phoenix-8.2.0.dist-info}/licenses/IP_NOTICE +0 -0
  38. {arize_phoenix-8.0.1.dist-info → arize_phoenix-8.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,9 @@
1
1
  import json
2
- from collections.abc import Mapping, Sized
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
- id_attr: NodeID[int]
108
- db_span: strawberry.Private[models.Span]
109
- name: str
110
- status_code: SpanStatusCode
111
- status_message: str
112
- start_time: datetime
113
- end_time: Optional[datetime]
114
- latency_ms: Optional[float]
115
- parent_id: Optional[ID] = strawberry.field(
116
- description="the parent span ID. If null, it is a root span"
117
- )
118
- span_kind: SpanKind
119
- context: SpanContext
120
- attributes: str = strawberry.field(
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
- metadata: Optional[str] = strawberry.field(
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
- num_documents: Optional[int]
127
- token_count_total: Optional[int]
128
- token_count_prompt: Optional[int]
129
- token_count_completion: Optional[int]
130
- input: Optional[SpanIOValue]
131
- output: Optional[SpanIOValue]
132
- events: list[SpanEvent]
133
- cumulative_token_count_total: Optional[int] = strawberry.field(
134
- description="Cumulative (prompt plus completion) token count from "
135
- "self and all descendant spans (children, grandchildren, etc.)",
136
- )
137
- cumulative_token_count_prompt: Optional[int] = strawberry.field(
138
- description="Cumulative (prompt) token count from self and all "
139
- "descendant spans (children, grandchildren, etc.)",
140
- )
141
- cumulative_token_count_completion: Optional[int] = strawberry.field(
142
- description="Cumulative (completion) token count from self and all "
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
- propagated_status_code: SpanStatusCode = strawberry.field(
146
- description="Propagated status code that percolates up error status "
147
- "codes from descendant spans (children, grandchildren, etc.)",
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.id_attr
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(self, info: Info[Context, None]) -> list[DocumentEvaluation]:
182
- return await info.context.data_loaders.document_evaluations.load(self.id_attr)
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
- if not self.num_documents:
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.id_attr, evaluation_name or None, self.num_documents),
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
- span_id = str(self.context.span_id)
206
- spans = await info.context.data_loaders.span_descendants.load(span_id)
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(self, info: Info[Context, None]) -> SpanAsExampleRevision:
213
- span = self.db_span
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.id_attr
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(self, info: Info[Context, None]) -> bool:
253
- examples = await info.context.data_loaders.span_dataset_examples.load(self.id_attr)
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(self, info: Info[Context, None]) -> list[InvocationParameter]:
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.db_span
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
- value: str
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
- @strawberry.field(
12
- description="Truncate value up to `chars` characters, appending '...' if truncated.",
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