arize-phoenix 11.28.0__py3-none-any.whl → 11.30.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 (23) hide show
  1. {arize_phoenix-11.28.0.dist-info → arize_phoenix-11.30.0.dist-info}/METADATA +3 -2
  2. {arize_phoenix-11.28.0.dist-info → arize_phoenix-11.30.0.dist-info}/RECORD +23 -22
  3. phoenix/server/api/dataloaders/average_experiment_run_latency.py +17 -24
  4. phoenix/server/api/dataloaders/document_evaluations.py +3 -3
  5. phoenix/server/api/dataloaders/experiment_annotation_summaries.py +88 -34
  6. phoenix/server/api/dataloaders/experiment_error_rates.py +21 -28
  7. phoenix/server/api/routers/v1/__init__.py +2 -0
  8. phoenix/server/api/routers/v1/documents.py +175 -0
  9. phoenix/server/api/routers/v1/spans.py +5 -3
  10. phoenix/server/api/routers/v1/traces.py +5 -3
  11. phoenix/server/api/types/Dataset.py +69 -21
  12. phoenix/server/api/types/Evaluation.py +5 -4
  13. phoenix/server/api/types/Experiment.py +4 -5
  14. phoenix/server/api/types/Span.py +2 -2
  15. phoenix/server/static/.vite/manifest.json +9 -9
  16. phoenix/server/static/assets/{components-C3HQDu_r.js → components-BBwXqJXQ.js} +4 -3
  17. phoenix/server/static/assets/{index-C-sjZRYC.js → index-C_gU3x10.js} +1 -1
  18. phoenix/server/static/assets/{pages-DvrxSPg3.js → pages-YmQb55Uo.js} +399 -392
  19. phoenix/version.py +1 -1
  20. {arize_phoenix-11.28.0.dist-info → arize_phoenix-11.30.0.dist-info}/WHEEL +0 -0
  21. {arize_phoenix-11.28.0.dist-info → arize_phoenix-11.30.0.dist-info}/entry_points.txt +0 -0
  22. {arize_phoenix-11.28.0.dist-info → arize_phoenix-11.30.0.dist-info}/licenses/IP_NOTICE +0 -0
  23. {arize_phoenix-11.28.0.dist-info → arize_phoenix-11.30.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arize-phoenix
3
- Version: 11.28.0
3
+ Version: 11.30.0
4
4
  Summary: AI Observability and Evaluation
5
5
  Project-URL: Documentation, https://arize.com/docs/phoenix/
6
6
  Project-URL: Issues, https://github.com/Arize-ai/phoenix/issues
@@ -50,7 +50,8 @@ Requires-Dist: python-multipart
50
50
  Requires-Dist: scikit-learn
51
51
  Requires-Dist: scipy
52
52
  Requires-Dist: sqlalchemy[asyncio]<3,>=2.0.4
53
- Requires-Dist: sqlean-py>=3.45.1
53
+ Requires-Dist: sqlean-py<3.50,>=3.45.1; platform_system == 'Windows'
54
+ Requires-Dist: sqlean-py>=3.45.1; platform_system != 'Windows'
54
55
  Requires-Dist: starlette
55
56
  Requires-Dist: strawberry-graphql==0.270.1
56
57
  Requires-Dist: tqdm
@@ -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=ngkyKGVatX3cO2WJdo2hKdaVKP-xJCMvqthvga6kJss,5196
8
8
  phoenix/settings.py,sha256=2kHfT3BNOVd4dAO1bq-syEQbHSG8oX2-7NhOwK2QREk,896
9
- phoenix/version.py,sha256=9BbsyqXMqD5D5cPHbfNkmPeW-WTp3yZaVegN3LRUWSU,24
9
+ phoenix/version.py,sha256=JHzYn5SQjWvqEZWixlgmU5X8oFvAzqJf2SdpbNxVVK4,24
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=qBFraOtmwCCnWJltKNP18DDG0mULXigytlFsa6YOz6k,4837
@@ -120,14 +120,14 @@ phoenix/server/api/utils.py,sha256=quCBRcusc6PUq9tJq7M8PgwFZp7nXgVAxtbw8feribY,8
120
120
  phoenix/server/api/dataloaders/__init__.py,sha256=ddiX1BdbyGkPTzMZNo-hkF_2kqIquelBUFvQejnAJYk,6834
121
121
  phoenix/server/api/dataloaders/annotation_configs_by_project.py,sha256=_Nfiug9o01JimU3Z0LpZJ0uaMCjchXomyt_dYAxPFRY,1178
122
122
  phoenix/server/api/dataloaders/annotation_summaries.py,sha256=0b23-bucBKyL25RWb2QzCNJjQzrq403qMmHKUVD5W4M,14377
123
- phoenix/server/api/dataloaders/average_experiment_run_latency.py,sha256=GLFoFAbztOH-0FVzzZ77mATIO63UcjB50j3qXiNi-tE,1811
123
+ phoenix/server/api/dataloaders/average_experiment_run_latency.py,sha256=_wEcC47zOtLFMYTfyaFWA93TlAVC3kAPZhMnRO-xB24,1809
124
124
  phoenix/server/api/dataloaders/dataset_example_revisions.py,sha256=xF7M2dg3UmjhdCrscnztCIBBI0cg3RF48IIqvilpc18,4623
125
125
  phoenix/server/api/dataloaders/dataset_example_spans.py,sha256=z_MFquqAcJ9wat7BBp7MVeJ9BYuu2EZEdaog52iWDno,1390
126
126
  phoenix/server/api/dataloaders/document_evaluation_summaries.py,sha256=9fdROnzp-mymggHwNvpRkCk93LUFxxLy55-j3HP_2HY,5565
127
- phoenix/server/api/dataloaders/document_evaluations.py,sha256=KCnCItJ2DQOCHvxFp1KK2AStPN1akGlAGOEzEAu6c6I,1246
127
+ phoenix/server/api/dataloaders/document_evaluations.py,sha256=W1b7TIlmPG61vR7kEcLZ5hAQYfkSKZAgrJOXYkA9-Ko,1246
128
128
  phoenix/server/api/dataloaders/document_retrieval_metrics.py,sha256=37EcAW7oYQuWYHMDHb0wcqbWj9lhSskvzDO7NJbT5Js,4136
129
- phoenix/server/api/dataloaders/experiment_annotation_summaries.py,sha256=fFEjpJzUOhu_cJKQ-YnwEvtn8NDl6bPs-moERe-Bp04,2767
130
- phoenix/server/api/dataloaders/experiment_error_rates.py,sha256=V4U_y16LwfBDksTZ4QP1dDALGSsQAQ1KcttKRE6lXto,1920
129
+ phoenix/server/api/dataloaders/experiment_annotation_summaries.py,sha256=CFVj7DwFYj330FLU5w3zEr12AGUX1e8ZX0X5buxMuEk,5643
130
+ phoenix/server/api/dataloaders/experiment_error_rates.py,sha256=06IZF07qt2y167DBM49QkSNdnphPArhcsgYFcunaL-U,1992
131
131
  phoenix/server/api/dataloaders/experiment_run_annotations.py,sha256=uJ--9Ue4tnmZYH9Zy-Cj2Y2t3xzs2xUqN8Y6EGe1MCo,1296
132
132
  phoenix/server/api/dataloaders/experiment_run_counts.py,sha256=j_7229IL705p_TycxIMYylhSgLHsAn91TiGPYXtOuQ8,1617
133
133
  phoenix/server/api/dataloaders/experiment_sequence_number.py,sha256=zM_f78fnqhppLtevrx9iISQSN7w_BNeXT9CoX8jYgAI,1534
@@ -255,10 +255,11 @@ phoenix/server/api/routers/auth.py,sha256=PKGwWdw7O015KmjMY1mIxlvXeU7OrmqIPF5TWT
255
255
  phoenix/server/api/routers/embeddings.py,sha256=BpZGJee0pdL0W5Rp1L0b30dEtZTgJeVqXky8LgZ0ZXw,898
256
256
  phoenix/server/api/routers/oauth2.py,sha256=rPcKFvfijzBYLjfwbCNzCn0ihn4wGWh4xh6BRqg9Ay4,24524
257
257
  phoenix/server/api/routers/utils.py,sha256=M41BoH-fl37izhRuN2aX7lWm7jOC20A_3uClv9TVUUY,583
258
- phoenix/server/api/routers/v1/__init__.py,sha256=ngLMPjC7lgZxgKy_Is33KxTRnMzSqy25qTTChCVx_Mo,2696
258
+ phoenix/server/api/routers/v1/__init__.py,sha256=_CxVCs26dPuC2KygV3VzYqmoAECeFwiSTava9mxIJTE,2790
259
259
  phoenix/server/api/routers/v1/annotation_configs.py,sha256=xp5lJmKYlRsINCUrRD9-lTAElw2v4hdFndS5BWrxICA,16048
260
260
  phoenix/server/api/routers/v1/annotations.py,sha256=fVl2qeh_ZbWXGvFBTZgeL7aGkkINIScdjuyxnOoSzNM,6817
261
261
  phoenix/server/api/routers/v1/datasets.py,sha256=9iPORLmbOrPKgUUcRDMs6ZczSIz7hvc6bngJy3IbdR0,38331
262
+ phoenix/server/api/routers/v1/documents.py,sha256=D8Pg6lEBHzSuPEDVts__X0ArIKBdQs_3gtIgoZXk_eU,6930
262
263
  phoenix/server/api/routers/v1/evaluations.py,sha256=aBrPO-xCAWyTxydaHq7W2wQFm65k89uVR-H3VWsd6WQ,13062
263
264
  phoenix/server/api/routers/v1/experiment_evaluations.py,sha256=DZ3UK9OoYKElpRcEER7559-KiAqWr-1IXpZ27FbfP3k,5249
264
265
  phoenix/server/api/routers/v1/experiment_runs.py,sha256=LZeCQWQIEOZ9jK5Gp_C4JbiYY6AmnnWe85cVcvdkCLE,7107
@@ -266,8 +267,8 @@ phoenix/server/api/routers/v1/experiments.py,sha256=hIBecGACzGZEgl93ap_JV52pUv-I
266
267
  phoenix/server/api/routers/v1/models.py,sha256=p3gJN-9SWiUYTUTft4bZMsZVCBNTb4nN1Foy68eRZzQ,1997
267
268
  phoenix/server/api/routers/v1/projects.py,sha256=XR6uJxHXXtC1q8LNyS9W6iaj440sv1OKCu-OSBfxEys,12824
268
269
  phoenix/server/api/routers/v1/prompts.py,sha256=chRYcLkOYDJdJfVZVukVTUyIRnLPvsJCg41CuPxOIU8,26695
269
- phoenix/server/api/routers/v1/spans.py,sha256=roDDE0RDBGSCircPgzwYWhwwyK33IaW7YliWN-lwxWw,49385
270
- phoenix/server/api/routers/v1/traces.py,sha256=ho6SXJ0R3g1ROxgtd7hNPo8QSZnA85oG0dgWDokHM2w,11365
270
+ phoenix/server/api/routers/v1/spans.py,sha256=6wu8nUQNp9ma_k5XGvcVx3fq5xPuaN5sbv15ouBWcVc,49438
271
+ phoenix/server/api/routers/v1/traces.py,sha256=ur4qVh8NDHDfwXKUNlAQoZhe4xAWe1Dv2ODixR5qroE,11418
271
272
  phoenix/server/api/routers/v1/users.py,sha256=eO8zMtGU33Td2_G1l9D7Z0a4CG1CwBUCj_Z9z2uk7wg,12089
272
273
  phoenix/server/api/routers/v1/utils.py,sha256=oXIOGPzPTkE0ZWUTRCoRIQQ7wTzoSwtWFaUSjlGBqts,4960
273
274
  phoenix/server/api/types/Annotation.py,sha256=gsl8CwjIbDUbZRj4d9USwZ_w_Tkz4i7zuZh9ftV80jA,1132
@@ -284,7 +285,7 @@ phoenix/server/api/types/CostBreakdown.py,sha256=yw9dlb0blGIB_dWNP8yEvDHJztHjpiV
284
285
  phoenix/server/api/types/CreateDatasetPayload.py,sha256=R-6zCmuD0f76RU9Giu78xwTHlASQs6Aq8yzvX1Kxc3g,140
285
286
  phoenix/server/api/types/CronExpression.py,sha256=R7oxuSSX_eTUHQWaoaSueQqWDmkkHr5dBKRN6q-6ROk,331
286
287
  phoenix/server/api/types/DataQualityMetric.py,sha256=Aieg3bHeBFaAf4mqeRcH1zT04sXAtQD8ATSHJt7FaBQ,1538
287
- phoenix/server/api/types/Dataset.py,sha256=StVJmOE996Citau11JtFTmcgLqvN9IeZsHAbe-Y1gkg,12933
288
+ phoenix/server/api/types/Dataset.py,sha256=23dst_glr7kFNC62-q6D9H2hJgrfZnGe7V-Bg72SJgg,15303
288
289
  phoenix/server/api/types/DatasetExample.py,sha256=_9byxGpXfYb-hmFMUJeG7Bw1wsRKSJaHwF2IPAbFpFw,3115
289
290
  phoenix/server/api/types/DatasetExampleRevision.py,sha256=c-jWR6dTguEZTm54IMlFr0Ic84I3nefyDnZb7nF5hnI,874
290
291
  phoenix/server/api/types/DatasetValues.py,sha256=7VbCOLlzOXpZN80-zYF2UGuafRcPsZF-8WQNc0YsKFc,1119
@@ -298,12 +299,12 @@ phoenix/server/api/types/DocumentEvaluationSummary.py,sha256=dx4Btlfw9_XsfmibjfW
298
299
  phoenix/server/api/types/DocumentRetrievalMetrics.py,sha256=amkpC3H5IU5-9GvO0telpbq00m6lIcv_2v446OpwFwc,1822
299
300
  phoenix/server/api/types/EmbeddingDimension.py,sha256=AYvpZ1nWINAgN4BAZsA_xI_2TNFK6h5jmqzvkPs651M,19428
300
301
  phoenix/server/api/types/EmbeddingMetadata.py,sha256=fJvNNYCbkf3SJalArLy9rcBq9Uj1SNac60zjqe1PFnM,461
301
- phoenix/server/api/types/Evaluation.py,sha256=jPUH4fUFTpQHmiHA4zS4o3zigYp2LNX7VhlT580HJ_I,1301
302
+ phoenix/server/api/types/Evaluation.py,sha256=qkMmq5G9pgwUNOPSTRwR-nNAoqLT3LlXrq7O8cAgXQ8,1356
302
303
  phoenix/server/api/types/EvaluationSummary.py,sha256=vILYejnfPvMwWEXOwhQZsANvYe3AdO2OkMR2rcgp1H4,1512
303
304
  phoenix/server/api/types/Event.py,sha256=iYt_Jx1Roioo0vZ0iPeJTHcTu6NSm4ilVMJ-IMUHAKk,3970
304
305
  phoenix/server/api/types/EventMetadata.py,sha256=-J0tYF9eZTHwCjwxQHY7Gckr2_MNW5OoWT1mydweZNM,635
305
306
  phoenix/server/api/types/ExampleRevisionInterface.py,sha256=gV3Gt9-3Oi5wjaVtepC6nOt3FzTzZFD1KebNnqiw56E,294
306
- phoenix/server/api/types/Experiment.py,sha256=8Hd-8-4Rcym95sPHIBLyTfMuRdyl50YxlKO_fMvdNAA,7830
307
+ phoenix/server/api/types/Experiment.py,sha256=ifymI5SGqotIo01yAx-AsUONLzvFvjAKEU8gKi2WOzA,7700
307
308
  phoenix/server/api/types/ExperimentAnnotationSummary.py,sha256=Uk3JtxIrsMoZT5tqc4nJdUOM3XegVzjUyoV3pkjNotE,256
308
309
  phoenix/server/api/types/ExperimentComparison.py,sha256=PXFcB0e8aaJ391yRsuRJr9_dvTZI1RAzF93oC_-HtxU,461
309
310
  phoenix/server/api/types/ExperimentRun.py,sha256=_fcwDLuURV0yviOlkjWAgJJwcCPdz-xGR6VX3UKf73s,6541
@@ -337,7 +338,7 @@ phoenix/server/api/types/ScalarDriftMetricEnum.py,sha256=IUAcRPpgL41WdoIgK6cNk2T
337
338
  phoenix/server/api/types/Segments.py,sha256=vT2v0efoa5cuBKxLtxTnsUP5YJJCZfTloM71Spu0tMI,2915
338
339
  phoenix/server/api/types/ServerStatus.py,sha256=t92OHuVhK9DXDk2vsBuHceQNKqYGpHwUp8DNGKz2wOk,88
339
340
  phoenix/server/api/types/SortDir.py,sha256=OUpXhlCzCxPoXSDkJJygEs9Rw9pMymfaZUG5zPTrw4Y,152
340
- phoenix/server/api/types/Span.py,sha256=6vurLa8yoBs7GH7Jpj5niPAX7JxKPKVUp2qEJbaZxCI,32000
341
+ phoenix/server/api/types/Span.py,sha256=ag5qWOEbWDCNUAkSUd0K3biE_PuGT2oweL4gVKElNis,32000
341
342
  phoenix/server/api/types/SpanAnnotation.py,sha256=uPWu7Z8rmpfKhaaxbged4_o00pPCR3nkn7Gji9vB8jY,1959
342
343
  phoenix/server/api/types/SpanCostDetailSummaryEntry.py,sha256=RXAdOC6MFyR9mwaoj8lMMdI3_9r3z6mR2izJvlsj12U,252
343
344
  phoenix/server/api/types/SpanCostSummary.py,sha256=wo03FCMcFzB5m4P5kvA5jzi9ACLbht38ozQbDJUh94g,357
@@ -392,10 +393,10 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
392
393
  phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
393
394
  phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
394
395
  phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
395
- phoenix/server/static/.vite/manifest.json,sha256=YLmkhNaP8CIb58FZA_WbstLTHmeLxO7Bf95-j2o1iwg,2328
396
- phoenix/server/static/assets/components-C3HQDu_r.js,sha256=KTCwdIg8fU5Xo3nRecbeh-LQqPr3s_T9LIEzYVbz8LE,664499
397
- phoenix/server/static/assets/index-C-sjZRYC.js,sha256=Y51REgta4DEkMYGcas1FLL1as0OyUOyEM1fFtRTxazc,63396
398
- phoenix/server/static/assets/pages-DvrxSPg3.js,sha256=LNxKvwIv55HqiVUdtSyiFcnS3qMSHUf0bBXOR0whd0k,1266943
396
+ phoenix/server/static/.vite/manifest.json,sha256=podIkdilPnaQGOQjbnF7zpOylgP4MXrSImpB5UGvNBc,2328
397
+ phoenix/server/static/assets/components-BBwXqJXQ.js,sha256=U8GMAX0TEjXRzhmqCMXjnVaQ9trJDICWTq9IcjqcNhE,664533
398
+ phoenix/server/static/assets/index-C_gU3x10.js,sha256=ouZyJslt5vOJGBHCHiO0ziM9Y5lTwAdB86lGFUbr-Ec,63396
399
+ phoenix/server/static/assets/pages-YmQb55Uo.js,sha256=tPGWJ9VTxJJSVoyQj_7VutaInS_SuhuXSal0nuJQTKQ,1269456
399
400
  phoenix/server/static/assets/vendor-CqDb5u4o.css,sha256=zIyFiNJKxMaQk8AvtLgt1rR01oO10d1MFndSDKH9Clw,5517
400
401
  phoenix/server/static/assets/vendor-RdRDaQiR.js,sha256=oTxLetZZXJ20yoKNAYExto9V73y8X5zjddWV46K9CWM,2595492
401
402
  phoenix/server/static/assets/vendor-arizeai-DsYDNOqt.js,sha256=0HIkPJXbKTh85nqphdAXYeStRzdaim0IQxRXiXxa21U,121514
@@ -442,9 +443,9 @@ phoenix/utilities/project.py,sha256=auVpARXkDb-JgeX5f2aStyFIkeKvGwN9l7qrFeJMVxI,
442
443
  phoenix/utilities/re.py,sha256=6YyUWIkv0zc2SigsxfOWIHzdpjKA_TZo2iqKq7zJKvw,2081
443
444
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
444
445
  phoenix/utilities/template_formatters.py,sha256=gh9PJD6WEGw7TEYXfSst1UR4pWWwmjxMLrDVQ_CkpkQ,2779
445
- arize_phoenix-11.28.0.dist-info/METADATA,sha256=13C-DWFlaybHe2koaiC7DFxTwuJ93qLXMo9pxK8nkXM,31634
446
- arize_phoenix-11.28.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
447
- arize_phoenix-11.28.0.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
448
- arize_phoenix-11.28.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
449
- arize_phoenix-11.28.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
450
- arize_phoenix-11.28.0.dist-info/RECORD,,
446
+ arize_phoenix-11.30.0.dist-info/METADATA,sha256=p2LDUgcsaZ4l5qRVqP5FVFjrpVpZTvzTN60j913wTtM,31733
447
+ arize_phoenix-11.30.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
448
+ arize_phoenix-11.30.0.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
449
+ arize_phoenix-11.30.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
450
+ arize_phoenix-11.30.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
451
+ arize_phoenix-11.30.0.dist-info/RECORD,,
@@ -23,32 +23,25 @@ class AverageExperimentRunLatencyDataLoader(DataLoader[Key, Result]):
23
23
 
24
24
  async def _load_fn(self, keys: list[Key]) -> list[Result]:
25
25
  experiment_ids = keys
26
- resolved_experiment_ids = (
27
- select(models.Experiment.id)
28
- .where(models.Experiment.id.in_(set(experiment_ids)))
29
- .subquery()
30
- )
31
- query = (
26
+ average_repetition_latency_ms = (
32
27
  select(
33
- resolved_experiment_ids.c.id,
34
- func.avg(
35
- func.extract("epoch", models.ExperimentRun.end_time)
36
- - func.extract("epoch", models.ExperimentRun.start_time)
37
- ),
28
+ models.ExperimentRun.experiment_id.label("experiment_id"),
29
+ func.avg(models.ExperimentRun.latency_ms).label("average_repetition_latency_ms"),
38
30
  )
39
- .outerjoin_from(
40
- from_=resolved_experiment_ids,
41
- target=models.ExperimentRun,
42
- onclause=resolved_experiment_ids.c.id == models.ExperimentRun.experiment_id,
43
- )
44
- .group_by(resolved_experiment_ids.c.id)
31
+ .select_from(models.ExperimentRun)
32
+ .where(models.ExperimentRun.experiment_id.in_(experiment_ids))
33
+ .group_by(models.ExperimentRun.dataset_example_id, models.ExperimentRun.experiment_id)
34
+ .subquery()
45
35
  )
36
+ query = select(
37
+ average_repetition_latency_ms.c.experiment_id,
38
+ func.avg(average_repetition_latency_ms.c.average_repetition_latency_ms).label(
39
+ "average_run_latency_ms"
40
+ ),
41
+ ).group_by(average_repetition_latency_ms.c.experiment_id)
46
42
  async with self._db() as session:
47
- avg_latencies = {
48
- experiment_id: avg_latency
49
- async for experiment_id, avg_latency in await session.stream(query)
43
+ average_run_latencies_ms = {
44
+ experiment_id: average_run_latency_ms
45
+ async for experiment_id, average_run_latency_ms in await session.stream(query)
50
46
  }
51
- return [
52
- avg_latencies.get(experiment_id, ValueError(f"Unknown experiment: {experiment_id}"))
53
- for experiment_id in keys
54
- ]
47
+ return [average_run_latencies_ms.get(experiment_id) for experiment_id in keys]
@@ -5,11 +5,11 @@ from strawberry.dataloader import DataLoader
5
5
  from typing_extensions import TypeAlias
6
6
 
7
7
  from phoenix.db import models
8
- from phoenix.server.api.types.Evaluation import DocumentEvaluation
8
+ from phoenix.server.api.types.Evaluation import DocumentAnnotation
9
9
  from phoenix.server.types import DbSessionFactory
10
10
 
11
11
  Key: TypeAlias = int
12
- Result: TypeAlias = list[DocumentEvaluation]
12
+ Result: TypeAlias = list[DocumentAnnotation]
13
13
 
14
14
 
15
15
  class DocumentEvaluationsDataLoader(DataLoader[Key, Result]):
@@ -26,6 +26,6 @@ class DocumentEvaluationsDataLoader(DataLoader[Key, Result]):
26
26
  )
27
27
  async for document_evaluation in data:
28
28
  document_evaluations_by_id[document_evaluation.span_rowid].append(
29
- DocumentEvaluation.from_sql_document_annotation(document_evaluation)
29
+ DocumentAnnotation.from_sql_document_annotation(document_evaluation)
30
30
  )
31
31
  return [document_evaluations_by_id[key] for key in keys]
@@ -2,7 +2,7 @@ from collections import defaultdict
2
2
  from dataclasses import dataclass
3
3
  from typing import Optional
4
4
 
5
- from sqlalchemy import func, select
5
+ from sqlalchemy import and_, func, select
6
6
  from strawberry.dataloader import AbstractCache, DataLoader
7
7
  from typing_extensions import TypeAlias
8
8
 
@@ -37,43 +37,97 @@ class ExperimentAnnotationSummaryDataLoader(DataLoader[Key, Result]):
37
37
  async def _load_fn(self, keys: list[Key]) -> list[Result]:
38
38
  experiment_ids = keys
39
39
  summaries: defaultdict[ExperimentID, Result] = defaultdict(list)
40
+ repetition_mean_scores_by_example_subquery = (
41
+ select(
42
+ models.ExperimentRun.experiment_id.label("experiment_id"),
43
+ models.ExperimentRunAnnotation.name.label("annotation_name"),
44
+ func.avg(models.ExperimentRunAnnotation.score).label("mean_repetition_score"),
45
+ )
46
+ .select_from(models.ExperimentRunAnnotation)
47
+ .join(
48
+ models.ExperimentRun,
49
+ models.ExperimentRunAnnotation.experiment_run_id == models.ExperimentRun.id,
50
+ )
51
+ .where(models.ExperimentRun.experiment_id.in_(experiment_ids))
52
+ .group_by(
53
+ models.ExperimentRun.experiment_id,
54
+ models.ExperimentRun.dataset_example_id,
55
+ models.ExperimentRunAnnotation.name,
56
+ )
57
+ .subquery()
58
+ .alias("repetition_mean_scores_by_example")
59
+ )
60
+ repetition_mean_scores_subquery = (
61
+ select(
62
+ repetition_mean_scores_by_example_subquery.c.experiment_id.label("experiment_id"),
63
+ repetition_mean_scores_by_example_subquery.c.annotation_name.label(
64
+ "annotation_name"
65
+ ),
66
+ func.avg(repetition_mean_scores_by_example_subquery.c.mean_repetition_score).label(
67
+ "mean_score"
68
+ ),
69
+ )
70
+ .select_from(repetition_mean_scores_by_example_subquery)
71
+ .group_by(
72
+ repetition_mean_scores_by_example_subquery.c.experiment_id,
73
+ repetition_mean_scores_by_example_subquery.c.annotation_name,
74
+ )
75
+ .subquery()
76
+ .alias("repetition_mean_scores")
77
+ )
78
+ repetitions_subquery = (
79
+ select(
80
+ models.ExperimentRun.experiment_id.label("experiment_id"),
81
+ models.ExperimentRunAnnotation.name.label("annotation_name"),
82
+ func.min(models.ExperimentRunAnnotation.score).label("min_score"),
83
+ func.max(models.ExperimentRunAnnotation.score).label("max_score"),
84
+ func.count().label("count"),
85
+ func.count(models.ExperimentRunAnnotation.error).label("error_count"),
86
+ )
87
+ .select_from(models.ExperimentRunAnnotation)
88
+ .join(
89
+ models.ExperimentRun,
90
+ models.ExperimentRunAnnotation.experiment_run_id == models.ExperimentRun.id,
91
+ )
92
+ .where(models.ExperimentRun.experiment_id.in_(experiment_ids))
93
+ .group_by(models.ExperimentRun.experiment_id, models.ExperimentRunAnnotation.name)
94
+ .subquery()
95
+ )
96
+ run_scores_query = (
97
+ select(
98
+ repetition_mean_scores_subquery.c.experiment_id.label("experiment_id"),
99
+ repetition_mean_scores_subquery.c.annotation_name.label("annotation_name"),
100
+ repetition_mean_scores_subquery.c.mean_score.label("mean_score"),
101
+ repetitions_subquery.c.min_score.label("min_score"),
102
+ repetitions_subquery.c.max_score.label("max_score"),
103
+ repetitions_subquery.c.count.label("count_"),
104
+ repetitions_subquery.c.error_count.label("error_count"),
105
+ )
106
+ .select_from(repetition_mean_scores_subquery)
107
+ .join(
108
+ repetitions_subquery,
109
+ and_(
110
+ repetitions_subquery.c.experiment_id
111
+ == repetition_mean_scores_subquery.c.experiment_id,
112
+ repetitions_subquery.c.annotation_name
113
+ == repetition_mean_scores_subquery.c.annotation_name,
114
+ ),
115
+ )
116
+ .order_by(repetition_mean_scores_subquery.c.annotation_name)
117
+ )
40
118
  async with self._db() as session:
41
- async for (
42
- experiment_id,
43
- annotation_name,
44
- min_score,
45
- max_score,
46
- mean_score,
47
- count,
48
- error_count,
49
- ) in await session.stream(
50
- select(
51
- models.ExperimentRun.experiment_id,
52
- models.ExperimentRunAnnotation.name,
53
- func.min(models.ExperimentRunAnnotation.score),
54
- func.max(models.ExperimentRunAnnotation.score),
55
- func.avg(models.ExperimentRunAnnotation.score),
56
- func.count(),
57
- func.count(models.ExperimentRunAnnotation.error),
58
- )
59
- .join(
60
- models.ExperimentRun,
61
- models.ExperimentRunAnnotation.experiment_run_id == models.ExperimentRun.id,
62
- )
63
- .where(models.ExperimentRun.experiment_id.in_(experiment_ids))
64
- .group_by(models.ExperimentRun.experiment_id, models.ExperimentRunAnnotation.name)
65
- ):
66
- summaries[experiment_id].append(
119
+ async for scores_tuple in await session.stream(run_scores_query):
120
+ summaries[scores_tuple.experiment_id].append(
67
121
  ExperimentAnnotationSummary(
68
- annotation_name=annotation_name,
69
- min_score=min_score,
70
- max_score=max_score,
71
- mean_score=mean_score,
72
- count=count,
73
- error_count=error_count,
122
+ annotation_name=scores_tuple.annotation_name,
123
+ min_score=scores_tuple.min_score,
124
+ max_score=scores_tuple.max_score,
125
+ mean_score=scores_tuple.mean_score,
126
+ count=scores_tuple.count_,
127
+ error_count=scores_tuple.error_count,
74
128
  )
75
129
  )
76
130
  return [
77
131
  sorted(summaries[experiment_id], key=lambda summary: summary.annotation_name)
78
- for experiment_id in keys
132
+ for experiment_id in experiment_ids
79
133
  ]
@@ -1,6 +1,6 @@
1
1
  from typing import Optional
2
2
 
3
- from sqlalchemy import case, func, select
3
+ from sqlalchemy import func, select
4
4
  from strawberry.dataloader import DataLoader
5
5
  from typing_extensions import TypeAlias
6
6
 
@@ -23,36 +23,29 @@ class ExperimentErrorRatesDataLoader(DataLoader[Key, Result]):
23
23
 
24
24
  async def _load_fn(self, keys: list[Key]) -> list[Result]:
25
25
  experiment_ids = keys
26
- resolved_experiment_ids = (
27
- select(models.Experiment.id)
28
- .where(models.Experiment.id.in_(set(experiment_ids)))
29
- .subquery()
30
- )
31
- query = (
26
+ average_repetition_error_rates_subquery = (
32
27
  select(
33
- resolved_experiment_ids.c.id,
34
- case(
35
- (
36
- func.count(models.ExperimentRun.id) != 0,
37
- func.count(models.ExperimentRun.error)
38
- / func.count(models.ExperimentRun.id),
39
- ),
40
- else_=None,
41
- ),
28
+ models.ExperimentRun.experiment_id.label("experiment_id"),
29
+ (
30
+ func.count(models.ExperimentRun.error) / func.count(models.ExperimentRun.id)
31
+ ).label("average_repetition_error_rate"),
42
32
  )
43
- .outerjoin_from(
44
- from_=resolved_experiment_ids,
45
- target=models.ExperimentRun,
46
- onclause=resolved_experiment_ids.c.id == models.ExperimentRun.experiment_id,
47
- )
48
- .group_by(resolved_experiment_ids.c.id)
33
+ .where(models.ExperimentRun.experiment_id.in_(experiment_ids))
34
+ .group_by(models.ExperimentRun.dataset_example_id, models.ExperimentRun.experiment_id)
35
+ .subquery()
36
+ .alias("average_repetition_error_rates")
49
37
  )
38
+ average_run_error_rates_query = select(
39
+ average_repetition_error_rates_subquery.c.experiment_id,
40
+ func.avg(average_repetition_error_rates_subquery.c.average_repetition_error_rate).label(
41
+ "average_run_error_rates"
42
+ ),
43
+ ).group_by(average_repetition_error_rates_subquery.c.experiment_id)
50
44
  async with self._db() as session:
51
- error_rates = {
45
+ average_run_error_rates = {
52
46
  experiment_id: error_rate
53
- async for experiment_id, error_rate in await session.stream(query)
47
+ async for experiment_id, error_rate in await session.stream(
48
+ average_run_error_rates_query
49
+ )
54
50
  }
55
- return [
56
- error_rates.get(experiment_id, ValueError(f"Unknown experiment ID: {experiment_id}"))
57
- for experiment_id in keys
58
- ]
51
+ return [average_run_error_rates.get(experiment_id) for experiment_id in experiment_ids]
@@ -7,6 +7,7 @@ from phoenix.server.bearer_auth import is_authenticated
7
7
  from .annotation_configs import router as annotation_configs_router
8
8
  from .annotations import router as annotations_router
9
9
  from .datasets import router as datasets_router
10
+ from .documents import router as documents_router
10
11
  from .evaluations import router as evaluations_router
11
12
  from .experiment_evaluations import router as experiment_evaluations_router
12
13
  from .experiment_runs import router as experiment_runs_router
@@ -70,5 +71,6 @@ def create_v1_router(authentication_enabled: bool) -> APIRouter:
70
71
  router.include_router(evaluations_router)
71
72
  router.include_router(prompts_router)
72
73
  router.include_router(projects_router)
74
+ router.include_router(documents_router)
73
75
  router.include_router(users_router)
74
76
  return router
@@ -0,0 +1,175 @@
1
+ from datetime import datetime, timezone
2
+ from typing import Any, Literal, Optional
3
+
4
+ from fastapi import APIRouter, Depends, HTTPException, Query
5
+ from pydantic import Field
6
+ from sqlalchemy import select
7
+ from starlette.requests import Request
8
+ from starlette.status import HTTP_404_NOT_FOUND
9
+ from strawberry.relay import GlobalID
10
+
11
+ from phoenix.db import models
12
+ from phoenix.db.helpers import SupportedSQLDialect
13
+ from phoenix.db.insertion.helpers import as_kv, insert_on_conflict
14
+ from phoenix.db.insertion.types import Precursors
15
+ from phoenix.server.api.types.Evaluation import DocumentAnnotation
16
+ from phoenix.server.authorization import is_not_locked
17
+ from phoenix.server.bearer_auth import PhoenixUser
18
+ from phoenix.server.dml_event import DocumentAnnotationInsertEvent
19
+
20
+ from .models import V1RoutesBaseModel
21
+ from .spans import SpanAnnotationResult
22
+ from .utils import RequestBody, ResponseBody, add_errors_to_responses
23
+
24
+ # Since the document annotations are spans related, we place it under spans
25
+ router = APIRouter(tags=["spans"])
26
+
27
+
28
+ class SpanDocumentAnnotationData(V1RoutesBaseModel):
29
+ span_id: str = Field(description="OpenTelemetry Span ID (hex format w/o 0x prefix)")
30
+ name: str = Field(description="The name of the document annotation. E.x. relevance")
31
+ annotator_kind: Literal["LLM", "CODE", "HUMAN"] = Field(
32
+ description="The kind of annotator. E.g. llm judge, a heuristic piece of code, or a human"
33
+ )
34
+ document_position: int = Field(
35
+ description="A 0 based index of the document. E.x. the first document during retrieval is 0"
36
+ )
37
+ result: Optional[SpanAnnotationResult] = Field(
38
+ default=None, description="The score and or label of the annotation"
39
+ )
40
+ metadata: Optional[dict[str, Any]] = Field(
41
+ default=None, description="Metadata for custom values of the annotation"
42
+ )
43
+ identifier: str = Field(
44
+ default="",
45
+ description=(
46
+ "An custom ID for the annotation. "
47
+ "If provided, the annotation will be updated if it already exists."
48
+ ),
49
+ )
50
+
51
+ # Precursor here means a value to add to a queue for processing async
52
+ def as_precursor(self, *, user_id: Optional[int] = None) -> Precursors.DocumentAnnotation:
53
+ return Precursors.DocumentAnnotation(
54
+ datetime.now(timezone.utc),
55
+ self.span_id,
56
+ self.document_position,
57
+ models.DocumentAnnotation(
58
+ name=self.name,
59
+ annotator_kind=self.annotator_kind,
60
+ document_position=self.document_position,
61
+ score=self.result.score if self.result else None,
62
+ label=self.result.label if self.result else None,
63
+ explanation=self.result.explanation if self.result else None,
64
+ metadata_=self.metadata or {},
65
+ identifier=self.identifier,
66
+ source="API",
67
+ user_id=user_id,
68
+ ),
69
+ )
70
+
71
+
72
+ class AnnotateSpanDocumentsRequestBody(RequestBody[list[SpanDocumentAnnotationData]]):
73
+ pass
74
+
75
+
76
+ class InsertedSpanDocumentAnnotation(V1RoutesBaseModel):
77
+ id: str = Field(description="The ID of the inserted span document annotation")
78
+
79
+
80
+ class AnnotateSpanDocumentsResponseBody(ResponseBody[list[InsertedSpanDocumentAnnotation]]):
81
+ pass
82
+
83
+
84
+ @router.post(
85
+ "/document_annotations",
86
+ dependencies=[Depends(is_not_locked)],
87
+ operation_id="annotateSpanDocuments",
88
+ responses=add_errors_to_responses(
89
+ [
90
+ {
91
+ "status_code": HTTP_404_NOT_FOUND,
92
+ "description": "Span not found",
93
+ }
94
+ ]
95
+ ),
96
+ response_description="Span document annotation inserted successfully",
97
+ include_in_schema=True,
98
+ )
99
+ async def annotate_span_documents(
100
+ request: Request,
101
+ request_body: AnnotateSpanDocumentsRequestBody,
102
+ sync: bool = Query(
103
+ default=False, description="If set to true, the annotations are inserted synchronously."
104
+ ),
105
+ ) -> AnnotateSpanDocumentsResponseBody:
106
+ if not request_body.data:
107
+ return AnnotateSpanDocumentsResponseBody(data=[])
108
+
109
+ user_id: Optional[int] = None
110
+ if request.app.state.authentication_enabled and isinstance(request.user, PhoenixUser):
111
+ user_id = int(request.user.identity)
112
+
113
+ span_document_annotations = request_body.data
114
+
115
+ precursors = [
116
+ annotation.as_precursor(user_id=user_id) for annotation in span_document_annotations
117
+ ]
118
+ if not sync:
119
+ await request.state.enqueue(*precursors)
120
+
121
+ span_ids = {p.span_id for p in precursors}
122
+ # Account for the fact that the spans could arrive after the annotation
123
+ async with request.app.state.db() as session:
124
+ existing_spans = {
125
+ span_id: (id_, num_docs)
126
+ async for span_id, id_, num_docs in await session.stream(
127
+ select(models.Span.span_id, models.Span.id, models.Span.num_documents).filter(
128
+ models.Span.span_id.in_(span_ids)
129
+ )
130
+ )
131
+ }
132
+
133
+ missing_span_ids = span_ids - set(existing_spans.keys())
134
+ # We prefer to fail the entire operation if there are missing spans in sync mode
135
+ if missing_span_ids:
136
+ raise HTTPException(
137
+ detail=f"Spans with IDs {', '.join(missing_span_ids)} do not exist.",
138
+ status_code=HTTP_404_NOT_FOUND,
139
+ )
140
+
141
+ # Validate that document positions are within bounds
142
+ for annotation in span_document_annotations:
143
+ _, num_docs = existing_spans[annotation.span_id]
144
+ if annotation.document_position not in range(num_docs):
145
+ raise HTTPException(
146
+ detail=f"Document position {annotation.document_position} is out of bounds for "
147
+ f"span {annotation.span_id} (max: {num_docs - 1})",
148
+ status_code=422, # Unprocessable Entity
149
+ )
150
+
151
+ inserted_document_annotation_ids = []
152
+ dialect = SupportedSQLDialect(session.bind.dialect.name)
153
+ for anno in precursors:
154
+ span_rowid, _ = existing_spans[anno.span_id]
155
+ values = dict(as_kv(anno.as_insertable(span_rowid).row))
156
+ span_document_annotation_id = await session.scalar(
157
+ insert_on_conflict(
158
+ values,
159
+ dialect=dialect,
160
+ table=models.DocumentAnnotation,
161
+ unique_by=("name", "span_rowid", "identifier", "document_position"),
162
+ ).returning(models.DocumentAnnotation.id)
163
+ )
164
+ inserted_document_annotation_ids.append(span_document_annotation_id)
165
+
166
+ # We queue an event to let the application know that annotations have changed
167
+ request.state.event_queue.put(
168
+ DocumentAnnotationInsertEvent(tuple(inserted_document_annotation_ids))
169
+ )
170
+ return AnnotateSpanDocumentsResponseBody(
171
+ data=[
172
+ InsertedSpanDocumentAnnotation(id=str(GlobalID(DocumentAnnotation.__name__, str(id_))))
173
+ for id_ in inserted_document_annotation_ids
174
+ ]
175
+ )
@@ -949,9 +949,11 @@ async def annotate_spans(
949
949
  span_ids = {p.span_id for p in precursors}
950
950
  async with request.app.state.db() as session:
951
951
  existing_spans = {
952
- span.span_id: span.id
953
- async for span in await session.stream_scalars(
954
- select(models.Span).filter(models.Span.span_id.in_(span_ids))
952
+ span_id: id_
953
+ async for span_id, id_ in await session.stream(
954
+ select(models.Span.span_id, models.Span.id).filter(
955
+ models.Span.span_id.in_(span_ids)
956
+ )
955
957
  )
956
958
  }
957
959
 
@@ -193,9 +193,11 @@ async def annotate_traces(
193
193
  trace_ids = {p.trace_id for p in precursors}
194
194
  async with request.app.state.db() as session:
195
195
  existing_traces = {
196
- trace.trace_id: trace.id
197
- async for trace in await session.stream_scalars(
198
- select(models.Trace).filter(models.Trace.trace_id.in_(trace_ids))
196
+ trace_id: id_
197
+ async for trace_id, id_ in await session.stream(
198
+ select(models.Trace.trace_id, models.Trace.id).filter(
199
+ models.Trace.trace_id.in_(trace_ids)
200
+ )
199
201
  )
200
202
  }
201
203