arize-phoenix 4.30.1__py3-none-any.whl → 4.30.2__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: arize-phoenix
3
- Version: 4.30.1
3
+ Version: 4.30.2
4
4
  Summary: AI Observability and Evaluation
5
5
  Project-URL: Documentation, https://docs.arize.com/phoenix/
6
6
  Project-URL: Issues, https://github.com/Arize-ai/phoenix/issues
@@ -6,7 +6,7 @@ phoenix/exceptions.py,sha256=n2L2KKuecrdflB9MsCdAYCiSEvGJptIsfRkXMoJle7A,169
6
6
  phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
7
  phoenix/services.py,sha256=OyML4t2XGnlqF0JXA9_uccL8HslTABxep9Ci7MViKEU,5216
8
8
  phoenix/settings.py,sha256=cO-qgis_S27nHirTobYI9hHPfZH18R--WMmxNdsVUwc,273
9
- phoenix/version.py,sha256=6kuemPn1sfBumZHqXy38x_dTEfi76ATKEh9dId-Cb78,23
9
+ phoenix/version.py,sha256=DvmdNpuJDXvg0nMZADyqJJU0JkqI_2lJ8G7e_VN97L0,23
10
10
  phoenix/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  phoenix/core/embedding_dimension.py,sha256=zKGbcvwOXgLf-yrJBpQyKtd-LEOPRKHnUToyAU8Owis,87
12
12
  phoenix/core/model.py,sha256=km_a--PBHOuA337ClRw9xqhOHhrUT6Rl9pz_zV0JYkQ,4843
@@ -64,7 +64,7 @@ phoenix/metrics/retrieval_metrics.py,sha256=XFQPo66h16w7-1AJ92M1VL_BUIXIWxXHGKF_
64
64
  phoenix/metrics/timeseries.py,sha256=Cib3E0njJzi0vZpmyADvbakFQA98rIkfDaYAOmsmBz8,6277
65
65
  phoenix/metrics/wrappers.py,sha256=umZqa_5lf1wZSFe3FgzxF-qp1xbPdKD54W628GlGCUI,8392
66
66
  phoenix/otel/__init__.py,sha256=YvEiD-3aGZs9agwLNCXU34ofV3G-Q-dolfsiinOJuT0,407
67
- phoenix/otel/otel.py,sha256=pfFiqziSmTj8KepEZlVtjvSTd_SfNa6F8r9eqrOR6OQ,11829
67
+ phoenix/otel/otel.py,sha256=NMQ-5KtpfCVGVkRzcz7fBsjCFTTR7R7wHna-87gvxfQ,17065
68
68
  phoenix/otel/settings.py,sha256=Qr2-RkgLQRfLhJqtLpEkSpqns7qLjPoOvpEOTqeSohM,3026
69
69
  phoenix/pointcloud/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
70
70
  phoenix/pointcloud/clustering.py,sha256=IzcG67kJ2hPP7pcqVmKPSL_6gKRonKdOT3bCtbTOqnk,820
@@ -72,7 +72,7 @@ phoenix/pointcloud/pointcloud.py,sha256=4zAIkKs2xOUbchpj4XDAV-iPMXrfAJ15TG6rlIYG
72
72
  phoenix/pointcloud/projectors.py,sha256=zO_RrtDYSv2rqVOfIP2_9Cv11Dc8EmcZR94xhFcBYPU,1057
73
73
  phoenix/pointcloud/umap_parameters.py,sha256=3UQSjrysVOvq2V4KNpTMqNqNiK0BsTZnPBHWZ4fyJtQ,1708
74
74
  phoenix/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
- phoenix/server/app.py,sha256=7LiZiusw_yHZBVWMk8gH4tX8yFitTJ2jdyhMWVm2DdM,26817
75
+ phoenix/server/app.py,sha256=4EPqxjfhFqxzk_JS560Gm8O9_TFtJ6lXW-72CfEzyGU,26920
76
76
  phoenix/server/dml_event.py,sha256=MpjCFqljxvgb9OB5Cez9vJesb3oHb3XxXictynBfcis,2851
77
77
  phoenix/server/dml_event_handler.py,sha256=6p-PucctivelVHfO-_9zNxWZYPr_eGjDF3bKjLtc5co,8251
78
78
  phoenix/server/grpc_server.py,sha256=jllxDNkpLQxDkvej4RhTokobowbvydF-SU8gSw1MTCc,3378
@@ -82,22 +82,23 @@ phoenix/server/telemetry.py,sha256=T_2OKrxNViAeaANlNspEekg_Y5uZIFWvKAnpz8Aoqvk,2
82
82
  phoenix/server/thread_server.py,sha256=RwXQGP_QhGD7le6WB7xEygEEuwBl5Ck_Zo8xGIYGi9M,2135
83
83
  phoenix/server/types.py,sha256=S2dReLNboR2nzjRK5j3MUyUDqu6AQFD7KRwJkeKj1q4,3609
84
84
  phoenix/server/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
85
- phoenix/server/api/context.py,sha256=WuhGT2549C5Yc7pWj2S7NaPeT4a-N-_mmz-Vg5bUkI8,3637
85
+ phoenix/server/api/context.py,sha256=XLlRtIhBl3iEq7exrN_DaO0n-XyvuvdO5bLB4uDrQhI,3724
86
86
  phoenix/server/api/exceptions.py,sha256=KdAzgwNan-wQ7THDrSoeJU2k9zWQVcH6lRiB462VsRA,990
87
87
  phoenix/server/api/interceptor.py,sha256=ykDnoC_apUd-llVli3m1CW18kNSIgjz2qZ6m5JmPDu8,1294
88
88
  phoenix/server/api/queries.py,sha256=hUzeHOUWuBQ-kjXh13-d5LgJfkbB8XSpFaHJX4YXpC8,23875
89
89
  phoenix/server/api/schema.py,sha256=4L2m6QXhaV13YPTZCEZ3hqCPQFHZOy3QnJVLRYQFzpg,548
90
90
  phoenix/server/api/utils.py,sha256=Kl47G-1A7QKTDrc75BU2QK6HupsG6MWuXxy351FOfKQ,858
91
- phoenix/server/api/dataloaders/__init__.py,sha256=TrOGnU_SD_vEIxOE_dm8HrD5C2ScLFQ4xQ7f8r-E76s,3064
91
+ phoenix/server/api/dataloaders/__init__.py,sha256=D1xEKunv8ZUBEPk2eStX-JhoPR3_NaQcNOp47H-oChM,3161
92
92
  phoenix/server/api/dataloaders/annotation_summaries.py,sha256=Wv8AORZoGd5TJ4Y-em8iqJu87AMpZP7lWOTr-SML-x8,5560
93
93
  phoenix/server/api/dataloaders/average_experiment_run_latency.py,sha256=q091UmkXx37OBKh7L-GJ5LXHyRXfX2w4XTk1NMHtPpw,1827
94
- phoenix/server/api/dataloaders/dataset_example_revisions.py,sha256=rZhJoIYUGgYhXwVBtq5u0bqtHmIQ2Sh6HNnJsSGIXis,3767
94
+ phoenix/server/api/dataloaders/dataset_example_revisions.py,sha256=_X2CwCc6lAkD4VuwAiyo4DfSVS8uuzub05sKyKiEXm8,5149
95
95
  phoenix/server/api/dataloaders/dataset_example_spans.py,sha256=-TjdyyJv2c2JiN1OXu6MMmQ-BEKlHXucEDcuObeRVsU,1416
96
96
  phoenix/server/api/dataloaders/document_evaluation_summaries.py,sha256=5XOom2KRAmCwPmtlraiZOSl3vhfaW-eiiYkmetAEalw,5616
97
97
  phoenix/server/api/dataloaders/document_evaluations.py,sha256=V6sE34jON_qFxt7eArJbktykAsty-gnBZHlEkORcj0E,1296
98
98
  phoenix/server/api/dataloaders/document_retrieval_metrics.py,sha256=JqDqkUuoeG6WfcmWSrmQptfF6IPX8XgzYzyECXTAjgg,4202
99
99
  phoenix/server/api/dataloaders/experiment_annotation_summaries.py,sha256=qDEBRo0XKJMYBYlXBgJ-69jwPK-0r4pi9ZhbjC_vx6M,2813
100
100
  phoenix/server/api/dataloaders/experiment_error_rates.py,sha256=wWTFOO2UxGIYUu53nnzzLk04-mJxw-BQgJELA9gT5bY,1949
101
+ phoenix/server/api/dataloaders/experiment_run_annotations.py,sha256=Ey42FYy6WIASWlO-KKwHewfvH_RBL0Ci3W3xrv4oyaE,1361
101
102
  phoenix/server/api/dataloaders/experiment_run_counts.py,sha256=8VlKRaXwD56-7q_uonRr7L6QlEkyEQubkFKmKx6vuz0,1661
102
103
  phoenix/server/api/dataloaders/experiment_sequence_number.py,sha256=hSpKyOz3ES4UTfOzOfHeQ1ZXat-bzcdKybVSbYmhrBw,1563
103
104
  phoenix/server/api/dataloaders/latency_ms_quantile.py,sha256=5Y2OQ_GeH1My2573eOm7zPbqpwDZ_WeMLoZMq3KDoNQ,7403
@@ -198,7 +199,7 @@ phoenix/server/api/types/ExampleRevisionInterface.py,sha256=gV3Gt9-3Oi5wjaVtepC6
198
199
  phoenix/server/api/types/Experiment.py,sha256=YNs8SzOMSYUKiAMANuWSv8NxSNFzSIFOBOWb_yztE0s,5482
199
200
  phoenix/server/api/types/ExperimentAnnotationSummary.py,sha256=Uk3JtxIrsMoZT5tqc4nJdUOM3XegVzjUyoV3pkjNotE,256
200
201
  phoenix/server/api/types/ExperimentComparison.py,sha256=0sFz6MoBDw39dds0qVyaqhVs9qqO5rkG1FMSjmfBeCc,441
201
- phoenix/server/api/types/ExperimentRun.py,sha256=122_SID7SLKPUq2dJ2Y4BBw40DNUtcxo6QCZuO8UbBs,2997
202
+ phoenix/server/api/types/ExperimentRun.py,sha256=LkR7fBRZ3nH57ZoHJwePTGtJYcTMM-EH5r1TVPiMyxg,2687
202
203
  phoenix/server/api/types/ExperimentRunAnnotation.py,sha256=iBxDaD9DgiF-Qymp5QyxWfJRGYXM1_CeWA_qzsZBqkI,1812
203
204
  phoenix/server/api/types/ExportedFile.py,sha256=e3GTn7B5LgsTbqiwjhMCQH7VsiqXitrBO4aCMS1lHsg,163
204
205
  phoenix/server/api/types/Functionality.py,sha256=tzV9xdhB8zqfsjWxP66NDC7EZsplYkYO7jRbLWJIeeg,382
@@ -295,8 +296,8 @@ phoenix/utilities/logging.py,sha256=lDXd6EGaamBNcQxL4vP1au9-i_SXe0OraUDiJOcszSw,
295
296
  phoenix/utilities/project.py,sha256=8IJuMM4yUMoooPi37sictGj8Etu9rGmq6RFtc9848cQ,436
296
297
  phoenix/utilities/re.py,sha256=PDve_OLjRTM8yQQJHC8-n3HdIONi7aNils3ZKRZ5uBM,2045
297
298
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
298
- arize_phoenix-4.30.1.dist-info/METADATA,sha256=dygvZQ4Y3cjF7a80jliAm0DoupHUB_N3STUVDRmL-D4,11977
299
- arize_phoenix-4.30.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
300
- arize_phoenix-4.30.1.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
301
- arize_phoenix-4.30.1.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
302
- arize_phoenix-4.30.1.dist-info/RECORD,,
299
+ arize_phoenix-4.30.2.dist-info/METADATA,sha256=qMHw9BdGbxXT2Z0TrtzXy-bv_atoD45Ohw0EF9pMu1k,11977
300
+ arize_phoenix-4.30.2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
301
+ arize_phoenix-4.30.2.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
302
+ arize_phoenix-4.30.2.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
303
+ arize_phoenix-4.30.2.dist-info/RECORD,,
phoenix/otel/otel.py CHANGED
@@ -51,9 +51,10 @@ def register(
51
51
  not provided, the `PHOENIX_PROJECT_NAME` environment variable will be used.
52
52
  batch (bool): If True, spans will be processed using a BatchSpanprocessor. If False, spans
53
53
  will be processed one at a time using a SimpleSpanProcessor.
54
- set_global_tracer_provider (bool): If False, the TracerProvider will not be set as the global
55
- tracer provider. Defaults to True.
56
- headers (dict, optional): Optional headers to include in the HTTP request to the collector.
54
+ set_global_tracer_provider (bool): If False, the TracerProvider will not be set as the
55
+ global tracer provider. Defaults to True.
56
+ headers (dict, optional): Optional headers to include in the request to the collector.
57
+ If not provided, the `PHOENIX_CLIENT_HEADERS` environment variable will be used.
57
58
  verbose (bool): If True, configuration details will be printed to stdout.
58
59
  """
59
60
 
@@ -73,7 +74,8 @@ def register(
73
74
  global_provider_msg = (
74
75
  "| \n"
75
76
  "| `register` has set this TracerProvider as the global OpenTelemetry default.\n"
76
- "| To disable this behavior, call `register` with `set_global_tracer_provider=False`.\n"
77
+ "| To disable this behavior, call `register` with "
78
+ "`set_global_tracer_provider=False`.\n"
77
79
  )
78
80
  else:
79
81
  global_provider_msg = ""
@@ -85,6 +87,22 @@ def register(
85
87
 
86
88
 
87
89
  class TracerProvider(_TracerProvider):
90
+ """
91
+ An extension of `opentelemetry.sdk.trace.TracerProvider` with Phoenix-aware defaults.
92
+
93
+ Extended keyword arguments are documented in the `Args` section. For further documentation, see
94
+ the OpenTelemetry documentation at https://opentelemetry.io/docs/specs/otel/trace/sdk/.
95
+
96
+ Args:
97
+ endpoint (str, optional): The collector endpoint to which spans will be exported. If
98
+ specified, a default SpanProcessor will be created and added to this TracerProvider.
99
+ If not provided, the `PHOENIX_OTEL_COLLECTOR_ENDPOINT` environment variable will be
100
+ used to infer which collector endpoint to use, defaults to the gRPC endpoint. When
101
+ specifying the endpoint, the transport method (HTTP or gRPC) will be inferred from the
102
+ URL.
103
+ verbose (bool): If True, configuration details will be printed to stdout.
104
+ """
105
+
88
106
  def __init__(
89
107
  self, *args: Any, endpoint: Optional[str] = None, verbose: bool = True, **kwargs: Any
90
108
  ):
@@ -112,6 +130,12 @@ class TracerProvider(_TracerProvider):
112
130
  print(self._tracing_details())
113
131
 
114
132
  def add_span_processor(self, *args: Any, **kwargs: Any) -> None:
133
+ """
134
+ Registers a new `SpanProcessor` for this `TracerProvider`.
135
+
136
+ If this `TracerProvider` has a default processor, it will be removed.
137
+ """
138
+
115
139
  if self._default_processor:
116
140
  self._active_span_processor.shutdown()
117
141
  self._active_span_processor._span_processors = tuple() # remove default processors
@@ -163,6 +187,24 @@ class TracerProvider(_TracerProvider):
163
187
 
164
188
 
165
189
  class SimpleSpanProcessor(_SimpleSpanProcessor):
190
+ """
191
+ Simple SpanProcessor implementation.
192
+
193
+ SimpleSpanProcessor is an implementation of `SpanProcessor` that passes ended spans directly to
194
+ the configured `SpanExporter`.
195
+
196
+ Args:
197
+ span_exporter (SpanExporter, optional): The `SpanExporter` to which ended spans will be
198
+ passed.
199
+ endpoint (str, optional): The collector endpoint to which spans will be exported. If not
200
+ provided, the `PHOENIX_OTEL_COLLECTOR_ENDPOINT` environment variable will be used to
201
+ infer which collector endpoint to use, defaults to the gRPC endpoint. When specifying
202
+ the endpoint, the transport method (HTTP or gRPC) will be inferred from the URL.
203
+ headers (dict, optional): Optional headers to include in the request to the collector.
204
+ If not provided, the `PHOENIX_CLIENT_HEADERS` or `OTEL_EXPORTER_OTLP_HEADERS`
205
+ environment variable will be used.
206
+ """
207
+
166
208
  def __init__(
167
209
  self,
168
210
  span_exporter: Optional[SpanExporter] = None,
@@ -182,6 +224,37 @@ class SimpleSpanProcessor(_SimpleSpanProcessor):
182
224
 
183
225
 
184
226
  class BatchSpanProcessor(_BatchSpanProcessor):
227
+ """
228
+ Batch SpanProcessor implementation.
229
+
230
+ `BatchSpanProcessor` is an implementation of `SpanProcessor` that batches ended spans and
231
+ pushes them to the configured `SpanExporter`.
232
+
233
+ `BatchSpanProcessor` is configurable with the following environment variables which correspond
234
+ to constructor parameters:
235
+
236
+ - :envvar:`OTEL_BSP_SCHEDULE_DELAY`
237
+ - :envvar:`OTEL_BSP_MAX_QUEUE_SIZE`
238
+ - :envvar:`OTEL_BSP_MAX_EXPORT_BATCH_SIZE`
239
+ - :envvar:`OTEL_BSP_EXPORT_TIMEOUT`
240
+
241
+ Args:
242
+ span_exporter (SpanExporter, optional): The `SpanExporter` to which ended spans will be
243
+ passed.
244
+ endpoint (str, optional): The collector endpoint to which spans will be exported. If not
245
+ provided, the `PHOENIX_OTEL_COLLECTOR_ENDPOINT` environment variable will be used to
246
+ infer which collector endpoint to use, defaults to the gRPC endpoint. When specifying
247
+ the endpoint, the transport method (HTTP or gRPC) will be inferred from the URL.
248
+ headers (dict, optional): Optional headers to include in the request to the collector.
249
+ If not provided, the `PHOENIX_CLIENT_HEADERS` or `OTEL_EXPORTER_OTLP_HEADERS`
250
+ environment variable will be used.
251
+ max_queue_size (int, optional): The maximum queue size.
252
+ schedule_delay_millis (float, optional): The delay between two consecutive exports in
253
+ milliseconds.
254
+ max_export_batch_size (int, optional): The maximum batch size.
255
+ export_timeout_millis (float, optional): The batch timeout in milliseconds.
256
+ """
257
+
185
258
  def __init__(
186
259
  self,
187
260
  span_exporter: Optional[SpanExporter] = None,
@@ -201,6 +274,20 @@ class BatchSpanProcessor(_BatchSpanProcessor):
201
274
 
202
275
 
203
276
  class HTTPSpanExporter(_HTTPSpanExporter):
277
+ """
278
+ OTLP span exporter using HTTP.
279
+
280
+ For more information, see:
281
+ - `opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter`
282
+
283
+ Args:
284
+ endpoint (str, optional): OpenTelemetry Collector receiver endpoint. If not provided, the
285
+ `PHOENIX_OTEL_COLLECTOR_ENDPOINT` environment variable will be used to infer which
286
+ collector endpoint to use, defaults to the HTTP endpoint.
287
+ headers: Headers to send when exporting. If not provided, the `PHOENIX_CLIENT_HEADERS`
288
+ or `OTEL_EXPORTER_OTLP_HEADERS` environment variables will be used.
289
+ """
290
+
204
291
  def __init__(self, *args: Any, **kwargs: Any):
205
292
  sig = inspect.signature(_HTTPSpanExporter)
206
293
  bound_args = sig.bind_partial(*args, **kwargs)
@@ -210,12 +297,30 @@ class HTTPSpanExporter(_HTTPSpanExporter):
210
297
  bound_args.arguments["headers"] = get_env_client_headers()
211
298
 
212
299
  if bound_args.arguments.get("endpoint") is None:
213
- _, endpoint = _normalized_endpoint(None)
300
+ _, endpoint = _normalized_endpoint(None, use_http=True)
214
301
  bound_args.arguments["endpoint"] = endpoint
215
302
  super().__init__(**bound_args.arguments)
216
303
 
217
304
 
218
305
  class GRPCSpanExporter(_GRPCSpanExporter):
306
+ """
307
+ OTLP span exporter using gRPC.
308
+
309
+ For more information, see:
310
+ - `opentelemetry.exporter.otlp.proto.grpc.trace_exporter.OTLPSpanExporter`
311
+
312
+ Args:
313
+ endpoint (str, optional): OpenTelemetry Collector receiver endpoint. If not provided, the
314
+ `PHOENIX_OTEL_COLLECTOR_ENDPOINT` environment variable will be used to infer which
315
+ collector endpoint to use, defaults to the gRPC endpoint.
316
+ insecure: Connection type
317
+ credentials: Credentials object for server authentication
318
+ headers: Headers to send when exporting. If not provided, the `PHOENIX_CLIENT_HEADERS`
319
+ or `OTEL_EXPORTER_OTLP_HEADERS` environment variables will be used.
320
+ timeout: Backend request timeout in seconds
321
+ compression: gRPC compression method to use
322
+ """
323
+
219
324
  def __init__(self, *args: Any, **kwargs: Any):
220
325
  sig = inspect.signature(_GRPCSpanExporter)
221
326
  bound_args = sig.bind_partial(*args, **kwargs)
@@ -270,12 +375,16 @@ _KNOWN_PROVIDERS = {
270
375
  }
271
376
 
272
377
 
273
- def _normalized_endpoint(endpoint: Optional[str]) -> Tuple[ParseResult, str]:
378
+ def _normalized_endpoint(
379
+ endpoint: Optional[str], use_http: bool = False
380
+ ) -> Tuple[ParseResult, str]:
274
381
  if endpoint is None:
275
382
  base_endpoint = get_env_collector_endpoint() or "http://localhost:6006"
276
383
  parsed = urlparse(base_endpoint)
277
384
  if parsed.hostname in _KNOWN_PROVIDERS:
278
385
  parsed = _KNOWN_PROVIDERS[parsed.hostname](parsed)
386
+ elif use_http:
387
+ parsed = _construct_http_endpoint(parsed)
279
388
  else:
280
389
  parsed = _construct_grpc_endpoint(parsed)
281
390
  else:
@@ -17,6 +17,7 @@ from phoenix.server.api.dataloaders import (
17
17
  DocumentRetrievalMetricsDataLoader,
18
18
  ExperimentAnnotationSummaryDataLoader,
19
19
  ExperimentErrorRatesDataLoader,
20
+ ExperimentRunAnnotations,
20
21
  ExperimentRunCountsDataLoader,
21
22
  ExperimentSequenceNumberDataLoader,
22
23
  LatencyMsQuantileDataLoader,
@@ -45,6 +46,7 @@ class DataLoaders:
45
46
  annotation_summaries: AnnotationSummaryDataLoader
46
47
  experiment_annotation_summaries: ExperimentAnnotationSummaryDataLoader
47
48
  experiment_error_rates: ExperimentErrorRatesDataLoader
49
+ experiment_run_annotations: ExperimentRunAnnotations
48
50
  experiment_run_counts: ExperimentRunCountsDataLoader
49
51
  experiment_sequence_number: ExperimentSequenceNumberDataLoader
50
52
  latency_ms_quantile: LatencyMsQuantileDataLoader
@@ -12,6 +12,7 @@ from .document_evaluations import DocumentEvaluationsDataLoader
12
12
  from .document_retrieval_metrics import DocumentRetrievalMetricsDataLoader
13
13
  from .experiment_annotation_summaries import ExperimentAnnotationSummaryDataLoader
14
14
  from .experiment_error_rates import ExperimentErrorRatesDataLoader
15
+ from .experiment_run_annotations import ExperimentRunAnnotations
15
16
  from .experiment_run_counts import ExperimentRunCountsDataLoader
16
17
  from .experiment_sequence_number import ExperimentSequenceNumberDataLoader
17
18
  from .latency_ms_quantile import LatencyMsQuantileCache, LatencyMsQuantileDataLoader
@@ -36,6 +37,7 @@ __all__ = [
36
37
  "AnnotationSummaryDataLoader",
37
38
  "ExperimentAnnotationSummaryDataLoader",
38
39
  "ExperimentErrorRatesDataLoader",
40
+ "ExperimentRunAnnotations",
39
41
  "ExperimentRunCountsDataLoader",
40
42
  "ExperimentSequenceNumberDataLoader",
41
43
  "LatencyMsQuantileDataLoader",
@@ -5,7 +5,8 @@ from typing import (
5
5
  Union,
6
6
  )
7
7
 
8
- from sqlalchemy import Integer, case, func, literal, or_, select, union
8
+ from sqlalchemy import and_, case, func, null, or_, select
9
+ from sqlalchemy.sql.expression import literal
9
10
  from strawberry.dataloader import DataLoader
10
11
  from typing_extensions import TypeAlias
11
12
 
@@ -22,40 +23,76 @@ Result: TypeAlias = DatasetExampleRevision
22
23
 
23
24
  class DatasetExampleRevisionsDataLoader(DataLoader[Key, Result]):
24
25
  def __init__(self, db: DbSessionFactory) -> None:
25
- super().__init__(load_fn=self._load_fn)
26
+ super().__init__(
27
+ load_fn=self._load_fn,
28
+ max_batch_size=200, # needed to prevent the size of the query from getting too large
29
+ )
26
30
  self._db = db
27
31
 
28
32
  async def _load_fn(self, keys: List[Key]) -> List[Union[Result, NotFound]]:
29
- # sqlalchemy has limited SQLite support for VALUES, so use UNION ALL instead.
30
- # For details, see https://github.com/sqlalchemy/sqlalchemy/issues/7228
31
- keys_subquery = union(
32
- *(
33
+ example_and_version_ids = tuple(
34
+ set(
35
+ (example_id, version_id)
36
+ for example_id, version_id in keys
37
+ if version_id is not None
38
+ )
39
+ )
40
+ versionless_example_ids = tuple(
41
+ set(example_id for example_id, version_id in keys if version_id is None)
42
+ )
43
+ resolved_example_and_version_ids = (
44
+ (
33
45
  select(
34
- literal(example_id, Integer).label("example_id"),
35
- literal(version_id, Integer).label("version_id"),
46
+ models.DatasetExample.id.label("example_id"),
47
+ models.DatasetVersion.id.label("version_id"),
48
+ )
49
+ .select_from(models.DatasetExample)
50
+ .join(
51
+ models.DatasetVersion,
52
+ onclause=literal(True), # cross join
53
+ )
54
+ .where(
55
+ or_(
56
+ *(
57
+ and_(
58
+ models.DatasetExample.id == example_id,
59
+ models.DatasetVersion.id == version_id,
60
+ )
61
+ for example_id, version_id in example_and_version_ids
62
+ )
63
+ )
36
64
  )
37
- for example_id, version_id in keys
38
65
  )
39
- ).subquery()
66
+ .union(
67
+ select(
68
+ models.DatasetExample.id.label("example_id"), null().label("version_id")
69
+ ).where(models.DatasetExample.id.in_(versionless_example_ids))
70
+ )
71
+ .subquery()
72
+ )
40
73
  revision_ids = (
41
74
  select(
42
- keys_subquery.c.example_id,
43
- keys_subquery.c.version_id,
75
+ resolved_example_and_version_ids.c.example_id,
76
+ resolved_example_and_version_ids.c.version_id,
44
77
  func.max(models.DatasetExampleRevision.id).label("revision_id"),
45
78
  )
46
- .select_from(keys_subquery)
79
+ .select_from(resolved_example_and_version_ids)
47
80
  .join(
48
81
  models.DatasetExampleRevision,
49
- onclause=keys_subquery.c.example_id
82
+ onclause=resolved_example_and_version_ids.c.example_id
50
83
  == models.DatasetExampleRevision.dataset_example_id,
51
84
  )
52
85
  .where(
53
86
  or_(
54
- keys_subquery.c.version_id.is_(None),
55
- models.DatasetExampleRevision.dataset_version_id <= keys_subquery.c.version_id,
87
+ resolved_example_and_version_ids.c.version_id.is_(None),
88
+ models.DatasetExampleRevision.dataset_version_id
89
+ <= resolved_example_and_version_ids.c.version_id,
56
90
  )
57
91
  )
58
- .group_by(keys_subquery.c.example_id, keys_subquery.c.version_id)
92
+ .group_by(
93
+ resolved_example_and_version_ids.c.example_id,
94
+ resolved_example_and_version_ids.c.version_id,
95
+ )
59
96
  ).subquery()
60
97
  query = (
61
98
  select(
@@ -0,0 +1,40 @@
1
+ from collections import defaultdict
2
+ from typing import (
3
+ DefaultDict,
4
+ List,
5
+ )
6
+
7
+ from sqlalchemy import select
8
+ from strawberry.dataloader import DataLoader
9
+ from typing_extensions import TypeAlias
10
+
11
+ from phoenix.db.models import ExperimentRunAnnotation as OrmExperimentRunAnnotation
12
+ from phoenix.server.types import DbSessionFactory
13
+
14
+ ExperimentRunID: TypeAlias = int
15
+ Key: TypeAlias = ExperimentRunID
16
+ Result: TypeAlias = List[OrmExperimentRunAnnotation]
17
+
18
+
19
+ class ExperimentRunAnnotations(DataLoader[Key, Result]):
20
+ def __init__(
21
+ self,
22
+ db: DbSessionFactory,
23
+ ) -> None:
24
+ super().__init__(load_fn=self._load_fn)
25
+ self._db = db
26
+
27
+ async def _load_fn(self, keys: List[Key]) -> List[Result]:
28
+ run_ids = tuple(set(keys))
29
+ annotations: DefaultDict[Key, Result] = defaultdict(list)
30
+ async with self._db() as session:
31
+ async for run_id, annotation in await session.stream(
32
+ select(
33
+ OrmExperimentRunAnnotation.experiment_run_id, OrmExperimentRunAnnotation
34
+ ).where(OrmExperimentRunAnnotation.experiment_run_id.in_(run_ids))
35
+ ):
36
+ annotations[run_id].append(annotation)
37
+ return [
38
+ sorted(annotations[run_id], key=lambda annotation: annotation.name, reverse=True)
39
+ for run_id in run_ids
40
+ ]
@@ -2,7 +2,6 @@ from datetime import datetime
2
2
  from typing import Optional
3
3
 
4
4
  import strawberry
5
- from sqlalchemy import select
6
5
  from strawberry import UNSET
7
6
  from strawberry.relay import Connection, GlobalID, Node, NodeID
8
7
  from strawberry.scalars import JSON
@@ -48,14 +47,7 @@ class ExperimentRun(Node):
48
47
  before=before if isinstance(before, CursorString) else None,
49
48
  )
50
49
  run_id = self.id_attr
51
- async with info.context.db() as session:
52
- annotations = (
53
- await session.scalars(
54
- select(models.ExperimentRunAnnotation)
55
- .where(models.ExperimentRunAnnotation.experiment_run_id == run_id)
56
- .order_by(models.ExperimentRunAnnotation.name.desc())
57
- )
58
- ).all()
50
+ annotations = await info.context.data_loaders.experiment_run_annotations.load(run_id)
59
51
  return connection_from_list(
60
52
  [to_gql_experiment_run_annotation(annotation) for annotation in annotations], args
61
53
  )
phoenix/server/app.py CHANGED
@@ -66,6 +66,7 @@ from phoenix.server.api.dataloaders import (
66
66
  DocumentRetrievalMetricsDataLoader,
67
67
  ExperimentAnnotationSummaryDataLoader,
68
68
  ExperimentErrorRatesDataLoader,
69
+ ExperimentRunAnnotations,
69
70
  ExperimentRunCountsDataLoader,
70
71
  ExperimentSequenceNumberDataLoader,
71
72
  LatencyMsQuantileDataLoader,
@@ -446,6 +447,7 @@ def create_graphql_router(
446
447
  ),
447
448
  experiment_annotation_summaries=ExperimentAnnotationSummaryDataLoader(db),
448
449
  experiment_error_rates=ExperimentErrorRatesDataLoader(db),
450
+ experiment_run_annotations=ExperimentRunAnnotations(db),
449
451
  experiment_run_counts=ExperimentRunCountsDataLoader(db),
450
452
  experiment_sequence_number=ExperimentSequenceNumberDataLoader(db),
451
453
  latency_ms_quantile=LatencyMsQuantileDataLoader(
phoenix/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "4.30.1"
1
+ __version__ = "4.30.2"