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,3 +1,4 @@
1
+ import logging
1
2
  import secrets
2
3
  from contextlib import AsyncExitStack
3
4
  from datetime import datetime, timezone
@@ -32,6 +33,8 @@ from phoenix.server.api.types.User import User, to_gql_user
32
33
  from phoenix.server.bearer_auth import PhoenixUser
33
34
  from phoenix.server.types import AccessTokenId, ApiKeyId, PasswordResetTokenId, RefreshTokenId
34
35
 
36
+ logger = logging.getLogger(__name__)
37
+
35
38
 
36
39
  @strawberry.input
37
40
  class CreateUserInput:
@@ -39,6 +42,7 @@ class CreateUserInput:
39
42
  username: str
40
43
  password: str
41
44
  role: UserRoleInput
45
+ send_welcome_email: Optional[bool] = False
42
46
 
43
47
 
44
48
  @strawberry.input
@@ -111,6 +115,12 @@ class UserMutationMixin:
111
115
  await session.flush()
112
116
  except (PostgreSQLIntegrityError, SQLiteIntegrityError) as error:
113
117
  raise Conflict(_user_operation_error_message(error))
118
+ if input.send_welcome_email and info.context.email_sender is not None:
119
+ try:
120
+ await info.context.email_sender.send_welcome_email(user.email, user.username)
121
+ except Exception as error:
122
+ # Log the error but do not raise it
123
+ logger.error(f"Failed to send welcome email: {error}")
114
124
  return UserMutationPayload(user=to_gql_user(user))
115
125
 
116
126
  @strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin, IsLocked]) # type: ignore
@@ -19,7 +19,7 @@ from phoenix.config import (
19
19
  getenv,
20
20
  )
21
21
  from phoenix.db import enums, models
22
- from phoenix.db.helpers import SupportedSQLDialect
22
+ from phoenix.db.helpers import SupportedSQLDialect, exclude_experiment_projects
23
23
  from phoenix.db.models import DatasetExample as OrmExample
24
24
  from phoenix.db.models import DatasetExampleRevision as OrmRevision
25
25
  from phoenix.db.models import DatasetVersion as OrmVersion
@@ -42,7 +42,6 @@ from phoenix.server.api.input_types.ClusterInput import ClusterInput
42
42
  from phoenix.server.api.input_types.Coordinates import InputCoordinate2D, InputCoordinate3D
43
43
  from phoenix.server.api.input_types.DatasetSort import DatasetSort
44
44
  from phoenix.server.api.input_types.InvocationParameters import InvocationParameter
45
- from phoenix.server.api.subscriptions import PLAYGROUND_PROJECT_NAME
46
45
  from phoenix.server.api.types.Cluster import Cluster, to_gql_clusters
47
46
  from phoenix.server.api.types.Dataset import Dataset, to_gql_dataset
48
47
  from phoenix.server.api.types.DatasetExample import DatasetExample
@@ -233,18 +232,8 @@ class Query:
233
232
  last=last,
234
233
  before=before if isinstance(before, CursorString) else None,
235
234
  )
236
- stmt = (
237
- select(models.Project)
238
- .outerjoin(
239
- models.Experiment,
240
- and_(
241
- models.Project.name == models.Experiment.project_name,
242
- models.Experiment.project_name != PLAYGROUND_PROJECT_NAME,
243
- ),
244
- )
245
- .where(models.Experiment.project_name.is_(None))
246
- .order_by(models.Project.id)
247
- )
235
+ stmt = select(models.Project).order_by(models.Project.id)
236
+ stmt = exclude_experiment_projects(stmt)
248
237
  async with info.context.db() as session:
249
238
  projects = await session.stream_scalars(stmt)
250
239
  data = [
@@ -9,6 +9,7 @@ from .evaluations import router as evaluations_router
9
9
  from .experiment_evaluations import router as experiment_evaluations_router
10
10
  from .experiment_runs import router as experiment_runs_router
11
11
  from .experiments import router as experiments_router
12
+ from .projects import router as projects_router
12
13
  from .prompts import router as prompts_router
13
14
  from .spans import router as spans_router
14
15
  from .traces import router as traces_router
@@ -63,4 +64,5 @@ def create_v1_router(authentication_enabled: bool) -> APIRouter:
63
64
  router.include_router(spans_router)
64
65
  router.include_router(evaluations_router)
65
66
  router.include_router(prompts_router)
67
+ router.include_router(projects_router)
66
68
  return router
@@ -0,0 +1,393 @@
1
+ from typing import Optional
2
+
3
+ from fastapi import APIRouter, HTTPException, Path, Query
4
+ from pydantic import Field
5
+ from sqlalchemy import select
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+ from starlette.requests import Request
8
+ from starlette.status import (
9
+ HTTP_204_NO_CONTENT,
10
+ HTTP_403_FORBIDDEN,
11
+ HTTP_404_NOT_FOUND,
12
+ HTTP_422_UNPROCESSABLE_ENTITY,
13
+ )
14
+ from strawberry.relay import GlobalID
15
+
16
+ from phoenix.config import DEFAULT_PROJECT_NAME
17
+ from phoenix.db import models
18
+ from phoenix.db.enums import UserRole
19
+ from phoenix.db.helpers import exclude_experiment_projects
20
+ from phoenix.server.api.routers.v1.models import V1RoutesBaseModel
21
+ from phoenix.server.api.routers.v1.utils import (
22
+ PaginatedResponseBody,
23
+ ResponseBody,
24
+ add_errors_to_responses,
25
+ )
26
+ from phoenix.server.api.types.node import from_global_id_with_expected_type
27
+ from phoenix.server.api.types.Project import Project as ProjectNodeType
28
+
29
+ router = APIRouter(tags=["projects"])
30
+
31
+
32
+ class ProjectData(V1RoutesBaseModel):
33
+ name: str = Field(..., min_length=1)
34
+ description: Optional[str] = None
35
+
36
+
37
+ class Project(ProjectData):
38
+ id: str
39
+
40
+
41
+ class GetProjectsResponseBody(PaginatedResponseBody[Project]):
42
+ pass
43
+
44
+
45
+ class GetProjectResponseBody(ResponseBody[Project]):
46
+ pass
47
+
48
+
49
+ class CreateProjectRequestBody(ProjectData):
50
+ pass
51
+
52
+
53
+ class CreateProjectResponseBody(ResponseBody[Project]):
54
+ pass
55
+
56
+
57
+ class UpdateProjectRequestBody(V1RoutesBaseModel):
58
+ description: Optional[str] = None
59
+
60
+
61
+ class UpdateProjectResponseBody(ResponseBody[Project]):
62
+ pass
63
+
64
+
65
+ @router.get(
66
+ "/projects",
67
+ operation_id="getProjects",
68
+ summary="List all projects", # noqa: E501
69
+ description="Retrieve a paginated list of all projects in the system.", # noqa: E501
70
+ response_description="A list of projects with pagination information", # noqa: E501
71
+ responses=add_errors_to_responses(
72
+ [
73
+ HTTP_422_UNPROCESSABLE_ENTITY,
74
+ ]
75
+ ),
76
+ )
77
+ async def get_projects(
78
+ request: Request,
79
+ cursor: Optional[str] = Query(
80
+ default=None,
81
+ description="Cursor for pagination (project ID)",
82
+ ),
83
+ limit: int = Query(
84
+ default=100, description="The max number of projects to return at a time.", gt=0
85
+ ),
86
+ include_experiment_projects: bool = Query(
87
+ default=False,
88
+ description="Include experiment projects in the response. Experiment projects are created from running experiments.", # noqa: E501
89
+ ),
90
+ ) -> GetProjectsResponseBody:
91
+ """
92
+ Retrieve a paginated list of all projects in the system.
93
+
94
+ Args:
95
+ request (Request): The FastAPI request object.
96
+ cursor (Optional[str]): Pagination cursor (project ID).
97
+ limit (int): Maximum number of projects to return per request.
98
+ include_experiment_projects (bool): Flag to include experiment projects in the response.
99
+ Experiment projects are created from running experiments.
100
+
101
+ Returns:
102
+ GetProjectsResponseBody: Response containing a list of projects and pagination information.
103
+
104
+ Raises:
105
+ HTTPException: If the cursor format is invalid.
106
+ """ # noqa: E501
107
+ stmt = select(models.Project).order_by(models.Project.id.desc())
108
+ if not include_experiment_projects:
109
+ stmt = exclude_experiment_projects(stmt)
110
+ async with request.app.state.db() as session:
111
+ if cursor:
112
+ try:
113
+ cursor_id = GlobalID.from_id(cursor).node_id
114
+ stmt = stmt.filter(models.Project.id <= int(cursor_id))
115
+ except ValueError:
116
+ raise HTTPException(
117
+ detail=f"Invalid cursor format: {cursor}",
118
+ status_code=HTTP_422_UNPROCESSABLE_ENTITY,
119
+ )
120
+
121
+ stmt = stmt.limit(limit + 1)
122
+ projects = (await session.scalars(stmt)).all()
123
+
124
+ if not projects:
125
+ return GetProjectsResponseBody(next_cursor=None, data=[])
126
+
127
+ next_cursor = None
128
+ if len(projects) == limit + 1:
129
+ last_project = projects[-1]
130
+ next_cursor = str(GlobalID(ProjectNodeType.__name__, str(last_project.id)))
131
+ projects = projects[:-1]
132
+
133
+ project_responses = [_to_project_response(project) for project in projects]
134
+ return GetProjectsResponseBody(next_cursor=next_cursor, data=project_responses)
135
+
136
+
137
+ @router.get(
138
+ "/projects/{project_identifier}",
139
+ operation_id="getProject",
140
+ summary="Get project by ID or name", # noqa: E501
141
+ description="Retrieve a specific project using its unique identifier: either project ID or project name. Note: When using a project name as the identifier, it cannot contain slash (/), question mark (?), or pound sign (#) characters.", # noqa: E501
142
+ response_description="The requested project", # noqa: E501
143
+ responses=add_errors_to_responses(
144
+ [
145
+ HTTP_404_NOT_FOUND,
146
+ HTTP_422_UNPROCESSABLE_ENTITY,
147
+ ]
148
+ ),
149
+ )
150
+ async def get_project(
151
+ request: Request,
152
+ project_identifier: str = Path(
153
+ description="The project identifier: either project ID or project name. If using a project name, it cannot contain slash (/), question mark (?), or pound sign (#) characters.", # noqa: E501
154
+ ),
155
+ ) -> GetProjectResponseBody:
156
+ """
157
+ Retrieve a specific project by its ID or name.
158
+
159
+ Args:
160
+ request (Request): The FastAPI request object.
161
+ project_identifier (str): The project identifier: either project ID or project name.
162
+ If using a project name, it cannot contain slash (/), question mark (?), or pound sign (#) characters.
163
+
164
+ Returns:
165
+ GetProjectResponseBody: Response containing the requested project.
166
+
167
+ Raises:
168
+ HTTPException: If the project identifier format is invalid or the project is not found.
169
+ """ # noqa: E501
170
+ async with request.app.state.db() as session:
171
+ project = await _get_project_by_identifier(session, project_identifier)
172
+ data = _to_project_response(project)
173
+ return GetProjectResponseBody(data=data)
174
+
175
+
176
+ @router.post(
177
+ "/projects",
178
+ operation_id="createProject",
179
+ summary="Create a new project", # noqa: E501
180
+ description="Create a new project with the specified configuration.", # noqa: E501
181
+ response_description="The newly created project", # noqa: E501
182
+ responses=add_errors_to_responses(
183
+ [
184
+ HTTP_422_UNPROCESSABLE_ENTITY,
185
+ ]
186
+ ),
187
+ )
188
+ async def create_project(
189
+ request: Request,
190
+ request_body: CreateProjectRequestBody,
191
+ ) -> CreateProjectResponseBody:
192
+ """
193
+ Create a new project.
194
+
195
+ Args:
196
+ request (Request): The FastAPI request object.
197
+ request_body (CreateProjectRequestBody): The request body containing project data.
198
+
199
+ Returns:
200
+ CreateProjectResponseBody: Response containing the created project.
201
+
202
+ Raises:
203
+ HTTPException: If any validation error occurs.
204
+ """
205
+ async with request.app.state.db() as session:
206
+ project = models.Project(
207
+ name=request_body.name,
208
+ description=request_body.description,
209
+ )
210
+ session.add(project)
211
+ await session.flush()
212
+ data = _to_project_response(project)
213
+ return CreateProjectResponseBody(data=data)
214
+
215
+
216
+ @router.put(
217
+ "/projects/{project_identifier}",
218
+ operation_id="updateProject",
219
+ summary="Update a project by ID or name", # noqa: E501
220
+ description="Update an existing project with new configuration. Project names cannot be changed. The project identifier is either project ID or project name. Note: When using a project name as the identifier, it cannot contain slash (/), question mark (?), or pound sign (#) characters.", # noqa: E501
221
+ response_description="The updated project", # noqa: E501
222
+ responses=add_errors_to_responses(
223
+ [
224
+ HTTP_403_FORBIDDEN,
225
+ HTTP_404_NOT_FOUND,
226
+ HTTP_422_UNPROCESSABLE_ENTITY,
227
+ ]
228
+ ),
229
+ )
230
+ async def update_project(
231
+ request: Request,
232
+ request_body: UpdateProjectRequestBody,
233
+ project_identifier: str = Path(
234
+ description="The project identifier: either project ID or project name. If using a project name, it cannot contain slash (/), question mark (?), or pound sign (#) characters.", # noqa: E501
235
+ ),
236
+ ) -> UpdateProjectResponseBody:
237
+ """
238
+ Update an existing project.
239
+
240
+ Args:
241
+ request (Request): The FastAPI request object.
242
+ request_body (UpdateProjectRequestBody): The request body containing the new description.
243
+ project_identifier (str): The project identifier: either project ID or project name.
244
+ If using a project name, it cannot contain slash (/), question mark (?), or pound sign (#) characters.
245
+
246
+ Returns:
247
+ UpdateProjectResponseBody: Response containing the updated project.
248
+
249
+ Raises:
250
+ HTTPException: If the project identifier format is invalid or the project is not found.
251
+ """ # noqa: E501
252
+ if request.app.state.authentication_enabled:
253
+ async with request.app.state.db() as session:
254
+ # Check if the user is an admin
255
+ stmt = (
256
+ select(models.UserRole.name)
257
+ .join(models.User)
258
+ .where(models.User.id == int(request.user.identity))
259
+ )
260
+ role_name = await session.scalar(stmt)
261
+ if role_name != UserRole.ADMIN.value:
262
+ raise HTTPException(
263
+ status_code=HTTP_403_FORBIDDEN,
264
+ detail="Only admins can update projects",
265
+ )
266
+ async with request.app.state.db() as session:
267
+ project = await _get_project_by_identifier(session, project_identifier)
268
+
269
+ # Update the description if provided
270
+ if request_body.description is not None:
271
+ project.description = request_body.description
272
+
273
+ data = _to_project_response(project)
274
+ return UpdateProjectResponseBody(data=data)
275
+
276
+
277
+ @router.delete(
278
+ "/projects/{project_identifier}",
279
+ operation_id="deleteProject",
280
+ summary="Delete a project by ID or name", # noqa: E501
281
+ description="Delete an existing project and all its associated data. The project identifier is either project ID or project name. The default project cannot be deleted. Note: When using a project name as the identifier, it cannot contain slash (/), question mark (?), or pound sign (#) characters.", # noqa: E501
282
+ response_description="No content returned on successful deletion", # noqa: E501
283
+ status_code=HTTP_204_NO_CONTENT,
284
+ responses=add_errors_to_responses(
285
+ [
286
+ HTTP_403_FORBIDDEN,
287
+ HTTP_404_NOT_FOUND,
288
+ HTTP_422_UNPROCESSABLE_ENTITY,
289
+ ]
290
+ ),
291
+ )
292
+ async def delete_project(
293
+ request: Request,
294
+ project_identifier: str = Path(
295
+ description="The project identifier: either project ID or project name. If using a project name, it cannot contain slash (/), question mark (?), or pound sign (#) characters.", # noqa: E501
296
+ ),
297
+ ) -> None:
298
+ """
299
+ Delete an existing project.
300
+
301
+ Args:
302
+ request (Request): The FastAPI request object.
303
+ project_identifier (str): The project identifier: either project ID or project name.
304
+ If using a project name, it cannot contain slash (/), question mark (?), or pound sign (#) characters.
305
+
306
+ Returns:
307
+ None: Returns a 204 No Content response on success.
308
+
309
+ Raises:
310
+ HTTPException: If the project identifier format is invalid, the project is not found, or it's the default project.
311
+ """ # noqa: E501
312
+ if request.app.state.authentication_enabled:
313
+ async with request.app.state.db() as session:
314
+ # Check if the user is an admin
315
+ stmt = (
316
+ select(models.UserRole.name)
317
+ .join(models.User)
318
+ .where(models.User.id == int(request.user.identity))
319
+ )
320
+ role_name = await session.scalar(stmt)
321
+ if role_name != UserRole.ADMIN.value:
322
+ raise HTTPException(
323
+ status_code=HTTP_403_FORBIDDEN,
324
+ detail="Only admins can delete projects",
325
+ )
326
+ async with request.app.state.db() as session:
327
+ project = await _get_project_by_identifier(session, project_identifier)
328
+
329
+ # The default project must not be deleted - it's forbidden
330
+ if project.name == DEFAULT_PROJECT_NAME:
331
+ raise HTTPException(
332
+ status_code=HTTP_403_FORBIDDEN,
333
+ detail="The default project cannot be deleted",
334
+ )
335
+
336
+ await session.delete(project)
337
+ return None
338
+
339
+
340
+ def _to_project_response(project: models.Project) -> Project:
341
+ return Project(
342
+ id=str(GlobalID(ProjectNodeType.__name__, str(project.id))),
343
+ name=project.name,
344
+ description=project.description,
345
+ )
346
+
347
+
348
+ async def _get_project_by_identifier(
349
+ session: AsyncSession,
350
+ project_identifier: str,
351
+ ) -> models.Project:
352
+ """
353
+ Get a project by its ID or name.
354
+
355
+ Args:
356
+ session: The database session.
357
+ project_identifier: The project ID or name.
358
+
359
+ Returns:
360
+ The project object.
361
+
362
+ Raises:
363
+ HTTPException: If the identifier format is invalid or the project is not found.
364
+ """
365
+ # Try to parse as a GlobalID first
366
+ try:
367
+ id_ = from_global_id_with_expected_type(
368
+ GlobalID.from_id(project_identifier),
369
+ ProjectNodeType.__name__,
370
+ )
371
+ except Exception:
372
+ try:
373
+ name = project_identifier
374
+ except HTTPException:
375
+ raise HTTPException(
376
+ status_code=HTTP_422_UNPROCESSABLE_ENTITY,
377
+ detail=f"Invalid project identifier format: {project_identifier}",
378
+ )
379
+ stmt = select(models.Project).filter_by(name=name)
380
+ project = await session.scalar(stmt)
381
+ if project is None:
382
+ raise HTTPException(
383
+ status_code=HTTP_404_NOT_FOUND,
384
+ detail=f"Project with name {name} not found",
385
+ )
386
+ else:
387
+ project = await session.get(models.Project, id_)
388
+ if project is None:
389
+ raise HTTPException(
390
+ status_code=HTTP_404_NOT_FOUND,
391
+ detail=f"Project with ID {project_identifier} not found",
392
+ )
393
+ return project
@@ -23,6 +23,7 @@ from strawberry.relay.types import GlobalID
23
23
  from strawberry.types import Info
24
24
  from typing_extensions import TypeAlias, assert_never
25
25
 
26
+ from phoenix.config import PLAYGROUND_PROJECT_NAME
26
27
  from phoenix.datetime_utils import local_now, normalize_datetime
27
28
  from phoenix.db import models
28
29
  from phoenix.server.api.auth import IsLocked, IsNotReadOnly
@@ -84,7 +85,6 @@ ChatCompletionResult: TypeAlias = tuple[
84
85
  DatasetExampleID, Optional[models.Span], models.ExperimentRun
85
86
  ]
86
87
  ChatStream: TypeAlias = AsyncGenerator[ChatCompletionSubscriptionPayload, None]
87
- PLAYGROUND_PROJECT_NAME = "playground"
88
88
 
89
89
 
90
90
  @strawberry.type
phoenix/server/app.py CHANGED
@@ -24,6 +24,7 @@ from urllib.parse import urlparse
24
24
 
25
25
  import strawberry
26
26
  from fastapi import APIRouter, Depends, FastAPI
27
+ from fastapi.middleware.cors import CORSMiddleware
27
28
  from fastapi.middleware.gzip import GZipMiddleware
28
29
  from fastapi.utils import is_body_allowed_for_status_code
29
30
  from grpc.aio import ServerInterceptor
@@ -59,6 +60,7 @@ from phoenix.config import (
59
60
  get_env_host,
60
61
  get_env_port,
61
62
  server_instrumentation_is_enabled,
63
+ verify_server_environment_variables,
62
64
  )
63
65
  from phoenix.core.model_schema import Model
64
66
  from phoenix.db import models
@@ -550,6 +552,7 @@ def create_graphql_router(
550
552
  read_only: bool = False,
551
553
  secret: Optional[str] = None,
552
554
  token_store: Optional[TokenStore] = None,
555
+ email_sender: Optional[EmailSender] = None,
553
556
  ) -> GraphQLRouter[Context, None]:
554
557
  """Creates the GraphQL router.
555
558
 
@@ -565,6 +568,8 @@ def create_graphql_router(
565
568
  cache_for_dataloaders (Optional[CacheForDataLoaders], optional): GraphQL data loaders.
566
569
  read_only (bool, optional): Marks the app as read-only. Defaults to False.
567
570
  secret (Optional[str], optional): The application secret for auth. Defaults to None.
571
+ token_store (Optional[TokenStore], optional): The token store for auth. Defaults to None.
572
+ email_sender (Optional[EmailSender], optional): The email sender. Defaults to None.
568
573
 
569
574
  Returns:
570
575
  GraphQLRouter: The router mounted at /graphql
@@ -653,6 +658,7 @@ def create_graphql_router(
653
658
  auth_enabled=authentication_enabled,
654
659
  secret=secret,
655
660
  token_store=token_store,
661
+ email_sender=email_sender,
656
662
  )
657
663
 
658
664
  return GraphQLRouter(
@@ -765,7 +771,9 @@ def create_app(
765
771
  email_sender: Optional[EmailSender] = None,
766
772
  oauth2_client_configs: Optional[list[OAuth2ClientConfig]] = None,
767
773
  bulk_inserter_factory: Optional[Callable[..., BulkInserter]] = None,
774
+ allowed_origins: Optional[list[str]] = None,
768
775
  ) -> FastAPI:
776
+ verify_server_environment_variables()
769
777
  if model.embedding_dimensions:
770
778
  try:
771
779
  import fast_hdbscan # noqa: F401
@@ -868,6 +876,7 @@ def create_app(
868
876
  read_only=read_only,
869
877
  secret=secret,
870
878
  token_store=token_store,
879
+ email_sender=email_sender,
871
880
  )
872
881
  if enable_prometheus:
873
882
  from phoenix.server.prometheus import PrometheusMiddleware
@@ -948,6 +957,14 @@ def create_app(
948
957
  FastAPIInstrumentor().instrument(tracer_provider=tracer_provider)
949
958
  FastAPIInstrumentor.instrument_app(app, tracer_provider=tracer_provider)
950
959
  shutdown_callbacks_list.append(FastAPIInstrumentor().uninstrument)
960
+ if allowed_origins:
961
+ app.add_middleware(
962
+ CORSMiddleware,
963
+ allow_origins=allowed_origins,
964
+ allow_credentials=True,
965
+ allow_methods=["*"],
966
+ allow_headers=["*"],
967
+ )
951
968
  return app
952
969
 
953
970