arize-phoenix 3.16.1__py3-none-any.whl → 7.7.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 (338) hide show
  1. arize_phoenix-7.7.0.dist-info/METADATA +261 -0
  2. arize_phoenix-7.7.0.dist-info/RECORD +345 -0
  3. {arize_phoenix-3.16.1.dist-info → arize_phoenix-7.7.0.dist-info}/WHEEL +1 -1
  4. arize_phoenix-7.7.0.dist-info/entry_points.txt +3 -0
  5. phoenix/__init__.py +86 -14
  6. phoenix/auth.py +309 -0
  7. phoenix/config.py +675 -45
  8. phoenix/core/model.py +32 -30
  9. phoenix/core/model_schema.py +102 -109
  10. phoenix/core/model_schema_adapter.py +48 -45
  11. phoenix/datetime_utils.py +24 -3
  12. phoenix/db/README.md +54 -0
  13. phoenix/db/__init__.py +4 -0
  14. phoenix/db/alembic.ini +85 -0
  15. phoenix/db/bulk_inserter.py +294 -0
  16. phoenix/db/engines.py +208 -0
  17. phoenix/db/enums.py +20 -0
  18. phoenix/db/facilitator.py +113 -0
  19. phoenix/db/helpers.py +159 -0
  20. phoenix/db/insertion/constants.py +2 -0
  21. phoenix/db/insertion/dataset.py +227 -0
  22. phoenix/db/insertion/document_annotation.py +171 -0
  23. phoenix/db/insertion/evaluation.py +191 -0
  24. phoenix/db/insertion/helpers.py +98 -0
  25. phoenix/db/insertion/span.py +193 -0
  26. phoenix/db/insertion/span_annotation.py +158 -0
  27. phoenix/db/insertion/trace_annotation.py +158 -0
  28. phoenix/db/insertion/types.py +256 -0
  29. phoenix/db/migrate.py +86 -0
  30. phoenix/db/migrations/data_migration_scripts/populate_project_sessions.py +199 -0
  31. phoenix/db/migrations/env.py +114 -0
  32. phoenix/db/migrations/script.py.mako +26 -0
  33. phoenix/db/migrations/versions/10460e46d750_datasets.py +317 -0
  34. phoenix/db/migrations/versions/3be8647b87d8_add_token_columns_to_spans_table.py +126 -0
  35. phoenix/db/migrations/versions/4ded9e43755f_create_project_sessions_table.py +66 -0
  36. phoenix/db/migrations/versions/cd164e83824f_users_and_tokens.py +157 -0
  37. phoenix/db/migrations/versions/cf03bd6bae1d_init.py +280 -0
  38. phoenix/db/models.py +807 -0
  39. phoenix/exceptions.py +5 -1
  40. phoenix/experiments/__init__.py +6 -0
  41. phoenix/experiments/evaluators/__init__.py +29 -0
  42. phoenix/experiments/evaluators/base.py +158 -0
  43. phoenix/experiments/evaluators/code_evaluators.py +184 -0
  44. phoenix/experiments/evaluators/llm_evaluators.py +473 -0
  45. phoenix/experiments/evaluators/utils.py +236 -0
  46. phoenix/experiments/functions.py +772 -0
  47. phoenix/experiments/tracing.py +86 -0
  48. phoenix/experiments/types.py +726 -0
  49. phoenix/experiments/utils.py +25 -0
  50. phoenix/inferences/__init__.py +0 -0
  51. phoenix/{datasets → inferences}/errors.py +6 -5
  52. phoenix/{datasets → inferences}/fixtures.py +49 -42
  53. phoenix/{datasets/dataset.py → inferences/inferences.py} +121 -105
  54. phoenix/{datasets → inferences}/schema.py +11 -11
  55. phoenix/{datasets → inferences}/validation.py +13 -14
  56. phoenix/logging/__init__.py +3 -0
  57. phoenix/logging/_config.py +90 -0
  58. phoenix/logging/_filter.py +6 -0
  59. phoenix/logging/_formatter.py +69 -0
  60. phoenix/metrics/__init__.py +5 -4
  61. phoenix/metrics/binning.py +4 -3
  62. phoenix/metrics/metrics.py +2 -1
  63. phoenix/metrics/mixins.py +7 -6
  64. phoenix/metrics/retrieval_metrics.py +2 -1
  65. phoenix/metrics/timeseries.py +5 -4
  66. phoenix/metrics/wrappers.py +9 -3
  67. phoenix/pointcloud/clustering.py +5 -5
  68. phoenix/pointcloud/pointcloud.py +7 -5
  69. phoenix/pointcloud/projectors.py +5 -6
  70. phoenix/pointcloud/umap_parameters.py +53 -52
  71. phoenix/server/api/README.md +28 -0
  72. phoenix/server/api/auth.py +44 -0
  73. phoenix/server/api/context.py +152 -9
  74. phoenix/server/api/dataloaders/__init__.py +91 -0
  75. phoenix/server/api/dataloaders/annotation_summaries.py +139 -0
  76. phoenix/server/api/dataloaders/average_experiment_run_latency.py +54 -0
  77. phoenix/server/api/dataloaders/cache/__init__.py +3 -0
  78. phoenix/server/api/dataloaders/cache/two_tier_cache.py +68 -0
  79. phoenix/server/api/dataloaders/dataset_example_revisions.py +131 -0
  80. phoenix/server/api/dataloaders/dataset_example_spans.py +38 -0
  81. phoenix/server/api/dataloaders/document_evaluation_summaries.py +144 -0
  82. phoenix/server/api/dataloaders/document_evaluations.py +31 -0
  83. phoenix/server/api/dataloaders/document_retrieval_metrics.py +89 -0
  84. phoenix/server/api/dataloaders/experiment_annotation_summaries.py +79 -0
  85. phoenix/server/api/dataloaders/experiment_error_rates.py +58 -0
  86. phoenix/server/api/dataloaders/experiment_run_annotations.py +36 -0
  87. phoenix/server/api/dataloaders/experiment_run_counts.py +49 -0
  88. phoenix/server/api/dataloaders/experiment_sequence_number.py +44 -0
  89. phoenix/server/api/dataloaders/latency_ms_quantile.py +188 -0
  90. phoenix/server/api/dataloaders/min_start_or_max_end_times.py +85 -0
  91. phoenix/server/api/dataloaders/project_by_name.py +31 -0
  92. phoenix/server/api/dataloaders/record_counts.py +116 -0
  93. phoenix/server/api/dataloaders/session_io.py +79 -0
  94. phoenix/server/api/dataloaders/session_num_traces.py +30 -0
  95. phoenix/server/api/dataloaders/session_num_traces_with_error.py +32 -0
  96. phoenix/server/api/dataloaders/session_token_usages.py +41 -0
  97. phoenix/server/api/dataloaders/session_trace_latency_ms_quantile.py +55 -0
  98. phoenix/server/api/dataloaders/span_annotations.py +26 -0
  99. phoenix/server/api/dataloaders/span_dataset_examples.py +31 -0
  100. phoenix/server/api/dataloaders/span_descendants.py +57 -0
  101. phoenix/server/api/dataloaders/span_projects.py +33 -0
  102. phoenix/server/api/dataloaders/token_counts.py +124 -0
  103. phoenix/server/api/dataloaders/trace_by_trace_ids.py +25 -0
  104. phoenix/server/api/dataloaders/trace_root_spans.py +32 -0
  105. phoenix/server/api/dataloaders/user_roles.py +30 -0
  106. phoenix/server/api/dataloaders/users.py +33 -0
  107. phoenix/server/api/exceptions.py +48 -0
  108. phoenix/server/api/helpers/__init__.py +12 -0
  109. phoenix/server/api/helpers/dataset_helpers.py +217 -0
  110. phoenix/server/api/helpers/experiment_run_filters.py +763 -0
  111. phoenix/server/api/helpers/playground_clients.py +948 -0
  112. phoenix/server/api/helpers/playground_registry.py +70 -0
  113. phoenix/server/api/helpers/playground_spans.py +455 -0
  114. phoenix/server/api/input_types/AddExamplesToDatasetInput.py +16 -0
  115. phoenix/server/api/input_types/AddSpansToDatasetInput.py +14 -0
  116. phoenix/server/api/input_types/ChatCompletionInput.py +38 -0
  117. phoenix/server/api/input_types/ChatCompletionMessageInput.py +24 -0
  118. phoenix/server/api/input_types/ClearProjectInput.py +15 -0
  119. phoenix/server/api/input_types/ClusterInput.py +2 -2
  120. phoenix/server/api/input_types/CreateDatasetInput.py +12 -0
  121. phoenix/server/api/input_types/CreateSpanAnnotationInput.py +18 -0
  122. phoenix/server/api/input_types/CreateTraceAnnotationInput.py +18 -0
  123. phoenix/server/api/input_types/DataQualityMetricInput.py +5 -2
  124. phoenix/server/api/input_types/DatasetExampleInput.py +14 -0
  125. phoenix/server/api/input_types/DatasetSort.py +17 -0
  126. phoenix/server/api/input_types/DatasetVersionSort.py +16 -0
  127. phoenix/server/api/input_types/DeleteAnnotationsInput.py +7 -0
  128. phoenix/server/api/input_types/DeleteDatasetExamplesInput.py +13 -0
  129. phoenix/server/api/input_types/DeleteDatasetInput.py +7 -0
  130. phoenix/server/api/input_types/DeleteExperimentsInput.py +7 -0
  131. phoenix/server/api/input_types/DimensionFilter.py +4 -4
  132. phoenix/server/api/input_types/GenerativeModelInput.py +17 -0
  133. phoenix/server/api/input_types/Granularity.py +1 -1
  134. phoenix/server/api/input_types/InvocationParameters.py +162 -0
  135. phoenix/server/api/input_types/PatchAnnotationInput.py +19 -0
  136. phoenix/server/api/input_types/PatchDatasetExamplesInput.py +35 -0
  137. phoenix/server/api/input_types/PatchDatasetInput.py +14 -0
  138. phoenix/server/api/input_types/PerformanceMetricInput.py +5 -2
  139. phoenix/server/api/input_types/ProjectSessionSort.py +29 -0
  140. phoenix/server/api/input_types/SpanAnnotationSort.py +17 -0
  141. phoenix/server/api/input_types/SpanSort.py +134 -69
  142. phoenix/server/api/input_types/TemplateOptions.py +10 -0
  143. phoenix/server/api/input_types/TraceAnnotationSort.py +17 -0
  144. phoenix/server/api/input_types/UserRoleInput.py +9 -0
  145. phoenix/server/api/mutations/__init__.py +28 -0
  146. phoenix/server/api/mutations/api_key_mutations.py +167 -0
  147. phoenix/server/api/mutations/chat_mutations.py +593 -0
  148. phoenix/server/api/mutations/dataset_mutations.py +591 -0
  149. phoenix/server/api/mutations/experiment_mutations.py +75 -0
  150. phoenix/server/api/{types/ExportEventsMutation.py → mutations/export_events_mutations.py} +21 -18
  151. phoenix/server/api/mutations/project_mutations.py +57 -0
  152. phoenix/server/api/mutations/span_annotations_mutations.py +128 -0
  153. phoenix/server/api/mutations/trace_annotations_mutations.py +127 -0
  154. phoenix/server/api/mutations/user_mutations.py +329 -0
  155. phoenix/server/api/openapi/__init__.py +0 -0
  156. phoenix/server/api/openapi/main.py +17 -0
  157. phoenix/server/api/openapi/schema.py +16 -0
  158. phoenix/server/api/queries.py +738 -0
  159. phoenix/server/api/routers/__init__.py +11 -0
  160. phoenix/server/api/routers/auth.py +284 -0
  161. phoenix/server/api/routers/embeddings.py +26 -0
  162. phoenix/server/api/routers/oauth2.py +488 -0
  163. phoenix/server/api/routers/v1/__init__.py +64 -0
  164. phoenix/server/api/routers/v1/datasets.py +1017 -0
  165. phoenix/server/api/routers/v1/evaluations.py +362 -0
  166. phoenix/server/api/routers/v1/experiment_evaluations.py +115 -0
  167. phoenix/server/api/routers/v1/experiment_runs.py +167 -0
  168. phoenix/server/api/routers/v1/experiments.py +308 -0
  169. phoenix/server/api/routers/v1/pydantic_compat.py +78 -0
  170. phoenix/server/api/routers/v1/spans.py +267 -0
  171. phoenix/server/api/routers/v1/traces.py +208 -0
  172. phoenix/server/api/routers/v1/utils.py +95 -0
  173. phoenix/server/api/schema.py +44 -241
  174. phoenix/server/api/subscriptions.py +597 -0
  175. phoenix/server/api/types/Annotation.py +21 -0
  176. phoenix/server/api/types/AnnotationSummary.py +55 -0
  177. phoenix/server/api/types/AnnotatorKind.py +16 -0
  178. phoenix/server/api/types/ApiKey.py +27 -0
  179. phoenix/server/api/types/AuthMethod.py +9 -0
  180. phoenix/server/api/types/ChatCompletionMessageRole.py +11 -0
  181. phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +46 -0
  182. phoenix/server/api/types/Cluster.py +25 -24
  183. phoenix/server/api/types/CreateDatasetPayload.py +8 -0
  184. phoenix/server/api/types/DataQualityMetric.py +31 -13
  185. phoenix/server/api/types/Dataset.py +288 -63
  186. phoenix/server/api/types/DatasetExample.py +85 -0
  187. phoenix/server/api/types/DatasetExampleRevision.py +34 -0
  188. phoenix/server/api/types/DatasetVersion.py +14 -0
  189. phoenix/server/api/types/Dimension.py +32 -31
  190. phoenix/server/api/types/DocumentEvaluationSummary.py +9 -8
  191. phoenix/server/api/types/EmbeddingDimension.py +56 -49
  192. phoenix/server/api/types/Evaluation.py +25 -31
  193. phoenix/server/api/types/EvaluationSummary.py +30 -50
  194. phoenix/server/api/types/Event.py +20 -20
  195. phoenix/server/api/types/ExampleRevisionInterface.py +14 -0
  196. phoenix/server/api/types/Experiment.py +152 -0
  197. phoenix/server/api/types/ExperimentAnnotationSummary.py +13 -0
  198. phoenix/server/api/types/ExperimentComparison.py +17 -0
  199. phoenix/server/api/types/ExperimentRun.py +119 -0
  200. phoenix/server/api/types/ExperimentRunAnnotation.py +56 -0
  201. phoenix/server/api/types/GenerativeModel.py +9 -0
  202. phoenix/server/api/types/GenerativeProvider.py +85 -0
  203. phoenix/server/api/types/Inferences.py +80 -0
  204. phoenix/server/api/types/InferencesRole.py +23 -0
  205. phoenix/server/api/types/LabelFraction.py +7 -0
  206. phoenix/server/api/types/MimeType.py +2 -2
  207. phoenix/server/api/types/Model.py +54 -54
  208. phoenix/server/api/types/PerformanceMetric.py +8 -5
  209. phoenix/server/api/types/Project.py +407 -142
  210. phoenix/server/api/types/ProjectSession.py +139 -0
  211. phoenix/server/api/types/Segments.py +4 -4
  212. phoenix/server/api/types/Span.py +221 -176
  213. phoenix/server/api/types/SpanAnnotation.py +43 -0
  214. phoenix/server/api/types/SpanIOValue.py +15 -0
  215. phoenix/server/api/types/SystemApiKey.py +9 -0
  216. phoenix/server/api/types/TemplateLanguage.py +10 -0
  217. phoenix/server/api/types/TimeSeries.py +19 -15
  218. phoenix/server/api/types/TokenUsage.py +11 -0
  219. phoenix/server/api/types/Trace.py +154 -0
  220. phoenix/server/api/types/TraceAnnotation.py +45 -0
  221. phoenix/server/api/types/UMAPPoints.py +7 -7
  222. phoenix/server/api/types/User.py +60 -0
  223. phoenix/server/api/types/UserApiKey.py +45 -0
  224. phoenix/server/api/types/UserRole.py +15 -0
  225. phoenix/server/api/types/node.py +4 -112
  226. phoenix/server/api/types/pagination.py +156 -57
  227. phoenix/server/api/utils.py +34 -0
  228. phoenix/server/app.py +864 -115
  229. phoenix/server/bearer_auth.py +163 -0
  230. phoenix/server/dml_event.py +136 -0
  231. phoenix/server/dml_event_handler.py +256 -0
  232. phoenix/server/email/__init__.py +0 -0
  233. phoenix/server/email/sender.py +97 -0
  234. phoenix/server/email/templates/__init__.py +0 -0
  235. phoenix/server/email/templates/password_reset.html +19 -0
  236. phoenix/server/email/types.py +11 -0
  237. phoenix/server/grpc_server.py +102 -0
  238. phoenix/server/jwt_store.py +505 -0
  239. phoenix/server/main.py +305 -116
  240. phoenix/server/oauth2.py +52 -0
  241. phoenix/server/openapi/__init__.py +0 -0
  242. phoenix/server/prometheus.py +111 -0
  243. phoenix/server/rate_limiters.py +188 -0
  244. phoenix/server/static/.vite/manifest.json +87 -0
  245. phoenix/server/static/assets/components-Cy9nwIvF.js +2125 -0
  246. phoenix/server/static/assets/index-BKvHIxkk.js +113 -0
  247. phoenix/server/static/assets/pages-CUi2xCVQ.js +4449 -0
  248. phoenix/server/static/assets/vendor-DvC8cT4X.js +894 -0
  249. phoenix/server/static/assets/vendor-DxkFTwjz.css +1 -0
  250. phoenix/server/static/assets/vendor-arizeai-Do1793cv.js +662 -0
  251. phoenix/server/static/assets/vendor-codemirror-BzwZPyJM.js +24 -0
  252. phoenix/server/static/assets/vendor-recharts-_Jb7JjhG.js +59 -0
  253. phoenix/server/static/assets/vendor-shiki-Cl9QBraO.js +5 -0
  254. phoenix/server/static/assets/vendor-three-DwGkEfCM.js +2998 -0
  255. phoenix/server/telemetry.py +68 -0
  256. phoenix/server/templates/index.html +82 -23
  257. phoenix/server/thread_server.py +3 -3
  258. phoenix/server/types.py +275 -0
  259. phoenix/services.py +27 -18
  260. phoenix/session/client.py +743 -68
  261. phoenix/session/data_extractor.py +31 -7
  262. phoenix/session/evaluation.py +3 -9
  263. phoenix/session/session.py +263 -219
  264. phoenix/settings.py +22 -0
  265. phoenix/trace/__init__.py +2 -22
  266. phoenix/trace/attributes.py +338 -0
  267. phoenix/trace/dsl/README.md +116 -0
  268. phoenix/trace/dsl/filter.py +663 -213
  269. phoenix/trace/dsl/helpers.py +73 -21
  270. phoenix/trace/dsl/query.py +574 -201
  271. phoenix/trace/exporter.py +24 -19
  272. phoenix/trace/fixtures.py +368 -32
  273. phoenix/trace/otel.py +71 -219
  274. phoenix/trace/projects.py +3 -2
  275. phoenix/trace/schemas.py +33 -11
  276. phoenix/trace/span_evaluations.py +21 -16
  277. phoenix/trace/span_json_decoder.py +6 -4
  278. phoenix/trace/span_json_encoder.py +2 -2
  279. phoenix/trace/trace_dataset.py +47 -32
  280. phoenix/trace/utils.py +21 -4
  281. phoenix/utilities/__init__.py +0 -26
  282. phoenix/utilities/client.py +132 -0
  283. phoenix/utilities/deprecation.py +31 -0
  284. phoenix/utilities/error_handling.py +3 -2
  285. phoenix/utilities/json.py +109 -0
  286. phoenix/utilities/logging.py +8 -0
  287. phoenix/utilities/project.py +2 -2
  288. phoenix/utilities/re.py +49 -0
  289. phoenix/utilities/span_store.py +0 -23
  290. phoenix/utilities/template_formatters.py +99 -0
  291. phoenix/version.py +1 -1
  292. arize_phoenix-3.16.1.dist-info/METADATA +0 -495
  293. arize_phoenix-3.16.1.dist-info/RECORD +0 -178
  294. phoenix/core/project.py +0 -619
  295. phoenix/core/traces.py +0 -96
  296. phoenix/experimental/evals/__init__.py +0 -73
  297. phoenix/experimental/evals/evaluators.py +0 -413
  298. phoenix/experimental/evals/functions/__init__.py +0 -4
  299. phoenix/experimental/evals/functions/classify.py +0 -453
  300. phoenix/experimental/evals/functions/executor.py +0 -353
  301. phoenix/experimental/evals/functions/generate.py +0 -138
  302. phoenix/experimental/evals/functions/processing.py +0 -76
  303. phoenix/experimental/evals/models/__init__.py +0 -14
  304. phoenix/experimental/evals/models/anthropic.py +0 -175
  305. phoenix/experimental/evals/models/base.py +0 -170
  306. phoenix/experimental/evals/models/bedrock.py +0 -221
  307. phoenix/experimental/evals/models/litellm.py +0 -134
  308. phoenix/experimental/evals/models/openai.py +0 -448
  309. phoenix/experimental/evals/models/rate_limiters.py +0 -246
  310. phoenix/experimental/evals/models/vertex.py +0 -173
  311. phoenix/experimental/evals/models/vertexai.py +0 -186
  312. phoenix/experimental/evals/retrievals.py +0 -96
  313. phoenix/experimental/evals/templates/__init__.py +0 -50
  314. phoenix/experimental/evals/templates/default_templates.py +0 -472
  315. phoenix/experimental/evals/templates/template.py +0 -195
  316. phoenix/experimental/evals/utils/__init__.py +0 -172
  317. phoenix/experimental/evals/utils/threads.py +0 -27
  318. phoenix/server/api/helpers.py +0 -11
  319. phoenix/server/api/routers/evaluation_handler.py +0 -109
  320. phoenix/server/api/routers/span_handler.py +0 -70
  321. phoenix/server/api/routers/trace_handler.py +0 -60
  322. phoenix/server/api/types/DatasetRole.py +0 -23
  323. phoenix/server/static/index.css +0 -6
  324. phoenix/server/static/index.js +0 -7447
  325. phoenix/storage/span_store/__init__.py +0 -23
  326. phoenix/storage/span_store/text_file.py +0 -85
  327. phoenix/trace/dsl/missing.py +0 -60
  328. phoenix/trace/langchain/__init__.py +0 -3
  329. phoenix/trace/langchain/instrumentor.py +0 -35
  330. phoenix/trace/llama_index/__init__.py +0 -3
  331. phoenix/trace/llama_index/callback.py +0 -102
  332. phoenix/trace/openai/__init__.py +0 -3
  333. phoenix/trace/openai/instrumentor.py +0 -30
  334. {arize_phoenix-3.16.1.dist-info → arize_phoenix-7.7.0.dist-info}/licenses/IP_NOTICE +0 -0
  335. {arize_phoenix-3.16.1.dist-info → arize_phoenix-7.7.0.dist-info}/licenses/LICENSE +0 -0
  336. /phoenix/{datasets → db/insertion}/__init__.py +0 -0
  337. /phoenix/{experimental → db/migrations}/__init__.py +0 -0
  338. /phoenix/{storage → db/migrations/data_migration_scripts}/__init__.py +0 -0
phoenix/server/app.py CHANGED
@@ -1,39 +1,192 @@
1
+ import asyncio
2
+ import contextlib
3
+ import importlib
4
+ import json
1
5
  import logging
6
+ import os
7
+ from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence
8
+ from contextlib import AbstractAsyncContextManager, AsyncExitStack
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime, timedelta, timezone
11
+ from functools import cached_property
2
12
  from pathlib import Path
3
- from typing import Any, NamedTuple, Optional, Union
13
+ from types import MethodType
14
+ from typing import (
15
+ TYPE_CHECKING,
16
+ Any,
17
+ NamedTuple,
18
+ Optional,
19
+ TypedDict,
20
+ Union,
21
+ cast,
22
+ )
23
+ from urllib.parse import urlparse
4
24
 
5
- from starlette.applications import Starlette
6
- from starlette.datastructures import QueryParams
7
- from starlette.endpoints import HTTPEndpoint
8
- from starlette.exceptions import HTTPException
25
+ import strawberry
26
+ from fastapi import APIRouter, Depends, FastAPI
27
+ from fastapi.middleware.gzip import GZipMiddleware
28
+ from fastapi.utils import is_body_allowed_for_status_code
29
+ from grpc.aio import ServerInterceptor
30
+ from sqlalchemy import select
31
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
32
+ from starlette.datastructures import State as StarletteState
33
+ from starlette.exceptions import HTTPException, WebSocketException
9
34
  from starlette.middleware import Middleware
35
+ from starlette.middleware.authentication import AuthenticationMiddleware
10
36
  from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
11
37
  from starlette.requests import Request
12
- from starlette.responses import FileResponse, PlainTextResponse, Response
13
- from starlette.routing import Mount, Route, WebSocketRoute
38
+ from starlette.responses import JSONResponse, PlainTextResponse, Response
14
39
  from starlette.staticfiles import StaticFiles
40
+ from starlette.status import HTTP_401_UNAUTHORIZED
15
41
  from starlette.templating import Jinja2Templates
16
- from starlette.types import Scope
42
+ from starlette.types import Scope, StatefulLifespan
17
43
  from starlette.websockets import WebSocket
18
- from strawberry.asgi import GraphQL
19
- from strawberry.schema import BaseSchema
44
+ from strawberry.extensions import SchemaExtension
45
+ from strawberry.fastapi import GraphQLRouter
46
+ from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL
47
+ from typing_extensions import TypeAlias
20
48
 
21
- import phoenix
22
- from phoenix.config import SERVER_DIR
49
+ import phoenix.trace.v1 as pb
50
+ from phoenix.config import (
51
+ DEFAULT_PROJECT_NAME,
52
+ ENV_PHOENIX_CSRF_TRUSTED_ORIGINS,
53
+ SERVER_DIR,
54
+ OAuth2ClientConfig,
55
+ get_env_csrf_trusted_origins,
56
+ get_env_fastapi_middleware_paths,
57
+ get_env_gql_extension_paths,
58
+ get_env_grpc_interceptor_paths,
59
+ get_env_host,
60
+ get_env_port,
61
+ server_instrumentation_is_enabled,
62
+ )
23
63
  from phoenix.core.model_schema import Model
24
- from phoenix.core.traces import Traces
64
+ from phoenix.db import models
65
+ from phoenix.db.bulk_inserter import BulkInserter
66
+ from phoenix.db.engines import create_engine
67
+ from phoenix.db.facilitator import Facilitator
68
+ from phoenix.db.helpers import SupportedSQLDialect
69
+ from phoenix.exceptions import PhoenixMigrationError
25
70
  from phoenix.pointcloud.umap_parameters import UMAPParameters
26
- from phoenix.server.api.context import Context
27
- from phoenix.server.api.routers.evaluation_handler import EvaluationHandler
28
- from phoenix.server.api.routers.span_handler import SpanHandler
29
- from phoenix.server.api.routers.trace_handler import TraceHandler
30
- from phoenix.server.api.schema import schema
31
- from phoenix.storage.span_store import SpanStore
71
+ from phoenix.server.api.context import Context, DataLoaders
72
+ from phoenix.server.api.dataloaders import (
73
+ AnnotationSummaryDataLoader,
74
+ AverageExperimentRunLatencyDataLoader,
75
+ CacheForDataLoaders,
76
+ DatasetExampleRevisionsDataLoader,
77
+ DatasetExampleSpansDataLoader,
78
+ DocumentEvaluationsDataLoader,
79
+ DocumentEvaluationSummaryDataLoader,
80
+ DocumentRetrievalMetricsDataLoader,
81
+ ExperimentAnnotationSummaryDataLoader,
82
+ ExperimentErrorRatesDataLoader,
83
+ ExperimentRunAnnotations,
84
+ ExperimentRunCountsDataLoader,
85
+ ExperimentSequenceNumberDataLoader,
86
+ LatencyMsQuantileDataLoader,
87
+ MinStartOrMaxEndTimeDataLoader,
88
+ ProjectByNameDataLoader,
89
+ RecordCountDataLoader,
90
+ SessionIODataLoader,
91
+ SessionNumTracesDataLoader,
92
+ SessionNumTracesWithErrorDataLoader,
93
+ SessionTokenUsagesDataLoader,
94
+ SessionTraceLatencyMsQuantileDataLoader,
95
+ SpanAnnotationsDataLoader,
96
+ SpanDatasetExamplesDataLoader,
97
+ SpanDescendantsDataLoader,
98
+ SpanProjectsDataLoader,
99
+ TokenCountDataLoader,
100
+ TraceByTraceIdsDataLoader,
101
+ TraceRootSpansDataLoader,
102
+ UserRolesDataLoader,
103
+ UsersDataLoader,
104
+ )
105
+ from phoenix.server.api.routers import (
106
+ auth_router,
107
+ create_embeddings_router,
108
+ create_v1_router,
109
+ oauth2_router,
110
+ )
111
+ from phoenix.server.api.routers.v1 import REST_API_VERSION
112
+ from phoenix.server.api.schema import build_graphql_schema
113
+ from phoenix.server.bearer_auth import BearerTokenAuthBackend, is_authenticated
114
+ from phoenix.server.dml_event import DmlEvent
115
+ from phoenix.server.dml_event_handler import DmlEventHandler
116
+ from phoenix.server.email.types import EmailSender
117
+ from phoenix.server.grpc_server import GrpcServer
118
+ from phoenix.server.jwt_store import JwtStore
119
+ from phoenix.server.oauth2 import OAuth2Clients
120
+ from phoenix.server.telemetry import initialize_opentelemetry_tracer_provider
121
+ from phoenix.server.types import (
122
+ CanGetLastUpdatedAt,
123
+ CanPutItem,
124
+ DaemonTask,
125
+ DbSessionFactory,
126
+ LastUpdatedAt,
127
+ TokenStore,
128
+ )
129
+ from phoenix.trace.fixtures import (
130
+ TracesFixture,
131
+ get_dataset_fixtures,
132
+ get_evals_from_fixture,
133
+ get_trace_fixture_by_name,
134
+ load_example_traces,
135
+ reset_fixture_span_ids_and_timestamps,
136
+ send_dataset_fixtures,
137
+ )
138
+ from phoenix.trace.otel import decode_otlp_span, encode_span_to_otlp
139
+ from phoenix.trace.schemas import Span
140
+ from phoenix.utilities.client import PHOENIX_SERVER_VERSION_HEADER
141
+ from phoenix.version import __version__ as phoenix_version
142
+
143
+ if TYPE_CHECKING:
144
+ from opentelemetry.trace import TracerProvider
32
145
 
33
146
  logger = logging.getLogger(__name__)
34
147
 
148
+ router = APIRouter(include_in_schema=False)
149
+
35
150
  templates = Jinja2Templates(directory=SERVER_DIR / "templates")
36
151
 
152
+ """
153
+ Threshold (in minutes) to determine if database is booted up for the first time.
154
+
155
+ Used to assess whether the `default` project was created recently.
156
+ If so, demo data is automatically ingested upon initial boot up to populate the database.
157
+ """
158
+ NEW_DB_AGE_THRESHOLD_MINUTES = 2
159
+
160
+ ProjectName: TypeAlias = str
161
+ _Callback: TypeAlias = Callable[[], Union[None, Awaitable[None]]]
162
+
163
+
164
+ def import_object_from_file(file_path: str, object_name: str) -> Any:
165
+ """Import an object (class or function) from a Python file."""
166
+ try:
167
+ if not os.path.isfile(file_path):
168
+ raise FileNotFoundError(f"File '{file_path}' does not exist.")
169
+ module_name = f"custom_module_{hash(file_path)}"
170
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
171
+ if spec is None:
172
+ raise ImportError(f"Could not load spec for '{file_path}'")
173
+ module = importlib.util.module_from_spec(spec)
174
+ loader = spec.loader
175
+ if loader is None:
176
+ raise ImportError(f"No loader found for '{file_path}'")
177
+ loader.exec_module(module)
178
+ try:
179
+ return getattr(module, object_name)
180
+ except AttributeError:
181
+ raise ImportError(f"Module '{file_path}' does not have an object '{object_name}'.")
182
+ except Exception as e:
183
+ raise ImportError(f"Could not import '{object_name}' from '{file_path}': {e}")
184
+
185
+
186
+ class OAuth2Idp(TypedDict):
187
+ name: str
188
+ displayName: str
189
+
37
190
 
38
191
  class AppConfig(NamedTuple):
39
192
  has_inferences: bool
@@ -42,6 +195,12 @@ class AppConfig(NamedTuple):
42
195
  min_dist: float
43
196
  n_neighbors: int
44
197
  n_samples: int
198
+ is_development: bool
199
+ web_manifest_path: Path
200
+ authentication_enabled: bool
201
+ """ Whether authentication is enabled """
202
+ websockets_enabled: bool
203
+ oauth2_idps: Sequence[OAuth2Idp]
45
204
 
46
205
 
47
206
  class Static(StaticFiles):
@@ -53,6 +212,19 @@ class Static(StaticFiles):
53
212
  self._app_config = app_config
54
213
  super().__init__(**kwargs)
55
214
 
215
+ @cached_property
216
+ def _web_manifest(self) -> dict[str, Any]:
217
+ try:
218
+ with open(self._app_config.web_manifest_path, "r") as f:
219
+ return cast(dict[str, Any], json.load(f))
220
+ except FileNotFoundError as e:
221
+ if self._app_config.is_development:
222
+ return {}
223
+ raise e
224
+
225
+ def _sanitize_basename(self, basename: str) -> str:
226
+ return basename[:-1] if basename.endswith("/") else basename
227
+
56
228
  async def get_response(self, path: str, scope: Scope) -> Response:
57
229
  response = None
58
230
  try:
@@ -71,8 +243,14 @@ class Static(StaticFiles):
71
243
  "min_dist": self._app_config.min_dist,
72
244
  "n_neighbors": self._app_config.n_neighbors,
73
245
  "n_samples": self._app_config.n_samples,
74
- "basename": request.scope.get("root_path", ""),
246
+ "basename": self._sanitize_basename(request.scope.get("root_path", "")),
247
+ "platform_version": phoenix_version,
75
248
  "request": request,
249
+ "is_development": self._app_config.is_development,
250
+ "manifest": self._web_manifest,
251
+ "authentication_enabled": self._app_config.authentication_enabled,
252
+ "oauth2_idps": self._app_config.oauth2_idps,
253
+ "websockets_enabled": self._app_config.websockets_enabled,
76
254
  },
77
255
  )
78
256
  except Exception as e:
@@ -80,137 +258,708 @@ class Static(StaticFiles):
80
258
  return response
81
259
 
82
260
 
261
+ class RequestOriginHostnameValidator(BaseHTTPMiddleware):
262
+ def __init__(self, *args: Any, trusted_hostnames: list[str], **kwargs: Any) -> None:
263
+ super().__init__(*args, **kwargs)
264
+ self._trusted_hostnames = trusted_hostnames
265
+
266
+ async def dispatch(
267
+ self,
268
+ request: Request,
269
+ call_next: RequestResponseEndpoint,
270
+ ) -> Response:
271
+ headers = request.headers
272
+ for key in "origin", "referer":
273
+ if not (url := headers.get(key)):
274
+ continue
275
+ if urlparse(url).hostname not in self._trusted_hostnames:
276
+ return Response(f"untrusted {key}", status_code=HTTP_401_UNAUTHORIZED)
277
+ return await call_next(request)
278
+
279
+
83
280
  class HeadersMiddleware(BaseHTTPMiddleware):
84
281
  async def dispatch(
85
282
  self,
86
283
  request: Request,
87
284
  call_next: RequestResponseEndpoint,
88
285
  ) -> Response:
286
+ from phoenix.version import __version__ as phoenix_version
287
+
89
288
  response = await call_next(request)
90
289
  response.headers["x-colab-notebook-cache-control"] = "no-cache"
91
- response.headers["Cache-Control"] = "no-store"
290
+ response.headers[PHOENIX_SERVER_VERSION_HEADER] = phoenix_version
92
291
  return response
93
292
 
94
293
 
95
- class GraphQLWithContext(GraphQL): # type: ignore
294
+ def user_fastapi_middlewares() -> list[Middleware]:
295
+ paths = get_env_fastapi_middleware_paths()
296
+ middlewares = []
297
+ for file_path, object_name in paths:
298
+ middleware_class = import_object_from_file(file_path, object_name)
299
+ if not issubclass(middleware_class, BaseHTTPMiddleware):
300
+ raise TypeError(f"{middleware_class} is not a subclass of BaseHTTPMiddleware")
301
+ middlewares.append(Middleware(middleware_class))
302
+ return middlewares
303
+
304
+
305
+ def user_gql_extensions() -> list[Union[type[SchemaExtension], SchemaExtension]]:
306
+ paths = get_env_gql_extension_paths()
307
+ extensions = []
308
+ for file_path, object_name in paths:
309
+ extension_class = import_object_from_file(file_path, object_name)
310
+ if not issubclass(extension_class, SchemaExtension):
311
+ raise TypeError(f"{extension_class} is not a subclass of SchemaExtension")
312
+ extensions.append(extension_class)
313
+ return extensions
314
+
315
+
316
+ def user_grpc_interceptors() -> list[ServerInterceptor]:
317
+ paths = get_env_grpc_interceptor_paths()
318
+ interceptors = []
319
+ for file_path, object_name in paths:
320
+ interceptor_class = import_object_from_file(file_path, object_name)
321
+ if not issubclass(interceptor_class, ServerInterceptor):
322
+ raise TypeError(f"{interceptor_class} is not a subclass of ServerInterceptor")
323
+ interceptors.append(interceptor_class)
324
+ return interceptors
325
+
326
+
327
+ ProjectRowId: TypeAlias = int
328
+
329
+
330
+ @router.get("/arize_phoenix_version")
331
+ async def version() -> PlainTextResponse:
332
+ return PlainTextResponse(f"{phoenix_version}")
333
+
334
+
335
+ DB_MUTEX: Optional[asyncio.Lock] = None
336
+
337
+
338
+ def _db(
339
+ engine: AsyncEngine, bypass_lock: bool = False
340
+ ) -> Callable[[], AbstractAsyncContextManager[AsyncSession]]:
341
+ Session = async_sessionmaker(engine, expire_on_commit=False)
342
+
343
+ @contextlib.asynccontextmanager
344
+ async def factory() -> AsyncIterator[AsyncSession]:
345
+ async with contextlib.AsyncExitStack() as stack:
346
+ if not bypass_lock and DB_MUTEX:
347
+ await stack.enter_async_context(DB_MUTEX)
348
+ yield await stack.enter_async_context(Session.begin())
349
+
350
+ return factory
351
+
352
+
353
+ @dataclass(frozen=True)
354
+ class ScaffolderConfig:
355
+ db: DbSessionFactory
356
+ tracing_fixture_names: Iterable[str] = field(default_factory=list)
357
+ force_fixture_ingestion: bool = False
358
+ scaffold_datasets: bool = False
359
+ phoenix_url: str = f"http://{get_env_host()}:{get_env_port()}"
360
+
361
+
362
+ class Scaffolder(DaemonTask):
96
363
  def __init__(
97
364
  self,
98
- schema: BaseSchema,
99
- model: Model,
100
- export_path: Path,
101
- graphiql: bool = False,
102
- corpus: Optional[Model] = None,
103
- traces: Optional[Traces] = None,
365
+ config: ScaffolderConfig,
366
+ queue_span: Callable[[Span, ProjectName], Awaitable[None]],
367
+ queue_evaluation: Callable[[pb.Evaluation], Awaitable[None]],
104
368
  ) -> None:
105
- self.model = model
106
- self.corpus = corpus
107
- self.traces = traces
108
- self.export_path = export_path
109
- super().__init__(schema, graphiql=graphiql)
369
+ super().__init__()
370
+ self._db = config.db
371
+ self._queue_span = queue_span
372
+ self._queue_evaluation = queue_evaluation
373
+ self._tracing_fixtures = [
374
+ get_trace_fixture_by_name(name) for name in set(config.tracing_fixture_names)
375
+ ]
376
+ self._force_fixture_ingestion = config.force_fixture_ingestion
377
+ self._scaffold_datasets = config.scaffold_datasets
378
+ self._phoenix_url = config.phoenix_url
110
379
 
111
- async def get_context(
112
- self,
113
- request: Union[Request, WebSocket],
114
- response: Optional[Response] = None,
115
- ) -> Context:
380
+ async def __aenter__(self) -> None:
381
+ if not self._tracing_fixtures:
382
+ return
383
+ await self.start()
384
+
385
+ async def _run(self) -> None:
386
+ """
387
+ Main entry point for Scaffolder.
388
+ Determines whether to load fixtures and handles them.
389
+ """
390
+ if await self._should_load_fixtures():
391
+ logger.info("Loading trace fixtures...")
392
+ await self._handle_tracing_fixtures()
393
+ logger.info("Finished loading fixtures.")
394
+ else:
395
+ logger.info("DB is not new, avoid loading demo fixtures.")
396
+
397
+ async def _should_load_fixtures(self) -> bool:
398
+ if self._force_fixture_ingestion:
399
+ return True
400
+
401
+ async with self._db() as session:
402
+ created_at = await session.scalar(
403
+ select(models.Project.created_at).where(models.Project.name == "default")
404
+ )
405
+ if created_at is None:
406
+ return False
407
+
408
+ is_new_db = datetime.now(timezone.utc) - created_at < timedelta(
409
+ minutes=NEW_DB_AGE_THRESHOLD_MINUTES
410
+ )
411
+ return is_new_db
412
+
413
+ async def _handle_tracing_fixtures(self) -> None:
414
+ """
415
+ Main handler for processing trace fixtures. Process each fixture by
416
+ loading its trace dataframe, gettting and processings its
417
+ spans and evals, and queuing.
418
+ """
419
+ loop = asyncio.get_running_loop()
420
+ for fixture in self._tracing_fixtures:
421
+ try:
422
+ trace_ds = await loop.run_in_executor(None, load_example_traces, fixture.name)
423
+
424
+ fixture_spans, fixture_evals = await loop.run_in_executor(
425
+ None,
426
+ reset_fixture_span_ids_and_timestamps,
427
+ (
428
+ # Apply `encode` here because legacy jsonl files contains UUIDs as strings.
429
+ # `encode` removes the hyphens in the UUIDs.
430
+ decode_otlp_span(encode_span_to_otlp(span))
431
+ for span in trace_ds.to_spans()
432
+ ),
433
+ get_evals_from_fixture(fixture.name),
434
+ )
435
+
436
+ # Ingest dataset fixtures
437
+ if self._scaffold_datasets:
438
+ await self._handle_dataset_fixtures(fixture)
439
+
440
+ project_name = fixture.project_name or fixture.name
441
+ logger.info(f"Loading '{project_name}' fixtures...")
442
+ for span in fixture_spans:
443
+ await self._queue_span(span, project_name)
444
+ for evaluation in fixture_evals:
445
+ await self._queue_evaluation(evaluation)
446
+
447
+ except FileNotFoundError:
448
+ logger.warning(f"Fixture file not found for '{fixture.name}'")
449
+ except ValueError as e:
450
+ logger.error(f"Error processing fixture '{fixture.name}': {e}")
451
+ except Exception as e:
452
+ logger.error(f"Unexpected error processing fixture '{fixture.name}': {e}")
453
+
454
+ async def _handle_dataset_fixtures(self, fixture: TracesFixture) -> None:
455
+ loop = asyncio.get_running_loop()
456
+ try:
457
+ dataset_fixtures = await loop.run_in_executor(None, get_dataset_fixtures, fixture.name)
458
+ await loop.run_in_executor(
459
+ None,
460
+ send_dataset_fixtures,
461
+ self._phoenix_url,
462
+ dataset_fixtures,
463
+ )
464
+ except Exception as e:
465
+ logger.error(f"Error processing dataset fixture: {e}")
466
+
467
+
468
+ def _lifespan(
469
+ *,
470
+ db: DbSessionFactory,
471
+ bulk_inserter: BulkInserter,
472
+ dml_event_handler: DmlEventHandler,
473
+ token_store: Optional[TokenStore] = None,
474
+ tracer_provider: Optional["TracerProvider"] = None,
475
+ enable_prometheus: bool = False,
476
+ startup_callbacks: Iterable[_Callback] = (),
477
+ shutdown_callbacks: Iterable[_Callback] = (),
478
+ read_only: bool = False,
479
+ scaffolder_config: Optional[ScaffolderConfig] = None,
480
+ ) -> StatefulLifespan[FastAPI]:
481
+ @contextlib.asynccontextmanager
482
+ async def lifespan(_: FastAPI) -> AsyncIterator[dict[str, Any]]:
483
+ for callback in startup_callbacks:
484
+ if isinstance((res := callback()), Awaitable):
485
+ await res
486
+ global DB_MUTEX
487
+ DB_MUTEX = asyncio.Lock() if db.dialect is SupportedSQLDialect.SQLITE else None
488
+ async with AsyncExitStack() as stack:
489
+ (
490
+ enqueue,
491
+ queue_span,
492
+ queue_evaluation,
493
+ enqueue_operation,
494
+ ) = await stack.enter_async_context(bulk_inserter)
495
+ grpc_server = GrpcServer(
496
+ queue_span,
497
+ disabled=read_only,
498
+ tracer_provider=tracer_provider,
499
+ enable_prometheus=enable_prometheus,
500
+ token_store=token_store,
501
+ interceptors=user_grpc_interceptors(),
502
+ )
503
+ await stack.enter_async_context(grpc_server)
504
+ await stack.enter_async_context(dml_event_handler)
505
+ if scaffolder_config:
506
+ scaffolder = Scaffolder(
507
+ config=scaffolder_config,
508
+ queue_span=queue_span,
509
+ queue_evaluation=queue_evaluation,
510
+ )
511
+ await stack.enter_async_context(scaffolder)
512
+ if isinstance(token_store, AbstractAsyncContextManager):
513
+ await stack.enter_async_context(token_store)
514
+ yield {
515
+ "event_queue": dml_event_handler,
516
+ "enqueue": enqueue,
517
+ "queue_span_for_bulk_insert": queue_span,
518
+ "queue_evaluation_for_bulk_insert": queue_evaluation,
519
+ "enqueue_operation": enqueue_operation,
520
+ }
521
+ for callback in shutdown_callbacks:
522
+ if isinstance((res := callback()), Awaitable):
523
+ await res
524
+
525
+ return lifespan
526
+
527
+
528
+ @router.get("/healthz")
529
+ async def check_healthz(_: Request) -> PlainTextResponse:
530
+ return PlainTextResponse("OK")
531
+
532
+
533
+ def create_graphql_router(
534
+ *,
535
+ graphql_schema: strawberry.Schema,
536
+ db: DbSessionFactory,
537
+ model: Model,
538
+ export_path: Path,
539
+ last_updated_at: CanGetLastUpdatedAt,
540
+ authentication_enabled: bool,
541
+ corpus: Optional[Model] = None,
542
+ cache_for_dataloaders: Optional[CacheForDataLoaders] = None,
543
+ event_queue: CanPutItem[DmlEvent],
544
+ read_only: bool = False,
545
+ secret: Optional[str] = None,
546
+ token_store: Optional[TokenStore] = None,
547
+ ) -> GraphQLRouter: # type: ignore[type-arg]
548
+ """Creates the GraphQL router.
549
+
550
+ Args:
551
+ schema (BaseSchema): The GraphQL schema.
552
+ db (DbSessionFactory): The database session factory pointing to a SQL database.
553
+ model (Model): The Model representing inferences (legacy)
554
+ export_path (Path): the file path to export data to for download (legacy)
555
+ last_updated_at (CanGetLastUpdatedAt): How to get the last updated timestamp for updates.
556
+ authentication_enabled (bool): Whether authentication is enabled.
557
+ event_queue (CanPutItem[DmlEvent]): The event queue for DML events.
558
+ corpus (Optional[Model], optional): the corpus for UMAP projection. Defaults to None.
559
+ cache_for_dataloaders (Optional[CacheForDataLoaders], optional): GraphQL data loaders.
560
+ read_only (bool, optional): Marks the app as read-only. Defaults to False.
561
+ secret (Optional[str], optional): The application secret for auth. Defaults to None.
562
+
563
+ Returns:
564
+ GraphQLRouter: The router mounted at /graphql
565
+ """
566
+
567
+ def get_context() -> Context:
116
568
  return Context(
117
- request=request,
118
- response=response,
119
- model=self.model,
120
- corpus=self.corpus,
121
- traces=self.traces,
122
- export_path=self.export_path,
569
+ db=db,
570
+ model=model,
571
+ corpus=corpus,
572
+ export_path=export_path,
573
+ last_updated_at=last_updated_at,
574
+ event_queue=event_queue,
575
+ data_loaders=DataLoaders(
576
+ average_experiment_run_latency=AverageExperimentRunLatencyDataLoader(db),
577
+ dataset_example_revisions=DatasetExampleRevisionsDataLoader(db),
578
+ dataset_example_spans=DatasetExampleSpansDataLoader(db),
579
+ document_evaluation_summaries=DocumentEvaluationSummaryDataLoader(
580
+ db,
581
+ cache_map=(
582
+ cache_for_dataloaders.document_evaluation_summary
583
+ if cache_for_dataloaders
584
+ else None
585
+ ),
586
+ ),
587
+ document_evaluations=DocumentEvaluationsDataLoader(db),
588
+ document_retrieval_metrics=DocumentRetrievalMetricsDataLoader(db),
589
+ annotation_summaries=AnnotationSummaryDataLoader(
590
+ db,
591
+ cache_map=(
592
+ cache_for_dataloaders.annotation_summary if cache_for_dataloaders else None
593
+ ),
594
+ ),
595
+ experiment_annotation_summaries=ExperimentAnnotationSummaryDataLoader(db),
596
+ experiment_error_rates=ExperimentErrorRatesDataLoader(db),
597
+ experiment_run_annotations=ExperimentRunAnnotations(db),
598
+ experiment_run_counts=ExperimentRunCountsDataLoader(db),
599
+ experiment_sequence_number=ExperimentSequenceNumberDataLoader(db),
600
+ latency_ms_quantile=LatencyMsQuantileDataLoader(
601
+ db,
602
+ cache_map=(
603
+ cache_for_dataloaders.latency_ms_quantile if cache_for_dataloaders else None
604
+ ),
605
+ ),
606
+ min_start_or_max_end_times=MinStartOrMaxEndTimeDataLoader(
607
+ db,
608
+ cache_map=(
609
+ cache_for_dataloaders.min_start_or_max_end_time
610
+ if cache_for_dataloaders
611
+ else None
612
+ ),
613
+ ),
614
+ record_counts=RecordCountDataLoader(
615
+ db,
616
+ cache_map=cache_for_dataloaders.record_count if cache_for_dataloaders else None,
617
+ ),
618
+ session_first_inputs=SessionIODataLoader(db, "first_input"),
619
+ session_last_outputs=SessionIODataLoader(db, "last_output"),
620
+ session_num_traces=SessionNumTracesDataLoader(db),
621
+ session_num_traces_with_error=SessionNumTracesWithErrorDataLoader(db),
622
+ session_token_usages=SessionTokenUsagesDataLoader(db),
623
+ session_trace_latency_ms_quantile=SessionTraceLatencyMsQuantileDataLoader(db),
624
+ span_annotations=SpanAnnotationsDataLoader(db),
625
+ span_dataset_examples=SpanDatasetExamplesDataLoader(db),
626
+ span_descendants=SpanDescendantsDataLoader(db),
627
+ span_projects=SpanProjectsDataLoader(db),
628
+ token_counts=TokenCountDataLoader(
629
+ db,
630
+ cache_map=cache_for_dataloaders.token_count if cache_for_dataloaders else None,
631
+ ),
632
+ trace_by_trace_ids=TraceByTraceIdsDataLoader(db),
633
+ trace_root_spans=TraceRootSpansDataLoader(db),
634
+ project_by_name=ProjectByNameDataLoader(db),
635
+ users=UsersDataLoader(db),
636
+ user_roles=UserRolesDataLoader(db),
637
+ ),
638
+ cache_for_dataloaders=cache_for_dataloaders,
639
+ read_only=read_only,
640
+ auth_enabled=authentication_enabled,
641
+ secret=secret,
642
+ token_store=token_store,
643
+ )
644
+
645
+ return GraphQLRouter(
646
+ graphql_schema,
647
+ graphql_ide="graphiql",
648
+ context_getter=get_context,
649
+ include_in_schema=False,
650
+ prefix="/graphql",
651
+ dependencies=(Depends(is_authenticated),) if authentication_enabled else (),
652
+ subscription_protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL],
653
+ )
654
+
655
+
656
+ def create_engine_and_run_migrations(
657
+ database_url: str,
658
+ ) -> AsyncEngine:
659
+ try:
660
+ return create_engine(connection_str=database_url, migrate=True, log_to_stdout=False)
661
+ except PhoenixMigrationError as e:
662
+ msg = (
663
+ "\n\n⚠️⚠️ Phoenix failed to migrate the database to the latest version. ⚠️⚠️\n\n"
664
+ "The database may be in a dirty state. To resolve this, the Alembic CLI can be used\n"
665
+ "from the `src/phoenix/db` directory inside the Phoenix project root. From here,\n"
666
+ "revert any partial migrations and run `alembic stamp` to reset the migration state,\n"
667
+ "then try starting Phoenix again.\n\n"
668
+ "If issues persist, please reach out for support in the Arize community Slack:\n"
669
+ "https://arize-ai.slack.com\n\n"
670
+ "You can also refer to the Alembic documentation for more information:\n"
671
+ "https://alembic.sqlalchemy.org/en/latest/tutorial.html\n\n"
672
+ ""
123
673
  )
674
+ raise PhoenixMigrationError(msg) from e
124
675
 
125
676
 
126
- class Download(HTTPEndpoint):
127
- path: Path
677
+ def instrument_engine_if_enabled(engine: AsyncEngine) -> list[Callable[[], None]]:
678
+ instrumentation_cleanups = []
679
+ if server_instrumentation_is_enabled():
680
+ from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
128
681
 
129
- async def get(self, request: Request) -> FileResponse:
130
- params = QueryParams(request.query_params)
131
- file = self.path / (params.get("filename", "") + ".parquet")
132
- if not file.is_file():
133
- raise HTTPException(status_code=404)
134
- return FileResponse(
135
- path=file,
136
- filename=file.name,
137
- media_type="application/x-octet-stream",
682
+ tracer_provider = initialize_opentelemetry_tracer_provider()
683
+ SQLAlchemyInstrumentor().instrument(
684
+ engine=engine.sync_engine,
685
+ tracer_provider=tracer_provider,
138
686
  )
687
+ instrumentation_cleanups.append(SQLAlchemyInstrumentor().uninstrument)
688
+ return instrumentation_cleanups
689
+
139
690
 
691
+ async def plain_text_http_exception_handler(request: Request, exc: HTTPException) -> Response:
692
+ """
693
+ Overrides the default handler for HTTPExceptions to return a plain text
694
+ response instead of a JSON response. For the original source code, see
695
+ https://github.com/tiangolo/fastapi/blob/d3cdd3bbd14109f3b268df7ca496e24bb64593aa/fastapi/exception_handlers.py#L11
696
+ """
697
+ headers = getattr(exc, "headers", None)
698
+ if not is_body_allowed_for_status_code(exc.status_code):
699
+ return Response(status_code=exc.status_code, headers=headers)
700
+ return PlainTextResponse(str(exc.detail), status_code=exc.status_code, headers=headers)
140
701
 
141
- async def version(_: Request) -> PlainTextResponse:
142
- return PlainTextResponse(f"{phoenix.__version__}")
702
+
703
+ async def websocket_denial_response_handler(websocket: WebSocket, exc: WebSocketException) -> None:
704
+ """
705
+ Overrides the default exception handler for WebSocketException to ensure
706
+ that the HTTP response returned when a WebSocket connection is denied has
707
+ the same status code as the raised exception. This is in keeping with the
708
+ WebSocket Denial Response Extension of the ASGI specificiation described
709
+ below.
710
+
711
+ "Websocket connections start with the client sending a HTTP request
712
+ containing the appropriate upgrade headers. On receipt of this request a
713
+ server can choose to either upgrade the connection or respond with an HTTP
714
+ response (denying the upgrade). The core ASGI specification does not allow
715
+ for any control over the denial response, instead specifying that the HTTP
716
+ status code 403 should be returned, whereas this extension allows an ASGI
717
+ framework to control the denial response."
718
+
719
+ For details, see:
720
+ - https://asgi.readthedocs.io/en/latest/extensions.html#websocket-denial-response
721
+ """
722
+ assert isinstance(exc, WebSocketException)
723
+ await websocket.send_denial_response(JSONResponse(status_code=exc.code, content=exc.reason))
143
724
 
144
725
 
145
726
  def create_app(
727
+ db: DbSessionFactory,
146
728
  export_path: Path,
147
729
  model: Model,
730
+ authentication_enabled: bool,
148
731
  umap_params: UMAPParameters,
732
+ enable_websockets: bool,
149
733
  corpus: Optional[Model] = None,
150
- traces: Optional[Traces] = None,
151
- span_store: Optional[SpanStore] = None,
152
734
  debug: bool = False,
735
+ dev: bool = False,
153
736
  read_only: bool = False,
154
- ) -> Starlette:
155
- graphql = GraphQLWithContext(
156
- schema=schema,
737
+ enable_prometheus: bool = False,
738
+ initial_spans: Optional[Iterable[Union[Span, tuple[Span, str]]]] = None,
739
+ initial_evaluations: Optional[Iterable[pb.Evaluation]] = None,
740
+ serve_ui: bool = True,
741
+ startup_callbacks: Iterable[_Callback] = (),
742
+ shutdown_callbacks: Iterable[_Callback] = (),
743
+ secret: Optional[str] = None,
744
+ password_reset_token_expiry: Optional[timedelta] = None,
745
+ access_token_expiry: Optional[timedelta] = None,
746
+ refresh_token_expiry: Optional[timedelta] = None,
747
+ scaffolder_config: Optional[ScaffolderConfig] = None,
748
+ email_sender: Optional[EmailSender] = None,
749
+ oauth2_client_configs: Optional[list[OAuth2ClientConfig]] = None,
750
+ bulk_inserter_factory: Optional[Callable[..., BulkInserter]] = None,
751
+ ) -> FastAPI:
752
+ if model.embedding_dimensions:
753
+ try:
754
+ import fast_hdbscan # noqa: F401
755
+ import umap # noqa: F401
756
+ except ImportError as exc:
757
+ raise ImportError(
758
+ "To visualize embeddings, please install `umap-learn` and `fast-hdbscan` "
759
+ "via `pip install arize-phoenix[embeddings]`"
760
+ ) from exc
761
+ logger.info(f"Server umap params: {umap_params}")
762
+ bulk_inserter_factory = bulk_inserter_factory or BulkInserter
763
+ startup_callbacks_list: list[_Callback] = list(startup_callbacks)
764
+ shutdown_callbacks_list: list[_Callback] = list(shutdown_callbacks)
765
+ startup_callbacks_list.append(Facilitator(db=db))
766
+ initial_batch_of_spans: Iterable[tuple[Span, str]] = (
767
+ ()
768
+ if initial_spans is None
769
+ else (
770
+ ((item, DEFAULT_PROJECT_NAME) if isinstance(item, Span) else item)
771
+ for item in initial_spans
772
+ )
773
+ )
774
+ initial_batch_of_evaluations = () if initial_evaluations is None else initial_evaluations
775
+ cache_for_dataloaders = (
776
+ CacheForDataLoaders() if db.dialect is SupportedSQLDialect.SQLITE else None
777
+ )
778
+ last_updated_at = LastUpdatedAt()
779
+ middlewares: list[Middleware] = [Middleware(HeadersMiddleware)]
780
+ middlewares.extend(user_fastapi_middlewares())
781
+ if origins := get_env_csrf_trusted_origins():
782
+ trusted_hostnames = [h for o in origins if o and (h := urlparse(o).hostname)]
783
+ middlewares.append(
784
+ Middleware(
785
+ RequestOriginHostnameValidator,
786
+ trusted_hostnames=trusted_hostnames,
787
+ )
788
+ )
789
+ elif email_sender or oauth2_client_configs:
790
+ logger.warning(
791
+ "CSRF protection can be enabled by listing trusted origins via "
792
+ f"the `{ENV_PHOENIX_CSRF_TRUSTED_ORIGINS}` environment variable. "
793
+ "This is recommended when setting up OAuth2 clients or sending "
794
+ "password reset emails."
795
+ )
796
+ if authentication_enabled and secret:
797
+ token_store = JwtStore(db, secret)
798
+ middlewares.append(
799
+ Middleware(
800
+ AuthenticationMiddleware,
801
+ backend=BearerTokenAuthBackend(token_store),
802
+ )
803
+ )
804
+ else:
805
+ token_store = None
806
+ dml_event_handler = DmlEventHandler(
807
+ db=db,
808
+ cache_for_dataloaders=cache_for_dataloaders,
809
+ last_updated_at=last_updated_at,
810
+ )
811
+ bulk_inserter = bulk_inserter_factory(
812
+ db,
813
+ enable_prometheus=enable_prometheus,
814
+ event_queue=dml_event_handler,
815
+ initial_batch_of_spans=initial_batch_of_spans,
816
+ initial_batch_of_evaluations=initial_batch_of_evaluations,
817
+ )
818
+ tracer_provider = None
819
+ graphql_schema_extensions: list[Union[type[SchemaExtension], SchemaExtension]] = []
820
+ graphql_schema_extensions.extend(user_gql_extensions())
821
+
822
+ if server_instrumentation_is_enabled():
823
+ tracer_provider = initialize_opentelemetry_tracer_provider()
824
+ from opentelemetry.trace import TracerProvider
825
+ from strawberry.extensions.tracing import OpenTelemetryExtension
826
+
827
+ if TYPE_CHECKING:
828
+ # Type-check the class before monkey-patching its private attribute.
829
+ assert OpenTelemetryExtension._tracer
830
+
831
+ class _OpenTelemetryExtension(OpenTelemetryExtension):
832
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
833
+ super().__init__(*args, **kwargs)
834
+ # Monkey-patch its private tracer to eliminate usage of the global
835
+ # TracerProvider, which in a notebook setting could be the one
836
+ # used by OpenInference.
837
+ self._tracer = cast(TracerProvider, tracer_provider).get_tracer("strawberry")
838
+
839
+ graphql_schema_extensions.append(_OpenTelemetryExtension)
840
+
841
+ graphql_router = create_graphql_router(
842
+ db=db,
843
+ graphql_schema=build_graphql_schema(graphql_schema_extensions),
157
844
  model=model,
158
845
  corpus=corpus,
159
- traces=traces,
846
+ authentication_enabled=authentication_enabled,
160
847
  export_path=export_path,
161
- graphiql=True,
848
+ last_updated_at=last_updated_at,
849
+ event_queue=dml_event_handler,
850
+ cache_for_dataloaders=cache_for_dataloaders,
851
+ read_only=read_only,
852
+ secret=secret,
853
+ token_store=token_store,
162
854
  )
163
- return Starlette(
164
- middleware=[
165
- Middleware(HeadersMiddleware),
166
- ],
855
+ if enable_prometheus:
856
+ from phoenix.server.prometheus import PrometheusMiddleware
857
+
858
+ middlewares.append(Middleware(PrometheusMiddleware))
859
+ app = FastAPI(
860
+ title="Arize-Phoenix REST API",
861
+ version=REST_API_VERSION,
862
+ lifespan=_lifespan(
863
+ db=db,
864
+ read_only=read_only,
865
+ bulk_inserter=bulk_inserter,
866
+ dml_event_handler=dml_event_handler,
867
+ token_store=token_store,
868
+ tracer_provider=tracer_provider,
869
+ enable_prometheus=enable_prometheus,
870
+ shutdown_callbacks=shutdown_callbacks_list,
871
+ startup_callbacks=startup_callbacks_list,
872
+ scaffolder_config=scaffolder_config,
873
+ ),
874
+ middleware=middlewares,
875
+ exception_handlers={
876
+ HTTPException: plain_text_http_exception_handler,
877
+ WebSocketException: websocket_denial_response_handler, # type: ignore[dict-item]
878
+ },
167
879
  debug=debug,
168
- routes=(
169
- []
170
- if traces is None or read_only
171
- else [
172
- Route(
173
- "/v1/spans",
174
- type("SpanEndpoint", (SpanHandler,), {"traces": traces}),
175
- ),
176
- Route(
177
- "/v1/traces",
178
- type("TraceEndpoint", (TraceHandler,), {"traces": traces, "store": span_store}),
179
- ),
180
- Route(
181
- "/v1/evaluations",
182
- type("EvaluationEndpoint", (EvaluationHandler,), {"traces": traces}),
183
- ),
184
- ]
185
- )
186
- + [
187
- Route("/arize_phoenix_version", version),
188
- Route(
189
- "/exports",
190
- type(
191
- "DownloadExports",
192
- (Download,),
193
- {"path": export_path},
194
- ),
195
- ),
196
- Route(
197
- "/graphql",
198
- graphql,
199
- ),
200
- WebSocketRoute("/graphql", graphql),
201
- Mount(
202
- "/",
203
- app=Static(
204
- directory=SERVER_DIR / "static",
205
- app_config=AppConfig(
206
- has_inferences=model.is_empty is not True,
207
- has_corpus=corpus is not None,
208
- min_dist=umap_params.min_dist,
209
- n_neighbors=umap_params.n_neighbors,
210
- n_samples=umap_params.n_samples,
211
- ),
880
+ swagger_ui_parameters={
881
+ "defaultModelsExpandDepth": -1, # hides the schema section in the Swagger UI
882
+ },
883
+ )
884
+ app.include_router(create_v1_router(authentication_enabled))
885
+ app.include_router(create_embeddings_router(authentication_enabled))
886
+ app.include_router(router)
887
+ app.include_router(graphql_router)
888
+ if authentication_enabled:
889
+ app.include_router(auth_router)
890
+ app.include_router(oauth2_router)
891
+ app.add_middleware(GZipMiddleware)
892
+ web_manifest_path = SERVER_DIR / "static" / ".vite" / "manifest.json"
893
+ if serve_ui and web_manifest_path.is_file():
894
+ oauth2_idps = [
895
+ OAuth2Idp(name=config.idp_name, displayName=config.idp_display_name)
896
+ for config in oauth2_client_configs or []
897
+ ]
898
+ app.mount(
899
+ "/",
900
+ app=Static(
901
+ directory=SERVER_DIR / "static",
902
+ app_config=AppConfig(
903
+ has_inferences=model.is_empty is not True,
904
+ has_corpus=corpus is not None,
905
+ min_dist=umap_params.min_dist,
906
+ n_neighbors=umap_params.n_neighbors,
907
+ n_samples=umap_params.n_samples,
908
+ is_development=dev,
909
+ authentication_enabled=authentication_enabled,
910
+ web_manifest_path=web_manifest_path,
911
+ oauth2_idps=oauth2_idps,
912
+ websockets_enabled=enable_websockets,
212
913
  ),
213
- name="static",
214
914
  ),
215
- ],
216
- )
915
+ name="static",
916
+ )
917
+ app.state.read_only = read_only
918
+ app.state.export_path = export_path
919
+ app.state.password_reset_token_expiry = password_reset_token_expiry
920
+ app.state.access_token_expiry = access_token_expiry
921
+ app.state.refresh_token_expiry = refresh_token_expiry
922
+ app.state.oauth2_clients = OAuth2Clients.from_configs(oauth2_client_configs or [])
923
+ app.state.db = db
924
+ app.state.email_sender = email_sender
925
+ app = _add_get_secret_method(app=app, secret=secret)
926
+ app = _add_get_token_store_method(app=app, token_store=token_store)
927
+ if tracer_provider:
928
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
929
+
930
+ FastAPIInstrumentor().instrument(tracer_provider=tracer_provider)
931
+ FastAPIInstrumentor.instrument_app(app, tracer_provider=tracer_provider)
932
+ shutdown_callbacks_list.append(FastAPIInstrumentor().uninstrument)
933
+ return app
934
+
935
+
936
+ def _add_get_secret_method(*, app: FastAPI, secret: Optional[str]) -> FastAPI:
937
+ """
938
+ Dynamically adds a `get_secret` method to the app's `state`.
939
+ """
940
+ app.state._secret = secret
941
+
942
+ def get_secret(self: StarletteState) -> str:
943
+ if (secret := self._secret) is None:
944
+ raise ValueError("app secret is not set")
945
+ assert isinstance(secret, str)
946
+ return secret
947
+
948
+ app.state.get_secret = MethodType(get_secret, app.state)
949
+ return app
950
+
951
+
952
+ def _add_get_token_store_method(*, app: FastAPI, token_store: Optional[JwtStore]) -> FastAPI:
953
+ """
954
+ Dynamically adds a `get_token_store` method to the app's `state`.
955
+ """
956
+ app.state._token_store = token_store
957
+
958
+ def get_token_store(self: StarletteState) -> JwtStore:
959
+ if (token_store := self._token_store) is None:
960
+ raise ValueError("token store is not set on the app")
961
+ assert isinstance(token_store, JwtStore)
962
+ return token_store
963
+
964
+ app.state.get_token_store = MethodType(get_token_store, app.state)
965
+ return app