arize-phoenix 8.22.1__py3-none-any.whl → 8.24.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 (31) hide show
  1. {arize_phoenix-8.22.1.dist-info → arize_phoenix-8.24.0.dist-info}/METADATA +22 -2
  2. {arize_phoenix-8.22.1.dist-info → arize_phoenix-8.24.0.dist-info}/RECORD +31 -29
  3. phoenix/config.py +65 -3
  4. phoenix/db/facilitator.py +118 -85
  5. phoenix/db/helpers.py +16 -0
  6. phoenix/server/api/context.py +2 -0
  7. phoenix/server/api/mutations/user_mutations.py +10 -0
  8. phoenix/server/api/queries.py +3 -14
  9. phoenix/server/api/routers/v1/__init__.py +2 -0
  10. phoenix/server/api/routers/v1/projects.py +393 -0
  11. phoenix/server/api/subscriptions.py +1 -1
  12. phoenix/server/app.py +17 -0
  13. phoenix/server/email/sender.py +74 -47
  14. phoenix/server/email/templates/welcome.html +12 -0
  15. phoenix/server/email/types.py +16 -1
  16. phoenix/server/main.py +8 -1
  17. phoenix/server/static/.vite/manifest.json +36 -36
  18. phoenix/server/static/assets/{components-BAc4OPED.js → components-B6cljCxu.js} +82 -82
  19. phoenix/server/static/assets/{index-Du53xkjY.js → index-DfHKoAV9.js} +2 -2
  20. phoenix/server/static/assets/{pages-Dz-gbBPF.js → pages-Dhitcl5V.js} +342 -339
  21. phoenix/server/static/assets/{vendor-CEisxXSv.js → vendor-C3H3sezv.js} +1 -1
  22. phoenix/server/static/assets/{vendor-arizeai-BCTsSnvS.js → vendor-arizeai-DT8pwHfH.js} +1 -1
  23. phoenix/server/static/assets/{vendor-codemirror-DIWnRs_7.js → vendor-codemirror-DvimrGxD.js} +1 -1
  24. phoenix/server/static/assets/{vendor-recharts-Bame54mG.js → vendor-recharts-DuSQBcYW.js} +1 -1
  25. phoenix/server/static/assets/{vendor-shiki-Cc73E4D-.js → vendor-shiki-i05Hmswh.js} +1 -1
  26. phoenix/session/session.py +10 -0
  27. phoenix/version.py +1 -1
  28. {arize_phoenix-8.22.1.dist-info → arize_phoenix-8.24.0.dist-info}/WHEEL +0 -0
  29. {arize_phoenix-8.22.1.dist-info → arize_phoenix-8.24.0.dist-info}/entry_points.txt +0 -0
  30. {arize_phoenix-8.22.1.dist-info → arize_phoenix-8.24.0.dist-info}/licenses/IP_NOTICE +0 -0
  31. {arize_phoenix-8.22.1.dist-info → arize_phoenix-8.24.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arize-phoenix
3
- Version: 8.22.1
3
+ Version: 8.24.0
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
@@ -141,7 +141,10 @@ Description-Content-Type: text/markdown
141
141
  <a target="_blank" href="https://arize-ai.slack.com/join/shared_invite/zt-11t1vbu4x-xkBIHmOREQnYnYDH1GDfCg?__hstc=259489365.a667dfafcfa0169c8aee4178d115dc81.1733501603539.1733501603539.1733501603539.1&__hssc=259489365.1.1733501603539&__hsfp=3822854628&submissionGuid=381a0676-8f38-437b-96f2-fc10875658df#/shared-invite/email">
142
142
  <img src="https://img.shields.io/static/v1?message=Community&logo=slack&labelColor=grey&color=blue&logoColor=white&label=%20"/>
143
143
  </a>
144
- <a target="_blank" href="https://twitter.com/ArizePhoenix">
144
+ <a target="_blank" href="https://bsky.app/profile/arize-phoenix.bsky.social">
145
+ <img src="https://img.shields.io/badge/-phoenix-blue.svg?color=blue&labelColor=gray&logo=bluesky">
146
+ </a>
147
+ <a target="_blank" href="https://x.com/ArizePhoenix">
145
148
  <img src="https://img.shields.io/badge/-ArizePhoenix-blue.svg?color=blue&labelColor=gray&logo=x">
146
149
  </a>
147
150
  <a target="_blank" href="https://pypi.org/project/arize-phoenix/">
@@ -156,6 +159,9 @@ Description-Content-Type: text/markdown
156
159
  <a target="_blank" href="https://hub.docker.com/r/arizephoenix/phoenix/tags">
157
160
  <img src="https://img.shields.io/docker/v/arizephoenix/phoenix?sort=semver&logo=docker&label=image&color=blue">
158
161
  </a>
162
+ <a target="_blank" href="https://github.com/Arize-ai/phoenix/tree/main/js/packages/phoenix-mcp">
163
+ <img src="https://badge.mcpx.dev?status=on" title="MCP Enabled"/>
164
+ </a>
159
165
  </p>
160
166
 
161
167
  Phoenix is an open-source AI observability platform designed for experimentation, evaluation, and troubleshooting. It provides:
@@ -181,6 +187,20 @@ pip install arize-phoenix
181
187
 
182
188
  Phoenix container images are available via [Docker Hub](https://hub.docker.com/r/arizephoenix/phoenix) and can be deployed using Docker or Kubernetes.
183
189
 
190
+ ## Packages
191
+
192
+ The `arize-phoenix` package includes the entire Phoenix platfom. However if you have deployed the Phoenix platform, there are light-weight Python sub-packages and TypeScript packages that can be used in conjunction with the platfrom.
193
+
194
+ ### Subpackages
195
+
196
+ | Package | Language | Description |
197
+ | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
198
+ | [arize-phoenix-otel](https://github.com/Arize-ai/phoenix/tree/main/packages/phoenix-otel) | Python [![PyPI Version](https://img.shields.io/pypi/v/arize-phoenix-otel)](https://pypi.org/project/arize-phoenix-otel/) | Provides a lightweight wrapper around OpenTelemetry primitives with Phoenix-aware defaults |
199
+ | [arize-phoenix-client](https://github.com/Arize-ai/phoenix/tree/main/packages/phoenix-client) | Python [![PyPI Version](https://img.shields.io/pypi/v/arize-phoenix-client)](https://pypi.org/project/arize-phoenix-client/) | Lightweight client for interacting with the Phoenix server via its OpenAPI REST interface |
200
+ | [arize-phoenix-evals](https://github.com/Arize-ai/phoenix/tree/main/packages/phoenix-evals) | Python [![PyPI Version](https://img.shields.io/pypi/v/arize-phoenix-evals)](https://pypi.org/project/arize-phoenix-evals/) | Tooling to evaluate LLM applications including RAG relevance, answer relevance, and more |
201
+ | [@arizeai/phoenix-client](https://github.com/Arize-ai/phoenix/tree/main/js/packages/phoenix-client) | JavaScript [![NPM Version](https://img.shields.io/npm/v/%40arizeai%2Fphoenix-client)](https://www.npmjs.com/package/@arizeai/phoenix-client) | Client for the Arize Phoenix API |
202
+ | [@arizeai/phoenix-mcp](https://github.com/Arize-ai/phoenix/tree/main/js/packages/phoenix-mcp) | JavaScript [![NPM Version](https://img.shields.io/npm/v/%40arizeai%2Fphoenix-mcp)](https://www.npmjs.com/package/@arizeai/phoenix-mcp) | MCP server implementation for Arize Phoenix providing unified interface to Phoenix's capabilities |
203
+
184
204
  ## Tracing Integrations
185
205
 
186
206
  Phoenix is built on top of OpenTelemetry and is vendor, language, and framework agnostic. For details about tracing integrations and example applications, see the [OpenInference](https://github.com/Arize-ai/openinference) project.
@@ -1,12 +1,12 @@
1
1
  phoenix/__init__.py,sha256=X3eUEwd2rG8KKWWYVNNDJoqo08ihfjgHhlP29dcdNJE,5481
2
2
  phoenix/auth.py,sha256=VVMHrWN31tln3Zo4z6ofecrV4daiqJjLd8r85mqlxek,10939
3
- phoenix/config.py,sha256=dSUZUhHVkfMP5yNs_fyvM96ZcZvY7KlyrDKkVflS9aM,35855
3
+ phoenix/config.py,sha256=SNBMUEo6lNn5WXP-x6B85UmADZfGEkhfeHDDfW3Af5Y,38090
4
4
  phoenix/datetime_utils.py,sha256=iJzNG6YJ6V7_u8B2iA7P2Z26FyxYbOPtx0dhJ7kNDHA,3398
5
5
  phoenix/exceptions.py,sha256=n2L2KKuecrdflB9MsCdAYCiSEvGJptIsfRkXMoJle7A,169
6
6
  phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
7
  phoenix/services.py,sha256=kpW1WL0kiB8XJsO6XycvZVJ-lBkNoenhQ7atCvBoSe8,5365
8
8
  phoenix/settings.py,sha256=x87BX7hWGQQZbrW_vrYqFR_izCGfO9gFc--JXUG4Tdk,754
9
- phoenix/version.py,sha256=9_O4XK4bMyz2wNHNrm0CKuEPSLnNOBbKUiFYksqNQDk,23
9
+ phoenix/version.py,sha256=gGnFGQFB9qEAewyp9uBaFtO7FGPLu10guHACzqxWA_g,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=qBFraOtmwCCnWJltKNP18DDG0mULXigytlFsa6YOz6k,4837
@@ -18,8 +18,8 @@ phoenix/db/alembic.ini,sha256=GIS6HpHaKaJbbuahZg1Rc1D2_QqyCkV9r58wdARGf6w,3262
18
18
  phoenix/db/bulk_inserter.py,sha256=faNjuwLqqsw4ky8sa4D0h9u5TvEDTOjrccUW89L008E,12725
19
19
  phoenix/db/engines.py,sha256=gxcP2aNy_JyKHv1MO4d2UM47GTMy1jDcN-FQETZ3iNA,7348
20
20
  phoenix/db/enums.py,sha256=tt7iovXLhVTLZ3_LbHNGgcI44SnNjXfkKtLAZG57T54,428
21
- phoenix/db/facilitator.py,sha256=nteTD8XQSQ7a6aZBOw7ZKiJ72-_1TZXKVF4HS3fHdas,5675
22
- phoenix/db/helpers.py,sha256=daKbpY2QhTPo9a_T1xNHKI4WzWHkMmmrGIws7Hw-RZ4,4884
21
+ phoenix/db/facilitator.py,sha256=-uzv3Wr4YuUtVff4_0bLTcIFvzWzWOA3IPq8s2RA_bc,6821
22
+ phoenix/db/helpers.py,sha256=rbbHcl-STzcEpcXCYx6jbKzko7r3ggrWHHsXjZ48HsM,5352
23
23
  phoenix/db/migrate.py,sha256=oUrXH8yEbcpL4eh09aSCuUiSrhFli0eT5D_j4ZmYChY,2797
24
24
  phoenix/db/models.py,sha256=TbHgtT7WWdkQK-OtLsUqp3MwP23HGV1IaSAWTqCf5ac,45707
25
25
  phoenix/db/insertion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -80,13 +80,13 @@ phoenix/pointcloud/pointcloud.py,sha256=SN_1wXZcwKrtSnHGZLDZGx71orqE1WyVF7E-D58d
80
80
  phoenix/pointcloud/projectors.py,sha256=TQgwc9cJDjJkin1WZyZzgl3HsYrLLiyWD7Czy4jNW3U,1088
81
81
  phoenix/pointcloud/umap_parameters.py,sha256=db_WEPoamuWtopZx7tQfAXPnoE0MS8FkAV0_ThjEx_Q,1735
82
82
  phoenix/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
- phoenix/server/app.py,sha256=X7Gr-bqIksPaZV1H8FHFsPHLIe025BLVLlgCbqV6Pq8,39491
83
+ phoenix/server/app.py,sha256=f9H8ekaGiMwCMbSf2kbQIUFKywY2VmaIuJgPlrC7obU,40221
84
84
  phoenix/server/bearer_auth.py,sha256=0UudvkAS_dxna5JEJJhGUYwB6Ny-e22ssX5Mm79QwCk,5907
85
85
  phoenix/server/dml_event.py,sha256=MjJmVEKytq75chBOSyvYDusUnEbg1pHpIjR3pZkUaJA,2838
86
86
  phoenix/server/dml_event_handler.py,sha256=EZLXmCvx4pJrCkz29gxwKwmvmUkTtPCHw6klR-XM8qE,8258
87
87
  phoenix/server/grpc_server.py,sha256=SknR-iLIUqU9swiAyLhbCUREA1obOM6w49xgdK1AQDs,3839
88
88
  phoenix/server/jwt_store.py,sha256=asxzY4_ZBM2FWAMstHvhvnKUP_0AA3v3xPTL2IOgNqY,16831
89
- phoenix/server/main.py,sha256=Gx63qC-5yI7EFfX3pv293eM5gb8xTO4swZhp242qV1o,16406
89
+ phoenix/server/main.py,sha256=qJcqXudiY65UeOWOaxP1-dqzrV-BBnbZu_IDd-qMKMk,16720
90
90
  phoenix/server/oauth2.py,sha256=EV4wcCwG0N7cJRcfGNURdP5rZgRVCeRDvXyle19A27Y,2064
91
91
  phoenix/server/prometheus.py,sha256=1KjvSfjSa2-BPjDybVMM_Kag316CsN-Zwt64YNr_snc,7825
92
92
  phoenix/server/rate_limiters.py,sha256=cFc73D2NaxqNZZDbwfIDw4So-fRVOJPBtqxOZ8Qky_s,7155
@@ -96,12 +96,12 @@ phoenix/server/types.py,sha256=gJJPBcDRkQ9VHZIt_aLqG_OBbGt1oWp4e3W3Jp61oKs,7409
96
96
  phoenix/server/api/README.md,sha256=Pyq1PLPgTzXAswrfIhGXrjI3Skq8it2jTVnanT6Ba4Q,1162
97
97
  phoenix/server/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
98
  phoenix/server/api/auth.py,sha256=nywpmfMI1trZTbZRD3oBj4kFjzg_vnxDljcM431T1eY,1246
99
- phoenix/server/api/context.py,sha256=OopBkMnY48TzulTtfuay3MXmhbFIWPtPoAsbhCW-Pl0,6459
99
+ phoenix/server/api/context.py,sha256=gB9bNgdqqD4Bn-6V4zlKeILl6L76jynPtOuM_5UZBNw,6557
100
100
  phoenix/server/api/exceptions.py,sha256=TA0JuY2YRnj35qGuMSQ8d0ToHum9gWm9W--3fSKHrX0,1171
101
101
  phoenix/server/api/interceptor.py,sha256=ykDnoC_apUd-llVli3m1CW18kNSIgjz2qZ6m5JmPDu8,1294
102
- phoenix/server/api/queries.py,sha256=Xd1K-6bu_DuUxczyXzf0M6EiRL7Ahnx47QVqPmo2-58,36167
102
+ phoenix/server/api/queries.py,sha256=WNH1LxB7bobfOMdBJts-9_ncfVhv2LThAKu14MRPMpk,35810
103
103
  phoenix/server/api/schema.py,sha256=fcs36xQwFF_Qe41_5cWR8wYpDvOrnbcyTeo5WNMbDsA,1702
104
- phoenix/server/api/subscriptions.py,sha256=DSIgQF6lQqkbc7D0AaI5R4g3hIHbU04H5Y2UIpwmpy0,22989
104
+ phoenix/server/api/subscriptions.py,sha256=TnZhdoNHMXp1NkUVLA-eB54woll7FvxtsB2pLt1dO0w,23001
105
105
  phoenix/server/api/utils.py,sha256=quCBRcusc6PUq9tJq7M8PgwFZp7nXgVAxtbw8feribY,833
106
106
  phoenix/server/api/dataloaders/__init__.py,sha256=LiOF9d95TPOD3pYuE906trLq-_q1VP8JQ9B6SHicbbM,4360
107
107
  phoenix/server/api/dataloaders/annotation_summaries.py,sha256=2sHmIDX7n8tuPeBTs9bMKtlMKWn_Ph9awTZqmwn2Owc,5505
@@ -201,7 +201,7 @@ phoenix/server/api/mutations/prompt_version_tag_mutations.py,sha256=t77osYb5he2A
201
201
  phoenix/server/api/mutations/span_annotations_mutations.py,sha256=sumBLUqRKlgMASWdWwYItmIJ2l7AyAp_PlIYeXYfguc,5970
202
202
  phoenix/server/api/mutations/trace_annotations_mutations.py,sha256=sEcEt8hbMt8YMiRX5o3xcJ5rWWZbDeBPMNz2teZoi3U,5945
203
203
  phoenix/server/api/mutations/trace_mutations.py,sha256=D5h2HYdlTo6yYZNq-O-PjaS9GeiZHxxVaOxDdh7fwjw,2957
204
- phoenix/server/api/mutations/user_mutations.py,sha256=FgZXQjCqmJ_0dyqiXv6NoGF7TjOO2a0aX7xDMDKtl-A,13603
204
+ phoenix/server/api/mutations/user_mutations.py,sha256=5pgtNKZwOnP1pbuegnNMwPn5u4teeWJ6vZSIcjDSCUg,14055
205
205
  phoenix/server/api/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
206
206
  phoenix/server/api/openapi/main.py,sha256=yKdzJYI4cxy_1mFcK4_7YObIcuRviBIfwNjB23RG14k,461
207
207
  phoenix/server/api/openapi/schema.py,sha256=WGmHWSIyJhtc5EIh_M3vlXU-EgHkFuTlyVofgS0kj1I,529
@@ -210,13 +210,14 @@ phoenix/server/api/routers/auth.py,sha256=T774FE5mqrfRSSYo1snpR5NIp3YzAJnsLsY9FJ
210
210
  phoenix/server/api/routers/embeddings.py,sha256=BpZGJee0pdL0W5Rp1L0b30dEtZTgJeVqXky8LgZ0ZXw,898
211
211
  phoenix/server/api/routers/oauth2.py,sha256=qXSUmnHPoRS8extKB8qUD8zbnUerAHeoEyEiVPiCyek,17285
212
212
  phoenix/server/api/routers/utils.py,sha256=M41BoH-fl37izhRuN2aX7lWm7jOC20A_3uClv9TVUUY,583
213
- phoenix/server/api/routers/v1/__init__.py,sha256=ZATgnXA3bM4fKfHrAx1kJE2sUowVxD2BsR5MzUwpDRs,2302
213
+ phoenix/server/api/routers/v1/__init__.py,sha256=_diWcDH9P49f_p0g2E8zkh-eVQkcSYCVdBO_zLccS8o,2393
214
214
  phoenix/server/api/routers/v1/datasets.py,sha256=gHlF4x0EmWiJ-8vwJygoh0bO3gvDBmi6vYnLAbSkQw4,37057
215
215
  phoenix/server/api/routers/v1/evaluations.py,sha256=RpOkTylp5Da6BvPZGuN8ksnxz_BVXRIwyOvwX9Iko8U,12647
216
216
  phoenix/server/api/routers/v1/experiment_evaluations.py,sha256=vx4CKlE84sAL1vtPiM_XWnbfrATQujOSzzduJDYgcyM,4829
217
217
  phoenix/server/api/routers/v1/experiment_runs.py,sha256=bInuasRv7ogiYf8fq-LwpJ5tptmMQsBNDlJAqwdymko,6378
218
218
  phoenix/server/api/routers/v1/experiments.py,sha256=V9_sxqLTE1MKGFu9H3FEdGKr70lYMbGZx813MGaavfQ,20430
219
219
  phoenix/server/api/routers/v1/models.py,sha256=r0nM2kFJ3mxDqgc5vFr1cjNuyOPs3RIKE_DS2VMdF48,1749
220
+ phoenix/server/api/routers/v1/projects.py,sha256=qv4RffYVGCJQuJ3FzaTVkueY8qKY0-GUbG9eSdm7oRY,14181
220
221
  phoenix/server/api/routers/v1/prompts.py,sha256=aBOUBwLDzZDIzJQkxJcR8ZKnakNJOLMwzsLKINSs1mA,26545
221
222
  phoenix/server/api/routers/v1/spans.py,sha256=uoU_bwIgz86fuvPjP5sX8goDyuCcnsTig-x3f17p60U,9625
222
223
  phoenix/server/api/routers/v1/traces.py,sha256=hSv35QIB4mwFgp53rOpz3zWIiSwbZzQnjafD790QuJU,7908
@@ -299,10 +300,11 @@ phoenix/server/api/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
299
300
  phoenix/server/api/types/node.py,sha256=BLl_IOFr0zrqUxaAtGLGui5aeM5VNVXFTzGeAKrztr0,822
300
301
  phoenix/server/api/types/pagination.py,sha256=BXm46gXZfrBS4hpiLvVSEdsbb29ctUMVJYjKXlOLxUA,9064
301
302
  phoenix/server/email/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
302
- phoenix/server/email/sender.py,sha256=Df-fZKbK_01KEkjo8UnDiYSuU69YfknyI1neZK_kmwA,3388
303
- phoenix/server/email/types.py,sha256=URLIMwtQWbbDEKLWjZTw9snCxsfxqRyoXNyByttbVWE,213
303
+ phoenix/server/email/sender.py,sha256=PU6YVnsoIT9ClYrTyuii-lklifHjdMmN7dStR4f0ZFE,4066
304
+ phoenix/server/email/types.py,sha256=IO2bTtCh-1cve-xiM4MWnunCCVNOQ3Z2cqTqF7vH-do,466
304
305
  phoenix/server/email/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
305
306
  phoenix/server/email/templates/password_reset.html,sha256=jv0Pe-06JloPZcubRWxPdAFHYEn9eDj_4SjmuoIwshI,441
307
+ phoenix/server/email/templates/welcome.html,sha256=E7RaTUH8VBGMcZ5RYIUukiKigJdLJ3_L6FsgoLkazEE,299
306
308
  phoenix/server/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
307
309
  phoenix/server/static/apple-touch-icon-114x114.png,sha256=xtFVXAYQnJkpUApg2D1hltSTuyO4Is4sD4A0ZkikiVU,9486
308
310
  phoenix/server/static/apple-touch-icon-120x120.png,sha256=iqZVAk634BbjJMozA8aHYyw15JjhIlIrG41FA2DFFaE,9957
@@ -314,16 +316,16 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
314
316
  phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
315
317
  phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
316
318
  phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
317
- phoenix/server/static/.vite/manifest.json,sha256=aDi-Uk9t65RmDnEk-9GWEFulGc6ogxP_FMVJrkIPZvI,2165
318
- phoenix/server/static/assets/components-BAc4OPED.js,sha256=VL_AageEdDLGbyR_LF0v8sfcbYtLllsbqrOflzE2OLo,455490
319
- phoenix/server/static/assets/index-Du53xkjY.js,sha256=kqiASYQH1J0Zg5c2Jz5XL4UhDOW8pqhhAfJITONFot8,60397
320
- phoenix/server/static/assets/pages-Dz-gbBPF.js,sha256=gBI9zPk6YJhSia8YDopK6JgtsxwqVeheWpLY76N43gw,862644
321
- phoenix/server/static/assets/vendor-CEisxXSv.js,sha256=TS8_RMXjkexQB-ENo06BZS77xnf8TMf68k03NhEZyVE,2510162
319
+ phoenix/server/static/.vite/manifest.json,sha256=HoxYrYg2l2nq5chl9GKo0pnga42JFMShSC0Kr4wLX5o,2165
320
+ phoenix/server/static/assets/components-B6cljCxu.js,sha256=VQZ_xj8sB55iva4mG-oFIPElEaArjo0-GF8hWOQ7L8c,455620
321
+ phoenix/server/static/assets/index-DfHKoAV9.js,sha256=OmqicccaM0lPOLABn3D6U-igVTGSREXbM4ZFiKWYGMI,60397
322
+ phoenix/server/static/assets/pages-Dhitcl5V.js,sha256=CdjHOTXth5jc1mqFO-ofYgFTglKUtVlBwiYEK8SBRJE,863027
323
+ phoenix/server/static/assets/vendor-C3H3sezv.js,sha256=Ask2DJPiTV2k5gNIopK_nOQFKCysUP5Z7h1gWVsFB3c,2510162
322
324
  phoenix/server/static/assets/vendor-Cg6lcjUC.css,sha256=nZrkr0u6NNElFGvpWHk9GTHeGoibCXCli1bE7mXZGZg,1816
323
- phoenix/server/static/assets/vendor-arizeai-BCTsSnvS.js,sha256=bOMRhNcnPkdTP12LEkBKJmvRBJwA5BKMxZvpkAX_irg,193248
324
- phoenix/server/static/assets/vendor-codemirror-DIWnRs_7.js,sha256=JkqAk2m_o589elkpaXuwlOaq0lfDrLK09-6D9eBBcKQ,781264
325
- phoenix/server/static/assets/vendor-recharts-Bame54mG.js,sha256=fhgG0ZqpDuiVwh71xq7Xb9vHxjnhxHOhp179cTnNwpM,282109
326
- phoenix/server/static/assets/vendor-shiki-Cc73E4D-.js,sha256=YEuUQiR8772y7nz6Eu1rdhl5evg5XJkRGxuwKcxCNqk,8980312
325
+ phoenix/server/static/assets/vendor-arizeai-DT8pwHfH.js,sha256=JraS2i2tsj4ToS_KGjq9BWS5zv4_FohPgF-vPV3MxEE,193248
326
+ phoenix/server/static/assets/vendor-codemirror-DvimrGxD.js,sha256=PC19rYrHBq_ICslA4t7DQpbazcNj5o0sA43H-j_Hzy0,781264
327
+ phoenix/server/static/assets/vendor-recharts-DuSQBcYW.js,sha256=FMnvLAuTQLvIU1IrPvWcIQNhSSSmOqf459tvo4kc8bA,282109
328
+ phoenix/server/static/assets/vendor-shiki-i05Hmswh.js,sha256=FQAOrcfIN8lFrk8VXMO6xMcnJdBxXh6CVvyMA8ZXj-M,8980312
327
329
  phoenix/server/static/assets/vendor-three-C5WAXd5r.js,sha256=ELkg06u70N7h8oFmvqdoHyPuUf9VgGEWeT4LKFx4VWo,620975
328
330
  phoenix/server/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
329
331
  phoenix/server/templates/index.html,sha256=e8_jdi7Eo19SK7DI_gglkTW094D17E0VAegoMmmmvIc,4330
@@ -331,7 +333,7 @@ phoenix/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
331
333
  phoenix/session/client.py,sha256=nXSn2Zmf9wTxgSe11Y9kKSLClkG8pNEAJgfEmy4mPm8,35234
332
334
  phoenix/session/data_extractor.py,sha256=Y0RzYFaNy9fQj8PEIeQ76TBZ90_E1FW7bXu3K5x0EZY,2782
333
335
  phoenix/session/evaluation.py,sha256=Q3fOMNELvqkk-b6a6PKc8pDJdsNQ0ZbTpseUSA2NKqs,5300
334
- phoenix/session/session.py,sha256=acCqJlPeczUDQcowXGvb8vcR1GcWqOxAn7oRBK3IL7U,27560
336
+ phoenix/session/session.py,sha256=paP01LC8g3_ABbiQRbqSaIcXaUa7I0WpbcLf-IeR3-k,28199
335
337
  phoenix/trace/__init__.py,sha256=ujk_uYjM8gmm-YqnyXxF-kekfwid0bcaPMTtNNcaw6U,407
336
338
  phoenix/trace/attributes.py,sha256=hyEKYZWPCP4NRmW7VmiC2voa3TH7FYKUBR9DYiVfXlw,12627
337
339
  phoenix/trace/errors.py,sha256=wB1z8qdPckngdfU-TORToekvg3344oNFAA83_hC2yFY,180
@@ -364,9 +366,9 @@ phoenix/utilities/project.py,sha256=auVpARXkDb-JgeX5f2aStyFIkeKvGwN9l7qrFeJMVxI,
364
366
  phoenix/utilities/re.py,sha256=6YyUWIkv0zc2SigsxfOWIHzdpjKA_TZo2iqKq7zJKvw,2081
365
367
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
366
368
  phoenix/utilities/template_formatters.py,sha256=gh9PJD6WEGw7TEYXfSst1UR4pWWwmjxMLrDVQ_CkpkQ,2779
367
- arize_phoenix-8.22.1.dist-info/METADATA,sha256=B4K95KRoJE5a4GmTA33RQ-3XI9H2kvRGjUjB7yMLpKs,21423
368
- arize_phoenix-8.22.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
369
- arize_phoenix-8.22.1.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
370
- arize_phoenix-8.22.1.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
371
- arize_phoenix-8.22.1.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
372
- arize_phoenix-8.22.1.dist-info/RECORD,,
369
+ arize_phoenix-8.24.0.dist-info/METADATA,sha256=Pceh1Pt4Y_uZNJas5q6Zyx2syDJg5xmVDP3Krvzk2ho,24495
370
+ arize_phoenix-8.24.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
371
+ arize_phoenix-8.24.0.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
372
+ arize_phoenix-8.24.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
373
+ arize_phoenix-8.24.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
374
+ arize_phoenix-8.24.0.dist-info/RECORD,,
phoenix/config.py CHANGED
@@ -8,10 +8,11 @@ from enum import Enum
8
8
  from importlib.metadata import version
9
9
  from pathlib import Path
10
10
  from typing import Any, Optional, Union, cast, overload
11
- from urllib.parse import quote_plus, urlparse
11
+ from urllib.parse import quote_plus, urljoin, urlparse
12
12
 
13
13
  import wrapt
14
14
  from email_validator import EmailNotValidError, validate_email
15
+ from starlette.datastructures import URL
15
16
 
16
17
  from phoenix.utilities.logging import log_a_list
17
18
 
@@ -182,6 +183,19 @@ If the username or email address already exists in the database, the user record
182
183
  modified, e.g., changed from non-admin to admin. Changing this environment variable for the next
183
184
  startup will not undo any records created in previous startups.
184
185
  """
186
+ ENV_PHOENIX_ROOT_URL = "PHOENIX_ROOT_URL"
187
+ """
188
+ This is the full URL used to access Phoenix from a web browser. This setting is important when
189
+ you have a reverse proxy in front of Phoenix. If the reverse proxy exposes Phoenix through a
190
+ sub-path, add that sub-path to the end of this URL setting.
191
+
192
+ WARNING: When a sub-path is needed, you must also specify the sub-path via the environment
193
+ variable PHOENIX_HOST_ROOT_PATH. Setting just this URL setting is not enough.
194
+
195
+ Examples:
196
+ - With a sub-path: "https://example.com/phoenix"
197
+ - Without a sub-path: "https://phoenix.example.com"
198
+ """
185
199
 
186
200
 
187
201
  # SMTP settings
@@ -209,7 +223,11 @@ ENV_PHOENIX_SMTP_VALIDATE_CERTS = "PHOENIX_SMTP_VALIDATE_CERTS"
209
223
  """
210
224
  Whether to validate SMTP server certificates. Defaults to true.
211
225
  """
212
-
226
+ ENV_PHOENIX_ALLOWED_ORIGINS = "PHOENIX_ALLOWED_ORIGINS"
227
+ """
228
+ List of allowed origins for CORS. Defaults to None.
229
+ When set to None, CORS is disabled.
230
+ """
213
231
  # API extension settings
214
232
  ENV_PHOENIX_FASTAPI_MIDDLEWARE_PATHS = "PHOENIX_FASTAPI_MIDDLEWARE_PATHS"
215
233
  ENV_PHOENIX_GQL_EXTENSION_PATHS = "PHOENIX_GQL_EXTENSION_PATHS"
@@ -824,7 +842,7 @@ def get_env_host() -> str:
824
842
 
825
843
 
826
844
  def get_env_host_root_path() -> str:
827
- if (host_root_path := getenv(ENV_PHOENIX_HOST_ROOT_PATH)) is None:
845
+ if not (host_root_path := getenv(ENV_PHOENIX_HOST_ROOT_PATH)):
828
846
  return HOST_ROOT_PATH
829
847
  if not host_root_path.startswith("/"):
830
848
  raise ValueError(
@@ -890,7 +908,33 @@ def get_env_client_headers() -> dict[str, str]:
890
908
  return headers
891
909
 
892
910
 
911
+ def get_env_root_url() -> URL:
912
+ """
913
+ Get the URL used to access Phoenix from a web browser
914
+
915
+ Returns:
916
+ URL: The root URL of the Phoenix server
917
+
918
+ Note:
919
+ This is intended to replace the legacy `get_base_url()` helper function. In
920
+ particular, `get_env_collector_endpoint()` is really for the client and should be
921
+ deprecated on the server side.
922
+ """
923
+ if root_url := getenv(ENV_PHOENIX_ROOT_URL):
924
+ result = urlparse(root_url)
925
+ if not result.scheme or not result.netloc:
926
+ raise ValueError(
927
+ f"The environment variable `{ENV_PHOENIX_ROOT_URL}` must be a valid URL."
928
+ )
929
+ return URL(root_url)
930
+ host = get_env_host()
931
+ if host == "0.0.0.0":
932
+ host = "127.0.0.1"
933
+ return URL(urljoin(f"http://{host}:{get_env_port()}", get_env_host_root_path()))
934
+
935
+
893
936
  def get_base_url() -> str:
937
+ """Deprecated: Use get_env_root_url() instead, but note the difference in behavior."""
894
938
  host = get_env_host()
895
939
  if host == "0.0.0.0":
896
940
  host = "127.0.0.1"
@@ -1049,4 +1093,22 @@ def get_env_disable_migrations() -> bool:
1049
1093
  DEFAULT_PROJECT_NAME = "default"
1050
1094
  _KUBERNETES_PHOENIX_PORT_PATTERN = re.compile(r"^tcp://\d{1,3}[.]\d{1,3}[.]\d{1,3}[.]\d{1,3}:\d+$")
1051
1095
 
1096
+
1097
+ def get_env_allowed_origins() -> Optional[list[str]]:
1098
+ """
1099
+ Gets the value of the PHOENIX_ALLOWED_ORIGINS environment variable.
1100
+ """
1101
+ allowed_origins = getenv(ENV_PHOENIX_ALLOWED_ORIGINS)
1102
+ if allowed_origins is None:
1103
+ return None
1104
+
1105
+ return allowed_origins.split(",")
1106
+
1107
+
1108
+ def verify_server_environment_variables() -> None:
1109
+ """Verify that the environment variables are set correctly. Raises an error otherwise."""
1110
+ get_env_root_url()
1111
+
1112
+
1052
1113
  SKLEARN_VERSION = cast(tuple[int, int], tuple(map(int, version("scikit-learn").split(".", 2)[:2])))
1114
+ PLAYGROUND_PROJECT_NAME = "playground"
phoenix/db/facilitator.py CHANGED
@@ -1,15 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import logging
4
5
  import secrets
6
+ from asyncio import gather
5
7
  from functools import partial
8
+ from typing import Optional
6
9
 
7
10
  from sqlalchemy import (
8
11
  distinct,
9
12
  insert,
10
13
  select,
11
14
  )
12
- from sqlalchemy.ext.asyncio import AsyncSession
13
15
 
14
16
  from phoenix.auth import (
15
17
  DEFAULT_ADMIN_EMAIL,
@@ -25,8 +27,11 @@ from phoenix.config import (
25
27
  )
26
28
  from phoenix.db import models
27
29
  from phoenix.db.enums import COLUMN_ENUMS, UserRole
30
+ from phoenix.server.email.types import WelcomeEmailSender
28
31
  from phoenix.server.types import DbSessionFactory
29
32
 
33
+ logger = logging.getLogger(__name__)
34
+
30
35
 
31
36
  class Facilitator:
32
37
  """
@@ -36,20 +41,25 @@ class Facilitator:
36
41
  carried out as callbacks at the very beginning of Starlette's lifespan process.
37
42
  """
38
43
 
39
- def __init__(self, *, db: DbSessionFactory) -> None:
44
+ def __init__(
45
+ self,
46
+ *,
47
+ db: DbSessionFactory,
48
+ email_sender: Optional[WelcomeEmailSender] = None,
49
+ ) -> None:
40
50
  self._db = db
51
+ self._email_sender = email_sender
41
52
 
42
53
  async def __call__(self) -> None:
43
54
  for fn in (
44
55
  _ensure_enums,
45
56
  _ensure_user_roles,
46
- _ensure_admins,
57
+ partial(_ensure_admins, email_sender=self._email_sender),
47
58
  ):
48
- async with self._db() as session:
49
- await fn(session)
59
+ await fn(self._db)
50
60
 
51
61
 
52
- async def _ensure_enums(session: AsyncSession) -> None:
62
+ async def _ensure_enums(db: DbSessionFactory) -> None:
53
63
  """
54
64
  Ensure that all enum values are present in their respective tables. If any values are missing,
55
65
  they will be added. If any values are present in the database but not in the enum, an error will
@@ -57,99 +67,122 @@ async def _ensure_enums(session: AsyncSession) -> None:
57
67
  """
58
68
  for column, enum in COLUMN_ENUMS.items():
59
69
  table = column.class_
60
- existing = set([_ async for _ in await session.stream_scalars(select(distinct(column)))])
61
- expected = set(e.value for e in enum)
62
- if unexpected := existing - expected:
63
- raise ValueError(f"Unexpected values in {table.name}.{column.key}: {unexpected}")
64
- if not (missing := expected - existing):
65
- continue
66
- await session.execute(insert(table), [{column.key: v} for v in missing])
70
+ async with db() as session:
71
+ existing = set(
72
+ [_ async for _ in await session.stream_scalars(select(distinct(column)))]
73
+ )
74
+ expected = set(e.value for e in enum)
75
+ if unexpected := existing - expected:
76
+ raise ValueError(f"Unexpected values in {table.name}.{column.key}: {unexpected}")
77
+ if not (missing := expected - existing):
78
+ continue
79
+ await session.execute(insert(table), [{column.key: v} for v in missing])
67
80
 
68
81
 
69
- async def _ensure_user_roles(session: AsyncSession) -> None:
82
+ async def _ensure_user_roles(db: DbSessionFactory) -> None:
70
83
  """
71
84
  Ensure that the system and admin roles are present in the database. If they are not, they will
72
85
  be added. The system user will have the email "system@localhost" and the admin user will have
73
86
  the email "admin@localhost".
74
87
  """
75
- role_ids = {
76
- name: id_
77
- async for name, id_ in await session.stream(
78
- select(models.UserRole.name, models.UserRole.id)
79
- )
80
- }
81
- existing_roles = [
82
- name
83
- async for name in await session.stream_scalars(
84
- select(distinct(models.UserRole.name)).join_from(models.User, models.UserRole)
85
- )
86
- ]
87
- if (system_role := UserRole.SYSTEM.value) not in existing_roles and (
88
- system_role_id := role_ids.get(system_role)
89
- ) is not None:
90
- system_user = models.User(
91
- user_role_id=system_role_id,
92
- username=DEFAULT_SYSTEM_USERNAME,
93
- email=DEFAULT_SYSTEM_EMAIL,
94
- reset_password=False,
95
- password_salt=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
96
- password_hash=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
97
- )
98
- session.add(system_user)
99
- if (admin_role := UserRole.ADMIN.value) not in existing_roles and (
100
- admin_role_id := role_ids.get(admin_role)
101
- ) is not None:
102
- salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
103
- password = get_env_default_admin_initial_password()
104
- compute = partial(compute_password_hash, password=password, salt=salt)
105
- loop = asyncio.get_running_loop()
106
- hash_ = await loop.run_in_executor(None, compute)
107
- admin_user = models.User(
108
- user_role_id=admin_role_id,
109
- username=DEFAULT_ADMIN_USERNAME,
110
- email=DEFAULT_ADMIN_EMAIL,
111
- password_salt=salt,
112
- password_hash=hash_,
113
- reset_password=True,
114
- )
115
- session.add(admin_user)
116
- await session.flush()
88
+ async with db() as session:
89
+ role_ids = {
90
+ name: id_
91
+ async for name, id_ in await session.stream(
92
+ select(models.UserRole.name, models.UserRole.id)
93
+ )
94
+ }
95
+ existing_roles = [
96
+ name
97
+ async for name in await session.stream_scalars(
98
+ select(distinct(models.UserRole.name)).join_from(models.User, models.UserRole)
99
+ )
100
+ ]
101
+ if (system_role := UserRole.SYSTEM.value) not in existing_roles and (
102
+ system_role_id := role_ids.get(system_role)
103
+ ) is not None:
104
+ system_user = models.User(
105
+ user_role_id=system_role_id,
106
+ username=DEFAULT_SYSTEM_USERNAME,
107
+ email=DEFAULT_SYSTEM_EMAIL,
108
+ reset_password=False,
109
+ password_salt=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
110
+ password_hash=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
111
+ )
112
+ session.add(system_user)
113
+ if (admin_role := UserRole.ADMIN.value) not in existing_roles and (
114
+ admin_role_id := role_ids.get(admin_role)
115
+ ) is not None:
116
+ salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
117
+ password = get_env_default_admin_initial_password()
118
+ compute = partial(compute_password_hash, password=password, salt=salt)
119
+ loop = asyncio.get_running_loop()
120
+ hash_ = await loop.run_in_executor(None, compute)
121
+ admin_user = models.User(
122
+ user_role_id=admin_role_id,
123
+ username=DEFAULT_ADMIN_USERNAME,
124
+ email=DEFAULT_ADMIN_EMAIL,
125
+ password_salt=salt,
126
+ password_hash=hash_,
127
+ reset_password=True,
128
+ )
129
+ session.add(admin_user)
130
+ await session.flush()
117
131
 
118
132
 
119
- async def _ensure_admins(session: AsyncSession) -> None:
133
+ async def _ensure_admins(
134
+ db: DbSessionFactory,
135
+ *,
136
+ email_sender: Optional[WelcomeEmailSender] = None,
137
+ ) -> None:
120
138
  """
121
139
  Ensure that all startup admin users are present in the database. If any are missing, they will
122
140
  be added. Existing records will not be modified.
123
141
  """
124
142
  if not (admins := get_env_admins()):
125
143
  return
126
- existing_emails = set(
127
- await session.scalars(select(models.User.email).where(models.User.email.in_(admins.keys())))
128
- )
129
- admins = {email: username for email, username in admins.items() if email not in existing_emails}
130
- if not admins:
131
- return
132
- existing_usernames = set(
133
- await session.scalars(
134
- select(models.User.username).where(models.User.username.in_(admins.values()))
144
+ async with db() as session:
145
+ existing_emails = set(
146
+ await session.scalars(
147
+ select(models.User.email).where(models.User.email.in_(admins.keys()))
148
+ )
135
149
  )
136
- )
137
- admins = {
138
- email: username for email, username in admins.items() if username not in existing_usernames
139
- }
140
- if not admins:
141
- return
142
- admin_role_id = await session.scalar(
143
- select(models.UserRole.id).filter_by(name=UserRole.ADMIN.value)
144
- )
145
- assert admin_role_id is not None, "Admin role not found in database"
146
- for email, username in admins.items():
147
- values = dict(
148
- user_role_id=admin_role_id,
149
- username=username,
150
- email=email,
151
- password_salt=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
152
- password_hash=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
153
- reset_password=True,
150
+ admins = {
151
+ email: username for email, username in admins.items() if email not in existing_emails
152
+ }
153
+ if not admins:
154
+ return
155
+ existing_usernames = set(
156
+ await session.scalars(
157
+ select(models.User.username).where(models.User.username.in_(admins.values()))
158
+ )
154
159
  )
155
- await session.execute(insert(models.User).values(values))
160
+ admins = {
161
+ email: username
162
+ for email, username in admins.items()
163
+ if username not in existing_usernames
164
+ }
165
+ if not admins:
166
+ return
167
+ admin_role_id = await session.scalar(
168
+ select(models.UserRole.id).filter_by(name=UserRole.ADMIN.value)
169
+ )
170
+ assert admin_role_id is not None, "Admin role not found in database"
171
+ for email, username in admins.items():
172
+ values = dict(
173
+ user_role_id=admin_role_id,
174
+ username=username,
175
+ email=email,
176
+ password_salt=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
177
+ password_hash=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
178
+ reset_password=True,
179
+ )
180
+ await session.execute(insert(models.User).values(values))
181
+ if email_sender is None:
182
+ return
183
+ for exc in await gather(
184
+ *(email_sender.send_welcome_email(email, username) for email, username in admins.items()),
185
+ return_exceptions=True,
186
+ ):
187
+ if isinstance(exc, Exception):
188
+ logger.error(f"Failed to send welcome email: {exc}")
phoenix/db/helpers.py CHANGED
@@ -19,6 +19,7 @@ from sqlalchemy import (
19
19
  )
20
20
  from typing_extensions import assert_never
21
21
 
22
+ from phoenix.config import PLAYGROUND_PROJECT_NAME
22
23
  from phoenix.db import models
23
24
 
24
25
 
@@ -157,3 +158,18 @@ def get_dataset_example_revisions(
157
158
  ),
158
159
  )
159
160
  )
161
+
162
+
163
+ _AnyTuple = TypeVar("_AnyTuple", bound=tuple[Any, ...])
164
+
165
+
166
+ def exclude_experiment_projects(
167
+ stmt: Select[_AnyTuple],
168
+ ) -> Select[_AnyTuple]:
169
+ return stmt.outerjoin(
170
+ models.Experiment,
171
+ and_(
172
+ models.Project.name == models.Experiment.project_name,
173
+ models.Experiment.project_name != PLAYGROUND_PROJECT_NAME,
174
+ ),
175
+ ).where(models.Experiment.project_name.is_(None))
@@ -53,6 +53,7 @@ from phoenix.server.api.dataloaders import (
53
53
  )
54
54
  from phoenix.server.bearer_auth import PhoenixUser
55
55
  from phoenix.server.dml_event import DmlEvent
56
+ from phoenix.server.email.types import EmailSender
56
57
  from phoenix.server.types import (
57
58
  CanGetLastUpdatedAt,
58
59
  CanPutItem,
@@ -124,6 +125,7 @@ class Context(BaseContext):
124
125
  auth_enabled: bool = False
125
126
  secret: Optional[str] = None
126
127
  token_store: Optional[TokenStore] = None
128
+ email_sender: Optional[EmailSender] = None
127
129
 
128
130
  def get_secret(self) -> str:
129
131
  """A type-safe way to get the application secret. Throws an error if the secret is not set.