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

Files changed (28) hide show
  1. {arize_phoenix-4.21.0.dist-info → arize_phoenix-4.22.1.dist-info}/METADATA +1 -1
  2. {arize_phoenix-4.21.0.dist-info → arize_phoenix-4.22.1.dist-info}/RECORD +25 -25
  3. phoenix/config.py +59 -1
  4. phoenix/db/migrations/future_versions/README.md +4 -0
  5. phoenix/db/migrations/future_versions/cd164e83824f_users_and_tokens.py +292 -0
  6. phoenix/db/migrations/versions/.gitignore +1 -0
  7. phoenix/db/models.py +61 -0
  8. phoenix/experiments/functions.py +4 -4
  9. phoenix/experiments/types.py +3 -3
  10. phoenix/server/api/context.py +0 -6
  11. phoenix/server/api/dataloaders/__init__.py +0 -9
  12. phoenix/server/api/routers/v1/experiment_runs.py +7 -2
  13. phoenix/server/api/types/Evaluation.py +1 -26
  14. phoenix/server/api/types/Project.py +1 -60
  15. phoenix/server/api/types/Span.py +1 -9
  16. phoenix/server/app.py +0 -11
  17. phoenix/server/dml_event_handler.py +0 -3
  18. phoenix/server/static/.vite/manifest.json +9 -9
  19. phoenix/server/static/assets/{components-D2V-mOGq.js → components-BC3-LP_a.js} +44 -44
  20. phoenix/server/static/assets/{index-B52Z3aZG.js → index-BjJvafYL.js} +1 -1
  21. phoenix/server/static/assets/{pages-CChOjmat.js → pages--n2933VW.js} +214 -195
  22. phoenix/version.py +1 -1
  23. phoenix/server/api/dataloaders/evaluation_summaries.py +0 -149
  24. phoenix/server/api/dataloaders/span_evaluations.py +0 -35
  25. phoenix/server/api/dataloaders/trace_evaluations.py +0 -35
  26. {arize_phoenix-4.21.0.dist-info → arize_phoenix-4.22.1.dist-info}/WHEEL +0 -0
  27. {arize_phoenix-4.21.0.dist-info → arize_phoenix-4.22.1.dist-info}/licenses/IP_NOTICE +0 -0
  28. {arize_phoenix-4.21.0.dist-info → arize_phoenix-4.22.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: arize-phoenix
3
- Version: 4.21.0
3
+ Version: 4.22.1
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
@@ -1,11 +1,11 @@
1
1
  phoenix/__init__.py,sha256=TGNWqm2UW-l67yIRpOtmqGHVAmdoobSNqUsiTtip7uQ,1542
2
- phoenix/config.py,sha256=eXciIho_PDh4ZSmq4Gtuo7Qz__yTluDP3_WUwig5OiU,8141
2
+ phoenix/config.py,sha256=wYA_8GSSz5rnpfIWDjeBL9ehKuTy9jqXaMZnxUqRYEU,10131
3
3
  phoenix/datetime_utils.py,sha256=yDKjwX2Vtqw9h5F_ProtP-TsXidM43uIvmJ_pOzYc9A,3405
4
4
  phoenix/exceptions.py,sha256=n2L2KKuecrdflB9MsCdAYCiSEvGJptIsfRkXMoJle7A,169
5
5
  phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
6
6
  phoenix/services.py,sha256=aTxhcOA1pZHB6U-B3TEcp6fqDF5oT0xCUvEUNMZVTUQ,5175
7
7
  phoenix/settings.py,sha256=cO-qgis_S27nHirTobYI9hHPfZH18R--WMmxNdsVUwc,273
8
- phoenix/version.py,sha256=Nuzask72HqOtOnJvPTcGSrVdJM7YOawRe8WLFLDLIJ4,23
8
+ phoenix/version.py,sha256=o6NOndzGr8AzXGIlB0Kpj1jAyiWB77iB92rO-Vlh2y0,23
9
9
  phoenix/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  phoenix/core/embedding_dimension.py,sha256=zKGbcvwOXgLf-yrJBpQyKtd-LEOPRKHnUToyAU8Owis,87
11
11
  phoenix/core/model.py,sha256=km_a--PBHOuA337ClRw9xqhOHhrUT6Rl9pz_zV0JYkQ,4843
@@ -18,7 +18,7 @@ phoenix/db/bulk_inserter.py,sha256=qgg8pt5k4VnHKOE0-KoReXVAfXRhLt-sMZihI-b4X9I,1
18
18
  phoenix/db/engines.py,sha256=R3btYTSOSd6BwRA59EmhhojL0HCQ7NnzFIXQrPYS0iU,4812
19
19
  phoenix/db/helpers.py,sha256=2zSc4n5IJfu-CaOFoBfqTB35M1nTFcAc8tqLsNtF2Jw,3488
20
20
  phoenix/db/migrate.py,sha256=MuhtNWnR24riROvarvKfbRb4_D5xuQi6P760vBUKl1E,2270
21
- phoenix/db/models.py,sha256=1fSwI9NjXcVv7PT40VEeKGGI13TWbqzrYfIemXV-DYY,20837
21
+ phoenix/db/models.py,sha256=C_s1TAgMRi2eExc48NwInErsfSzVhNZtj4ow23jUKSc,23352
22
22
  phoenix/db/insertion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  phoenix/db/insertion/constants.py,sha256=8wifm7X-1XvroZ__R2Gc96NsgLhTDn0zXl4lehlXtcA,70
24
24
  phoenix/db/insertion/dataset.py,sha256=_vxy5e6W5jEuvO2fMKbbNCn9JvHkwI4LRKk_10eKFVg,7171
@@ -32,13 +32,16 @@ phoenix/db/insertion/types.py,sha256=nQYYnpzcPxj2kdUoXfKE8ilOKlx1zpKLPc40OGuBlfk
32
32
  phoenix/db/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  phoenix/db/migrations/env.py,sha256=QbzB5zrRs6XQQmrYeUpuzeilcMlM-MsbaAgHHYcIHTI,3626
34
34
  phoenix/db/migrations/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
35
+ phoenix/db/migrations/future_versions/README.md,sha256=3QtDx40SAD-IITjbdlKR2N_CBxT5y37C1OQs05EDt7o,184
36
+ phoenix/db/migrations/future_versions/cd164e83824f_users_and_tokens.py,sha256=L3UlrrT9_mgIakkjRl4MfSHd5dNmZi0D0jizi5V4dTI,8795
37
+ phoenix/db/migrations/versions/.gitignore,sha256=chLdMrfkICZvLY7lCEcuqF32sVp61Jml4PodFryEU94,33
35
38
  phoenix/db/migrations/versions/10460e46d750_datasets.py,sha256=eZAyz720DmpOd7RnuxDN2dVNXVuMrdlCA7eAOxyMtfs,8695
36
39
  phoenix/db/migrations/versions/3be8647b87d8_add_token_columns_to_spans_table.py,sha256=x6oKFwn7Zmite4G0trDQPpMCn0I7jejuBcN3-ivEuDg,3938
37
40
  phoenix/db/migrations/versions/cf03bd6bae1d_init.py,sha256=09cpofqje8zi4eQFfUn-i21x7VcsUYOfLKKUlrtKrGc,8662
38
41
  phoenix/experiments/__init__.py,sha256=6JGwgUd7xCbGpuHqYZlsmErmYvVgv7N_j43bn3dUqsk,123
39
- phoenix/experiments/functions.py,sha256=4XaOLE1Co9sW_yjM1sypQClmOLtt9kwoxmhIEJ3f_rk,32209
42
+ phoenix/experiments/functions.py,sha256=WnyBaO6UEesQ1P77GXy-brQSSY9NF1EpAULbtbr4mHo,32228
40
43
  phoenix/experiments/tracing.py,sha256=wVpt8Ie9WNPoi1djJdcrkwCokHdTO0bicXViLg3O-1Y,2831
41
- phoenix/experiments/types.py,sha256=SQqI-8CqA-bDTVIOUC1NOMi9bjEcDSlcG7o3Li1IBFY,23415
44
+ phoenix/experiments/types.py,sha256=HegeRSLyx2lESlzAc2PXYQJVHBFbCLwqQQmPcDMdUnM,23433
42
45
  phoenix/experiments/utils.py,sha256=wLu5Kvt1b4a8rGPRWq5G8RQ9XSiV8fCIVm51zWBI3-g,758
43
46
  phoenix/experiments/evaluators/__init__.py,sha256=j63fi3fa3U7-itVPHa82GowhjQRU-wO6yhO34u_lhsA,714
44
47
  phoenix/experiments/evaluators/base.py,sha256=jAwJs-V7jCp2UBChL0S3813Xyd9GN4rU4IEhX0nkFGs,5549
@@ -65,9 +68,9 @@ phoenix/pointcloud/pointcloud.py,sha256=4zAIkKs2xOUbchpj4XDAV-iPMXrfAJ15TG6rlIYG
65
68
  phoenix/pointcloud/projectors.py,sha256=zO_RrtDYSv2rqVOfIP2_9Cv11Dc8EmcZR94xhFcBYPU,1057
66
69
  phoenix/pointcloud/umap_parameters.py,sha256=3UQSjrysVOvq2V4KNpTMqNqNiK0BsTZnPBHWZ4fyJtQ,1708
67
70
  phoenix/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
- phoenix/server/app.py,sha256=1715YnPAPUUojIG6SYHCanSlMLahxPO3h0dKktLwVFU,19889
71
+ phoenix/server/app.py,sha256=fZYS5Jttj70iHnyDMivXs74oZMAx_gQgdWyNUXs59jE,19407
69
72
  phoenix/server/dml_event.py,sha256=MpjCFqljxvgb9OB5Cez9vJesb3oHb3XxXictynBfcis,2851
70
- phoenix/server/dml_event_handler.py,sha256=pKEWiDApy4mBHvp477mo0y94cm65r9gD5ggQgsA_hak,8460
73
+ phoenix/server/dml_event_handler.py,sha256=6p-PucctivelVHfO-_9zNxWZYPr_eGjDF3bKjLtc5co,8251
71
74
  phoenix/server/grpc_server.py,sha256=jllxDNkpLQxDkvej4RhTokobowbvydF-SU8gSw1MTCc,3378
72
75
  phoenix/server/main.py,sha256=dvjv3g8ANpkvSGCUN02S2Yse643Nlwrp_bj4iXBSVTE,11082
73
76
  phoenix/server/prometheus.py,sha256=j9DHB2fERuq_ZKmwVaqR-9wx5WcPPuU1Cm5Bhg5241Y,2996
@@ -75,12 +78,12 @@ phoenix/server/telemetry.py,sha256=T_2OKrxNViAeaANlNspEekg_Y5uZIFWvKAnpz8Aoqvk,2
75
78
  phoenix/server/thread_server.py,sha256=RwXQGP_QhGD7le6WB7xEygEEuwBl5Ck_Zo8xGIYGi9M,2135
76
79
  phoenix/server/types.py,sha256=UCCkwEzUAbRdu-hZpG7A2hdPM09onBezaXNtWX4A7og,3431
77
80
  phoenix/server/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
- phoenix/server/api/context.py,sha256=4YkrPe9AnB4l6qXSV5FsrqUdkXG1jxCU8ST7iTNlwZY,3047
81
+ phoenix/server/api/context.py,sha256=2-kJpoix-OISxyAhoI5FFEnQMt9ad-3HQ3VOFCjdbxU,2799
79
82
  phoenix/server/api/interceptor.py,sha256=ykDnoC_apUd-llVli3m1CW18kNSIgjz2qZ6m5JmPDu8,1294
80
83
  phoenix/server/api/queries.py,sha256=Nuw74Nh1XhDkLNV8hTO3WPPNeqcSlNhUF76r5HaytGc,20268
81
84
  phoenix/server/api/schema.py,sha256=BcxdqO5CSGqpKd-AAJHMjFlzaK9oJA8GJuxmMfcdjn4,434
82
85
  phoenix/server/api/utils.py,sha256=Kl47G-1A7QKTDrc75BU2QK6HupsG6MWuXxy351FOfKQ,858
83
- phoenix/server/api/dataloaders/__init__.py,sha256=OaqWVJwb2BM2lDHVbILCdpjbB4M5w2UYFXIz_FEO0YI,3476
86
+ phoenix/server/api/dataloaders/__init__.py,sha256=TrOGnU_SD_vEIxOE_dm8HrD5C2ScLFQ4xQ7f8r-E76s,3064
84
87
  phoenix/server/api/dataloaders/annotation_summaries.py,sha256=Wv8AORZoGd5TJ4Y-em8iqJu87AMpZP7lWOTr-SML-x8,5560
85
88
  phoenix/server/api/dataloaders/average_experiment_run_latency.py,sha256=q091UmkXx37OBKh7L-GJ5LXHyRXfX2w4XTk1NMHtPpw,1827
86
89
  phoenix/server/api/dataloaders/dataset_example_revisions.py,sha256=i0g8F4akEf3kQOzAvBjO27QwXNsq-kJEM8dtzduxQgY,3720
@@ -88,7 +91,6 @@ phoenix/server/api/dataloaders/dataset_example_spans.py,sha256=-TjdyyJv2c2JiN1OX
88
91
  phoenix/server/api/dataloaders/document_evaluation_summaries.py,sha256=5XOom2KRAmCwPmtlraiZOSl3vhfaW-eiiYkmetAEalw,5616
89
92
  phoenix/server/api/dataloaders/document_evaluations.py,sha256=V6sE34jON_qFxt7eArJbktykAsty-gnBZHlEkORcj0E,1296
90
93
  phoenix/server/api/dataloaders/document_retrieval_metrics.py,sha256=JqDqkUuoeG6WfcmWSrmQptfF6IPX8XgzYzyECXTAjgg,4202
91
- phoenix/server/api/dataloaders/evaluation_summaries.py,sha256=FEfUMKgk0uqQlJCS5ynmjkBoJVkqW9EVQ8vhgQ2ReKY,5656
92
94
  phoenix/server/api/dataloaders/experiment_annotation_summaries.py,sha256=qDEBRo0XKJMYBYlXBgJ-69jwPK-0r4pi9ZhbjC_vx6M,2813
93
95
  phoenix/server/api/dataloaders/experiment_error_rates.py,sha256=wWTFOO2UxGIYUu53nnzzLk04-mJxw-BQgJELA9gT5bY,1949
94
96
  phoenix/server/api/dataloaders/experiment_run_counts.py,sha256=8VlKRaXwD56-7q_uonRr7L6QlEkyEQubkFKmKx6vuz0,1661
@@ -100,10 +102,8 @@ phoenix/server/api/dataloaders/record_counts.py,sha256=64OsyiQRDZASlibpQAXtRkzyA
100
102
  phoenix/server/api/dataloaders/span_annotations.py,sha256=v3vGkLGTn-n4khxXZY2btgvfXitvytWgBahzrrWEz0I,1078
101
103
  phoenix/server/api/dataloaders/span_dataset_examples.py,sha256=BtLZp11fyyeaWGGBPZj2StzFM0m5jxt52zB2nFMVybo,1306
102
104
  phoenix/server/api/dataloaders/span_descendants.py,sha256=b7jGTn0Hi22gv2yskloLnf3BG3upS9z5hnKLMT9Sxac,2094
103
- phoenix/server/api/dataloaders/span_evaluations.py,sha256=IfwXW23GQaWti8F49wSJocWf7Tklf2ZJ0F6aB4cSVHs,1248
104
105
  phoenix/server/api/dataloaders/span_projects.py,sha256=LbQWiboCFqq4CHS18OzvRUwL9yORqP26fh5p7JbpFdg,1244
105
106
  phoenix/server/api/dataloaders/token_counts.py,sha256=6gDVely8BYiCBdmiq1ECO0lMChUYPIlsZbB34rmL1xM,4684
106
- phoenix/server/api/dataloaders/trace_evaluations.py,sha256=vraPehNsausR4dbdvq-HudRVNARJUbep3T-Ud9jwWYY,1262
107
107
  phoenix/server/api/dataloaders/trace_row_ids.py,sha256=RODX4NULlBzMxHMrsq0dp1ij6ZlLH4ZzQBnafGdxOvU,1100
108
108
  phoenix/server/api/dataloaders/cache/__init__.py,sha256=SYoOM9n8FJaMdQarma5d1blu-jIg2GB8Shqg5ezSzZ8,106
109
109
  phoenix/server/api/dataloaders/cache/two_tier_cache.py,sha256=I38L1RsOis98OQftE7n1Q9QBZfFJO6OW_qIINkuJllo,2295
@@ -154,7 +154,7 @@ phoenix/server/api/routers/v1/__init__.py,sha256=nb49zcOdAi3DSGuC9gUubN9Yri-o7-W
154
154
  phoenix/server/api/routers/v1/datasets.py,sha256=pyLtVEGnjwxh1wJySBOUFrsjtawatfpaF8F3WijK8qU,37049
155
155
  phoenix/server/api/routers/v1/evaluations.py,sha256=FSfz9MTi8s65F07abDXlb9-y97fDZSYbqsCXpimwO7g,12628
156
156
  phoenix/server/api/routers/v1/experiment_evaluations.py,sha256=RTQnjupjmh07xowjq77ajbuAZhzIEfYxA4ZtECvGwOU,4844
157
- phoenix/server/api/routers/v1/experiment_runs.py,sha256=dr7oA3RMyFVON4Dq3fJtDoTeXETcglWVYAakVW8zyZw,6196
157
+ phoenix/server/api/routers/v1/experiment_runs.py,sha256=0G7GgGcZv9dzK47tsPp-p4k5O7W4F_aNRrsNuJN7mho,6393
158
158
  phoenix/server/api/routers/v1/experiments.py,sha256=GeT3Rya4bdaCr6sCf2Vx6fQ_gfMX5XyFHmODCSJiCfU,9951
159
159
  phoenix/server/api/routers/v1/pydantic_compat.py,sha256=FeK8oe2brqu-djsoqRxiKL4tw5cHmi89OHVfCFxYsAo,2890
160
160
  phoenix/server/api/routers/v1/spans.py,sha256=Hd63VHQUqSy0VHsYQ5DPLf5WwKALMXZOIh_ig_5MStc,8780
@@ -180,7 +180,7 @@ phoenix/server/api/types/DocumentEvaluationSummary.py,sha256=EpgKn0uiH2Vbpyltc1M
180
180
  phoenix/server/api/types/DocumentRetrievalMetrics.py,sha256=amkpC3H5IU5-9GvO0telpbq00m6lIcv_2v446OpwFwc,1822
181
181
  phoenix/server/api/types/EmbeddingDimension.py,sha256=2OTVzAcfY-2xnJLl7122EAaqJyp0sob1v9-ryaNWknw,18961
182
182
  phoenix/server/api/types/EmbeddingMetadata.py,sha256=fJvNNYCbkf3SJalArLy9rcBq9Uj1SNac60zjqe1PFnM,461
183
- phoenix/server/api/types/Evaluation.py,sha256=oW9UfUCs6Td60kb5s6_sJNvUkxk5TQXjG0pmQ0-gT4M,3298
183
+ phoenix/server/api/types/Evaluation.py,sha256=rLJptUSRUPPx9sdUyc4UPWxzkr9gcKtkpAS5Zi_EyFQ,2406
184
184
  phoenix/server/api/types/EvaluationSummary.py,sha256=N1DhPXGhBbOOQakF43OGuZ5fl4vye7Uc-HnW5M262F8,1518
185
185
  phoenix/server/api/types/Event.py,sha256=XdYgaIxcVIW-YFViCkxj5l9OaVNepyIrCtm5Iqg2le8,3989
186
186
  phoenix/server/api/types/EventMetadata.py,sha256=-J0tYF9eZTHwCjwxQHY7Gckr2_MNW5OoWT1mydweZNM,635
@@ -199,13 +199,13 @@ phoenix/server/api/types/MimeType.py,sha256=Zpi6zCalkSFgsvhzvOs-O1gYA04usAi9H__Q
199
199
  phoenix/server/api/types/Model.py,sha256=BRIzH5xSGiDrAUYvhwDpwxT6--ddS3Xr3vCvP8_vzdo,8051
200
200
  phoenix/server/api/types/NumericRange.py,sha256=afEjgF97Go_OvmjMggbPBt-zGM8IONewAyEiKEHRds0,192
201
201
  phoenix/server/api/types/PerformanceMetric.py,sha256=W92B7OghEOgzFvmY0LCqpgavHaQggTGshdgfD0yqHX4,350
202
- phoenix/server/api/types/Project.py,sha256=f29k2bsDIQTvqDkgW_dA9ufpQAmw6XEWwiPrv-AaASk,15506
202
+ phoenix/server/api/types/Project.py,sha256=CZ5Hl9fNeHiW8MfSbzXGBYP3A7jw0GubWqtHmzW-bUM,13299
203
203
  phoenix/server/api/types/PromptResponse.py,sha256=Q8HKtpp8GpUOcxPCzZpkkokidDd6u0aZOv_SuPZZd5Q,630
204
204
  phoenix/server/api/types/Retrieval.py,sha256=OhMK2ncjoyp5h1yjKhjlKpoTbQrMHuxmgSFw-AO1rWw,285
205
205
  phoenix/server/api/types/ScalarDriftMetricEnum.py,sha256=IUAcRPpgL41WdoIgK6cNk2Te38SspXGyEs-S1fY23_A,232
206
206
  phoenix/server/api/types/Segments.py,sha256=m2yoegrxA1Tn7ZAy1rMjjD1isc752MaAXMoffkBlvrM,2921
207
207
  phoenix/server/api/types/SortDir.py,sha256=OUpXhlCzCxPoXSDkJJygEs9Rw9pMymfaZUG5zPTrw4Y,152
208
- phoenix/server/api/types/Span.py,sha256=xzJoRuzFf1S64jDuPmGLjSFZ4oPyKUyfK1CtmtZ4LY0,14801
208
+ phoenix/server/api/types/Span.py,sha256=ypzCF70a22QH7e7N8UQbO3FPtsPnAUxqqnq7UlcdGV4,14373
209
209
  phoenix/server/api/types/SpanAnnotation.py,sha256=6b5G-b_OoRvDL2ayWk7MkbqarLK-F-pQMx21CpUuNGY,1168
210
210
  phoenix/server/api/types/TimeSeries.py,sha256=wjzuxHFqCey0O7Ys25qiXyuqXK8an-osyNWUE8A_8G4,5227
211
211
  phoenix/server/api/types/Trace.py,sha256=-nh3A-S_BlQK1VSSOTWqM85l-WwJsRHifxeDi0sFWZE,3246
@@ -227,10 +227,10 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
227
227
  phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
228
228
  phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
229
229
  phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
230
- phoenix/server/static/.vite/manifest.json,sha256=uUMHsl_PTGtJlYZ2ZOLRLq5uyja6vwRbryBq4ZtKYoU,1929
231
- phoenix/server/static/assets/components-D2V-mOGq.js,sha256=A9VauVKZBYNd3FYG79nBexMNDWFPhn0uYLxv3MaoZjY,186964
232
- phoenix/server/static/assets/index-B52Z3aZG.js,sha256=s5weFibNVveQw5Mv6qkwTPW63m3JEqbtcrCno_Z7qEI,7362
233
- phoenix/server/static/assets/pages-CChOjmat.js,sha256=tZB8gxmm_B06n8rccl9a8U8JdC8whimsxlYWB8pFWvA,450347
230
+ phoenix/server/static/.vite/manifest.json,sha256=D9OZ5VZQ3XozxyFU4FF1fd4MmUGkyB_xa67tlUi1NWI,1929
231
+ phoenix/server/static/assets/components-BC3-LP_a.js,sha256=LnC6sDg6caKtsMyIno5Dw7Ure5BGAReRrsn5XBCcP50,187118
232
+ phoenix/server/static/assets/index-BjJvafYL.js,sha256=dz-kVZPp63OEi2l0SYVU5PeimOsUgW3VQQdYdnoZLQE,7362
233
+ phoenix/server/static/assets/pages--n2933VW.js,sha256=WJZ6L_bWgzgJc3EAcbwbzuGADKEcZfMBwSVjDHhKslQ,452799
234
234
  phoenix/server/static/assets/vendor-BMWfu6zp.js,sha256=AAVTM5SjGUI_CmAWFUFmhpp5VDhvCD-MrEoh-pXXADY,1355423
235
235
  phoenix/server/static/assets/vendor-DxkFTwjz.css,sha256=nZrkr0u6NNElFGvpWHk9GTHeGoibCXCli1bE7mXZGZg,1816
236
236
  phoenix/server/static/assets/vendor-arizeai-Sj74jm5V.js,sha256=9lD4YeMt5WtyfrqIApcH9WFQxyJJUtth0syWabkzX-I,304008
@@ -281,8 +281,8 @@ phoenix/utilities/logging.py,sha256=lDXd6EGaamBNcQxL4vP1au9-i_SXe0OraUDiJOcszSw,
281
281
  phoenix/utilities/project.py,sha256=8IJuMM4yUMoooPi37sictGj8Etu9rGmq6RFtc9848cQ,436
282
282
  phoenix/utilities/re.py,sha256=PDve_OLjRTM8yQQJHC8-n3HdIONi7aNils3ZKRZ5uBM,2045
283
283
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
284
- arize_phoenix-4.21.0.dist-info/METADATA,sha256=9epvC-jCcVrQ7I-bwHNr7dmEtOUvNCB_zjvVQhTe7NU,11902
285
- arize_phoenix-4.21.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
286
- arize_phoenix-4.21.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
287
- arize_phoenix-4.21.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
288
- arize_phoenix-4.21.0.dist-info/RECORD,,
284
+ arize_phoenix-4.22.1.dist-info/METADATA,sha256=aMe9GmDUEAXZE2A5wixJeuulWvcq7dA7U4FGnGVVCf8,11902
285
+ arize_phoenix-4.22.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
286
+ arize_phoenix-4.22.1.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
287
+ arize_phoenix-4.22.1.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
288
+ arize_phoenix-4.22.1.dist-info/RECORD,,
phoenix/config.py CHANGED
@@ -2,7 +2,7 @@ import os
2
2
  import tempfile
3
3
  from logging import getLogger
4
4
  from pathlib import Path
5
- from typing import Dict, List, Optional
5
+ from typing import Dict, List, Optional, Tuple
6
6
 
7
7
  from .utilities.re import parse_env_headers
8
8
 
@@ -60,6 +60,11 @@ ENV_PHOENIX_SERVER_INSTRUMENTATION_OTLP_TRACE_COLLECTOR_GRPC_ENDPOINT = (
60
60
  "PHOENIX_SERVER_INSTRUMENTATION_OTLP_TRACE_COLLECTOR_GRPC_ENDPOINT"
61
61
  )
62
62
 
63
+ # Auth is under active development. Phoenix users are strongly advised not to
64
+ # set these environment variables until the feature is officially released.
65
+ ENV_DANGEROUSLY_SET_PHOENIX_ENABLE_AUTH = "DANGEROUSLY_SET_PHOENIX_ENABLE_AUTH"
66
+ ENV_DANGEROUSLY_SET_PHOENIX_SECRET = "DANGEROUSLY_SET_PHOENIX_SECRET"
67
+
63
68
 
64
69
  def server_instrumentation_is_enabled() -> bool:
65
70
  return bool(
@@ -100,6 +105,57 @@ def get_working_dir() -> Path:
100
105
  return Path.home().resolve() / ".phoenix"
101
106
 
102
107
 
108
+ def get_boolean_env_var(env_var: str) -> Optional[bool]:
109
+ """
110
+ Parses a boolean environment variable, returning None if the variable is not set.
111
+ """
112
+ if (value := os.environ.get(env_var)) is None:
113
+ return None
114
+ assert (lower := value.lower()) in (
115
+ "true",
116
+ "false",
117
+ ), f"{env_var} must be set to TRUE or FALSE (case-insensitive)"
118
+ return lower == "true"
119
+
120
+
121
+ def get_env_enable_auth() -> bool:
122
+ """
123
+ Gets the value of the DANGEROUSLY_SET_PHOENIX_ENABLE_AUTH environment variable.
124
+ """
125
+ return get_boolean_env_var(ENV_DANGEROUSLY_SET_PHOENIX_ENABLE_AUTH) is True
126
+
127
+
128
+ def get_env_phoenix_secret() -> Optional[str]:
129
+ """
130
+ Gets the value of the DANGEROUSLY_SET_PHOENIX_SECRET environment variable
131
+ and performs validation.
132
+ """
133
+ phoenix_secret = os.environ.get(ENV_DANGEROUSLY_SET_PHOENIX_SECRET)
134
+ if phoenix_secret is None:
135
+ return None
136
+ # todo: add validation for the phoenix secret
137
+ return phoenix_secret
138
+
139
+
140
+ def get_auth_settings() -> Tuple[bool, Optional[str]]:
141
+ """
142
+ Gets auth settings and performs validation.
143
+ """
144
+ enable_auth = get_env_enable_auth()
145
+ phoenix_secret = get_env_phoenix_secret()
146
+ if enable_auth:
147
+ assert phoenix_secret, (
148
+ "DANGEROUSLY_SET_PHOENIX_SECRET must be set "
149
+ "when auth is enabled with DANGEROUSLY_SET_PHOENIX_ENABLE_AUTH"
150
+ )
151
+ else:
152
+ assert not phoenix_secret, (
153
+ "DANGEROUSLY_SET_PHOENIX_SECRET cannot be set "
154
+ "unless auth is enabled with DANGEROUSLY_SET_PHOENIX_ENABLE_AUTH"
155
+ )
156
+ return enable_auth, phoenix_secret
157
+
158
+
103
159
  PHOENIX_DIR = Path(__file__).resolve().parent
104
160
  # Server config
105
161
  SERVER_DIR = PHOENIX_DIR / "server"
@@ -123,6 +179,8 @@ EXPORT_DIR = ROOT_DIR / "exports"
123
179
  INFERENCES_DIR = ROOT_DIR / "inferences"
124
180
  TRACE_DATASETS_DIR = ROOT_DIR / "trace_datasets"
125
181
 
182
+ ENABLE_AUTH, PHOENIX_SECRET = get_auth_settings()
183
+
126
184
 
127
185
  def ensure_working_dir() -> None:
128
186
  """
@@ -0,0 +1,4 @@
1
+ # Future Migrations
2
+
3
+ This folder contains future migrations for unreleased features that are under active development. Do not run these migrations unless you are a Phoenix developer.
4
+
@@ -0,0 +1,292 @@
1
+ """users and tokens
2
+
3
+ Revision ID: cd164e83824f
4
+ Revises: 10460e46d750
5
+ Create Date: 2024-08-01 18:36:52.157604
6
+
7
+ """
8
+
9
+ from datetime import datetime, timezone
10
+ from typing import Any, Dict, List, Optional, Sequence, TypedDict, Union
11
+
12
+ import sqlalchemy as sa
13
+ from alembic import op
14
+ from phoenix.datetime_utils import normalize_datetime
15
+ from sqlalchemy import (
16
+ JSON,
17
+ TIMESTAMP,
18
+ CheckConstraint,
19
+ Dialect,
20
+ ForeignKey,
21
+ MetaData,
22
+ TypeDecorator,
23
+ func,
24
+ insert,
25
+ )
26
+ from sqlalchemy.dialects import postgresql
27
+ from sqlalchemy.ext.asyncio.engine import AsyncConnection
28
+ from sqlalchemy.ext.compiler import compiles
29
+ from sqlalchemy.orm import (
30
+ DeclarativeBase,
31
+ Mapped,
32
+ mapped_column,
33
+ )
34
+
35
+
36
+ class JSONB(JSON):
37
+ # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
38
+ __visit_name__ = "JSONB"
39
+
40
+
41
+ @compiles(JSONB, "sqlite") # type: ignore
42
+ def _(*args: Any, **kwargs: Any) -> str:
43
+ # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
44
+ return "JSONB"
45
+
46
+
47
+ JSON_ = (
48
+ JSON()
49
+ .with_variant(
50
+ postgresql.JSONB(), # type: ignore
51
+ "postgresql",
52
+ )
53
+ .with_variant(
54
+ JSONB(),
55
+ "sqlite",
56
+ )
57
+ )
58
+
59
+
60
+ class JsonDict(TypeDecorator[Dict[str, Any]]):
61
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
62
+ cache_ok = True
63
+ impl = JSON_
64
+
65
+ def process_bind_param(self, value: Optional[Dict[str, Any]], _: Dialect) -> Dict[str, Any]:
66
+ return value if isinstance(value, dict) else {}
67
+
68
+
69
+ class JsonList(TypeDecorator[List[Any]]):
70
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
71
+ cache_ok = True
72
+ impl = JSON_
73
+
74
+ def process_bind_param(self, value: Optional[List[Any]], _: Dialect) -> List[Any]:
75
+ return value if isinstance(value, list) else []
76
+
77
+
78
+ class UtcTimeStamp(TypeDecorator[datetime]):
79
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
80
+ cache_ok = True
81
+ impl = TIMESTAMP(timezone=True)
82
+
83
+ def process_bind_param(self, value: Optional[datetime], _: Dialect) -> Optional[datetime]:
84
+ return normalize_datetime(value)
85
+
86
+ def process_result_value(self, value: Optional[Any], _: Dialect) -> Optional[datetime]:
87
+ return normalize_datetime(value, timezone.utc)
88
+
89
+
90
+ class ExperimentRunOutput(TypedDict, total=False):
91
+ task_output: Any
92
+
93
+
94
+ class Base(DeclarativeBase):
95
+ # Enforce best practices for naming constraints
96
+ # https://alembic.sqlalchemy.org/en/latest/naming.html#integration-of-naming-conventions-into-operations-autogenerate
97
+ metadata = MetaData(
98
+ naming_convention={
99
+ "ix": "ix_%(table_name)s_%(column_0_N_name)s",
100
+ "uq": "uq_%(table_name)s_%(column_0_N_name)s",
101
+ "ck": "ck_%(table_name)s_`%(constraint_name)s`",
102
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
103
+ "pk": "pk_%(table_name)s",
104
+ }
105
+ )
106
+ type_annotation_map = {
107
+ Dict[str, Any]: JsonDict,
108
+ List[Dict[str, Any]]: JsonList,
109
+ ExperimentRunOutput: JsonDict,
110
+ }
111
+
112
+
113
+ class UserRole(Base):
114
+ __tablename__ = "user_roles"
115
+ id: Mapped[int] = mapped_column(primary_key=True)
116
+ role: Mapped[str] = mapped_column(unique=True)
117
+
118
+
119
+ class User(Base):
120
+ __tablename__ = "users"
121
+ id: Mapped[int] = mapped_column(primary_key=True)
122
+ user_role_id: Mapped[int] = mapped_column(
123
+ ForeignKey("user_roles.id"),
124
+ index=True,
125
+ )
126
+ username: Mapped[Optional[str]] = mapped_column(nullable=True, unique=True, index=True)
127
+ email: Mapped[str] = mapped_column(nullable=False, unique=True, index=True)
128
+ auth_method: Mapped[str] = mapped_column(
129
+ CheckConstraint("auth_method IN ('LOCAL')", name="valid_auth_method")
130
+ )
131
+ password_hash: Mapped[Optional[str]]
132
+ reset_password: Mapped[bool]
133
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
134
+ updated_at: Mapped[datetime] = mapped_column(
135
+ UtcTimeStamp, server_default=func.now(), onupdate=func.now()
136
+ )
137
+ deleted_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp)
138
+
139
+
140
+ # revision identifiers, used by Alembic.
141
+ revision: str = "cd164e83824f"
142
+ down_revision: Union[str, None] = "3be8647b87d8"
143
+ branch_labels: Union[str, Sequence[str], None] = None
144
+ depends_on: Union[str, Sequence[str], None] = None
145
+
146
+
147
+ async def insert_roles_and_users(connection: AsyncConnection) -> None:
148
+ """
149
+ Populates the `user_roles` table and adds a system user and initial admin
150
+ user to the `users` table.
151
+ """
152
+ await connection.execute(
153
+ insert(UserRole).values([{"role": "SYSTEM"}, {"role": "ADMIN"}, {"role": "GENERAL"}])
154
+ )
155
+ system_user_role_id = sa.select(UserRole.id).where(UserRole.role == "SYSTEM").scalar_subquery()
156
+ admin_user_role_id = sa.select(UserRole.id).where(UserRole.role == "ADMIN").scalar_subquery()
157
+ await connection.execute(
158
+ insert(User).values(
159
+ [
160
+ {
161
+ "user_role_id": system_user_role_id,
162
+ "username": None,
163
+ "email": "system@localhost",
164
+ "auth_method": "LOCAL",
165
+ "password_hash": None,
166
+ "reset_password": False,
167
+ },
168
+ {
169
+ "user_role_id": admin_user_role_id,
170
+ "username": "admin",
171
+ "email": "admin@localhost",
172
+ "auth_method": "LOCAL",
173
+ "password_hash": None, # todo: replace this with the hashed PHOENIX_SECRET
174
+ "reset_password": True,
175
+ },
176
+ ]
177
+ )
178
+ )
179
+
180
+
181
+ def upgrade() -> None:
182
+ op.create_table(
183
+ "user_roles",
184
+ sa.Column("id", sa.Integer, primary_key=True),
185
+ sa.Column(
186
+ "role",
187
+ sa.String,
188
+ nullable=False,
189
+ unique=True,
190
+ ),
191
+ )
192
+ op.create_table(
193
+ "users",
194
+ sa.Column("id", sa.Integer, primary_key=True),
195
+ sa.Column(
196
+ "user_role_id",
197
+ sa.Integer,
198
+ sa.ForeignKey("user_roles.id"),
199
+ nullable=False,
200
+ index=True,
201
+ ),
202
+ sa.Column("username", sa.String, nullable=True, unique=True, index=True),
203
+ sa.Column("email", sa.String, nullable=False, unique=True, index=True),
204
+ sa.Column(
205
+ "auth_method",
206
+ sa.String,
207
+ sa.CheckConstraint("auth_method IN ('LOCAL')", "valid_auth_method"),
208
+ nullable=False,
209
+ ),
210
+ sa.Column("password_hash", sa.String, nullable=True),
211
+ sa.Column("reset_password", sa.Boolean, nullable=False),
212
+ sa.Column(
213
+ "created_at",
214
+ sa.TIMESTAMP(timezone=True),
215
+ nullable=False,
216
+ server_default=sa.func.now(),
217
+ ),
218
+ sa.Column(
219
+ "updated_at",
220
+ sa.TIMESTAMP(timezone=True),
221
+ nullable=False,
222
+ server_default=sa.func.now(),
223
+ onupdate=sa.func.now(),
224
+ ),
225
+ sa.Column(
226
+ "deleted_at",
227
+ sa.TIMESTAMP(timezone=True),
228
+ nullable=True,
229
+ ),
230
+ )
231
+ op.create_table(
232
+ "api_keys",
233
+ sa.Column("id", sa.Integer, primary_key=True),
234
+ sa.Column(
235
+ "user_id",
236
+ sa.Integer,
237
+ sa.ForeignKey("users.id"),
238
+ nullable=False,
239
+ index=True,
240
+ ),
241
+ sa.Column("name", sa.String, nullable=False),
242
+ sa.Column("description", sa.String, nullable=True),
243
+ sa.Column(
244
+ "created_at",
245
+ sa.TIMESTAMP(timezone=True),
246
+ nullable=False,
247
+ server_default=sa.func.now(),
248
+ ),
249
+ sa.Column(
250
+ "expires_at",
251
+ sa.TIMESTAMP(timezone=True),
252
+ nullable=True,
253
+ ),
254
+ )
255
+ op.create_table(
256
+ "audit_api_keys",
257
+ sa.Column("id", sa.Integer, primary_key=True),
258
+ sa.Column(
259
+ "api_key_id",
260
+ sa.Integer,
261
+ sa.ForeignKey("api_keys.id"),
262
+ nullable=False,
263
+ index=True,
264
+ ),
265
+ sa.Column(
266
+ "user_id",
267
+ sa.Integer,
268
+ sa.ForeignKey("users.id"),
269
+ nullable=False,
270
+ index=True,
271
+ ),
272
+ sa.Column(
273
+ "action",
274
+ sa.String,
275
+ sa.CheckConstraint("action IN ('CREATE', 'DELETE')", "valid_action"),
276
+ nullable=False,
277
+ ),
278
+ sa.Column(
279
+ "created_at",
280
+ sa.TIMESTAMP(timezone=True),
281
+ nullable=False,
282
+ server_default=sa.func.now(),
283
+ ),
284
+ )
285
+ op.run_async(insert_roles_and_users)
286
+
287
+
288
+ def downgrade() -> None:
289
+ op.drop_table("audit_api_keys")
290
+ op.drop_table("api_keys")
291
+ op.drop_table("users")
292
+ op.drop_table("user_roles")
@@ -0,0 +1 @@
1
+ cd164e83824f_users_and_tokens.py
phoenix/db/models.py CHANGED
@@ -34,6 +34,7 @@ from sqlalchemy.orm import (
34
34
  )
35
35
  from sqlalchemy.sql import expression
36
36
 
37
+ from phoenix.config import ENABLE_AUTH
37
38
  from phoenix.datetime_utils import normalize_datetime
38
39
 
39
40
 
@@ -616,3 +617,63 @@ class ExperimentRunAnnotation(Base):
616
617
  "name",
617
618
  ),
618
619
  )
620
+
621
+
622
+ # todo: unnest the following models when auth is released (https://github.com/Arize-ai/phoenix/issues/4183)
623
+ if ENABLE_AUTH:
624
+
625
+ class UserRole(Base):
626
+ __tablename__ = "user_roles"
627
+ id: Mapped[int] = mapped_column(primary_key=True)
628
+ role: Mapped[str] = mapped_column(unique=True)
629
+
630
+ class User(Base):
631
+ __tablename__ = "users"
632
+ id: Mapped[int] = mapped_column(primary_key=True)
633
+ user_role_id: Mapped[int] = mapped_column(
634
+ ForeignKey("user_roles.id"),
635
+ index=True,
636
+ )
637
+ username: Mapped[Optional[str]] = mapped_column(nullable=True, unique=True, index=True)
638
+ email: Mapped[str] = mapped_column(nullable=False, unique=True, index=True)
639
+ auth_method: Mapped[str] = mapped_column(
640
+ CheckConstraint("auth_method IN ('LOCAL')", name="valid_auth_method")
641
+ )
642
+ password_hash: Mapped[Optional[str]]
643
+ reset_password: Mapped[bool]
644
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
645
+ updated_at: Mapped[datetime] = mapped_column(
646
+ UtcTimeStamp, server_default=func.now(), onupdate=func.now()
647
+ )
648
+ deleted_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp)
649
+
650
+ class APIKey(Base):
651
+ __tablename__ = "api_keys"
652
+ id: Mapped[int] = mapped_column(primary_key=True)
653
+ user_id: Mapped[int] = mapped_column(
654
+ ForeignKey("users.id"),
655
+ index=True,
656
+ )
657
+ name: Mapped[str]
658
+ description: Mapped[Optional[str]]
659
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
660
+ expires_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp)
661
+
662
+ # todo: standardize audit table format (https://github.com/Arize-ai/phoenix/issues/4185)
663
+ class AuditAPIKey(Base):
664
+ __tablename__ = "audit_api_keys"
665
+ id: Mapped[int] = mapped_column(primary_key=True)
666
+ api_key_id: Mapped[int] = mapped_column(
667
+ ForeignKey("api_keys.id"),
668
+ nullable=False,
669
+ index=True,
670
+ )
671
+ user_id: Mapped[int] = mapped_column(
672
+ ForeignKey("users.id"),
673
+ nullable=False,
674
+ index=True,
675
+ )
676
+ action: Mapped[str] = mapped_column(
677
+ CheckConstraint("action IN ('CREATE', 'DELETE')", name="valid_action")
678
+ )
679
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
@@ -447,14 +447,14 @@ def evaluate_experiment(
447
447
  )
448
448
  if not dataset.examples:
449
449
  raise ValueError(f"Dataset has no examples: {dataset_id=}, {dataset_version_id=}")
450
- experiment_runs = tuple(
451
- ExperimentRun.from_dict(exp_run)
450
+ experiment_runs = {
451
+ exp_run["id"]: ExperimentRun.from_dict(exp_run)
452
452
  for exp_run in sync_client.get(f"/v1/experiments/{experiment.id}/runs").json()["data"]
453
- )
453
+ }
454
454
  if not experiment_runs:
455
455
  raise ValueError("Experiment has not been run")
456
456
  params = ExperimentParameters(n_examples=len(dataset.examples))
457
- task_summary = TaskSummary.from_task_runs(params, experiment_runs)
457
+ task_summary = TaskSummary.from_task_runs(params, experiment_runs.values())
458
458
  ran_experiment = object.__new__(RanExperiment)
459
459
  ran_experiment.__init__( # type: ignore[misc]
460
460
  dataset=dataset,
@@ -167,7 +167,7 @@ class Dataset:
167
167
  def from_dict(cls, obj: Mapping[str, Any]) -> Dataset:
168
168
  examples = tuple(map(Example.from_dict, obj.get("examples") or ()))
169
169
  return cls(
170
- id=obj["id"],
170
+ id=obj["dataset_id"],
171
171
  version_id=obj["version_id"],
172
172
  examples={ex.id: ex for ex in examples},
173
173
  )
@@ -225,7 +225,7 @@ class ExperimentRun:
225
225
  )
226
226
 
227
227
  def __post_init__(self) -> None:
228
- if bool(self.output) == bool(self.error):
228
+ if self.output is None and self.error is None:
229
229
  raise ValueError("Must specify exactly one of experiment_run_output or error")
230
230
 
231
231
 
@@ -284,7 +284,7 @@ class ExperimentEvaluationRun:
284
284
  )
285
285
 
286
286
  def __post_init__(self) -> None:
287
- if bool(self.result) == bool(self.error):
287
+ if self.result is None and self.error is None:
288
288
  raise ValueError("Must specify either result or error")
289
289
 
290
290