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
@@ -0,0 +1,11 @@
1
+ from .auth import router as auth_router
2
+ from .embeddings import create_embeddings_router
3
+ from .oauth2 import router as oauth2_router
4
+ from .v1 import create_v1_router
5
+
6
+ __all__ = [
7
+ "auth_router",
8
+ "create_embeddings_router",
9
+ "create_v1_router",
10
+ "oauth2_router",
11
+ ]
@@ -0,0 +1,284 @@
1
+ import asyncio
2
+ import secrets
3
+ from datetime import datetime, timedelta, timezone
4
+ from functools import partial
5
+ from pathlib import Path
6
+ from urllib.parse import urlencode, urlparse, urlunparse
7
+
8
+ from fastapi import APIRouter, Depends, HTTPException, Request, Response
9
+ from sqlalchemy import select
10
+ from sqlalchemy.orm import joinedload
11
+ from starlette.status import (
12
+ HTTP_204_NO_CONTENT,
13
+ HTTP_401_UNAUTHORIZED,
14
+ HTTP_404_NOT_FOUND,
15
+ HTTP_422_UNPROCESSABLE_ENTITY,
16
+ HTTP_503_SERVICE_UNAVAILABLE,
17
+ )
18
+
19
+ from phoenix.auth import (
20
+ DEFAULT_SECRET_LENGTH,
21
+ PHOENIX_ACCESS_TOKEN_COOKIE_NAME,
22
+ PHOENIX_REFRESH_TOKEN_COOKIE_NAME,
23
+ Token,
24
+ compute_password_hash,
25
+ delete_access_token_cookie,
26
+ delete_oauth2_nonce_cookie,
27
+ delete_oauth2_state_cookie,
28
+ delete_refresh_token_cookie,
29
+ is_valid_password,
30
+ set_access_token_cookie,
31
+ set_refresh_token_cookie,
32
+ validate_password_format,
33
+ )
34
+ from phoenix.config import get_base_url, get_env_disable_rate_limit, get_env_host_root_path
35
+ from phoenix.db import enums, models
36
+ from phoenix.server.bearer_auth import PhoenixUser, create_access_and_refresh_tokens
37
+ from phoenix.server.email.types import EmailSender
38
+ from phoenix.server.rate_limiters import ServerRateLimiter, fastapi_ip_rate_limiter
39
+ from phoenix.server.types import (
40
+ AccessTokenClaims,
41
+ PasswordResetTokenClaims,
42
+ PasswordResetTokenId,
43
+ RefreshTokenClaims,
44
+ TokenStore,
45
+ UserId,
46
+ )
47
+
48
+ rate_limiter = ServerRateLimiter(
49
+ per_second_rate_limit=0.2,
50
+ enforcement_window_seconds=60,
51
+ partition_seconds=60,
52
+ active_partitions=2,
53
+ )
54
+ login_rate_limiter = fastapi_ip_rate_limiter(
55
+ rate_limiter,
56
+ paths=[
57
+ "/auth/login",
58
+ "/auth/logout",
59
+ "/auth/refresh",
60
+ "/auth/password-reset-email",
61
+ "/auth/password-reset",
62
+ ],
63
+ )
64
+
65
+ auth_dependencies = [Depends(login_rate_limiter)] if not get_env_disable_rate_limit() else []
66
+ router = APIRouter(prefix="/auth", include_in_schema=False, dependencies=auth_dependencies)
67
+
68
+
69
+ @router.post("/login")
70
+ async def login(request: Request) -> Response:
71
+ assert isinstance(access_token_expiry := request.app.state.access_token_expiry, timedelta)
72
+ assert isinstance(refresh_token_expiry := request.app.state.refresh_token_expiry, timedelta)
73
+ token_store: TokenStore = request.app.state.get_token_store()
74
+ data = await request.json()
75
+ email = data.get("email")
76
+ password = data.get("password")
77
+
78
+ if not email or not password:
79
+ raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Email and password required")
80
+
81
+ async with request.app.state.db() as session:
82
+ user = await session.scalar(
83
+ select(models.User).filter_by(email=email).options(joinedload(models.User.role))
84
+ )
85
+ if (
86
+ user is None
87
+ or (password_hash := user.password_hash) is None
88
+ or (salt := user.password_salt) is None
89
+ ):
90
+ raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=LOGIN_FAILED_MESSAGE)
91
+
92
+ loop = asyncio.get_running_loop()
93
+ password_is_valid = partial(
94
+ is_valid_password, password=password, salt=salt, password_hash=password_hash
95
+ )
96
+ if not await loop.run_in_executor(None, password_is_valid):
97
+ raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=LOGIN_FAILED_MESSAGE)
98
+
99
+ access_token, refresh_token = await create_access_and_refresh_tokens(
100
+ token_store=token_store,
101
+ user=user,
102
+ access_token_expiry=access_token_expiry,
103
+ refresh_token_expiry=refresh_token_expiry,
104
+ )
105
+ response = Response(status_code=HTTP_204_NO_CONTENT)
106
+ response = set_access_token_cookie(
107
+ response=response, access_token=access_token, max_age=access_token_expiry
108
+ )
109
+ response = set_refresh_token_cookie(
110
+ response=response, refresh_token=refresh_token, max_age=refresh_token_expiry
111
+ )
112
+ return response
113
+
114
+
115
+ @router.post("/logout")
116
+ async def logout(
117
+ request: Request,
118
+ ) -> Response:
119
+ token_store: TokenStore = request.app.state.get_token_store()
120
+ user_id = None
121
+ if isinstance(user := request.user, PhoenixUser):
122
+ user_id = user.identity
123
+ elif (refresh_token := request.cookies.get(PHOENIX_REFRESH_TOKEN_COOKIE_NAME)) and (
124
+ isinstance(
125
+ refresh_token_claims := await token_store.read(Token(refresh_token)),
126
+ RefreshTokenClaims,
127
+ )
128
+ and isinstance(subject := refresh_token_claims.subject, UserId)
129
+ ):
130
+ user_id = subject
131
+ if user_id:
132
+ await token_store.log_out(user_id)
133
+ response = Response(status_code=HTTP_204_NO_CONTENT)
134
+ response = delete_access_token_cookie(response)
135
+ response = delete_refresh_token_cookie(response)
136
+ response = delete_oauth2_state_cookie(response)
137
+ response = delete_oauth2_nonce_cookie(response)
138
+ return response
139
+
140
+
141
+ @router.post("/refresh")
142
+ async def refresh_tokens(request: Request) -> Response:
143
+ assert isinstance(access_token_expiry := request.app.state.access_token_expiry, timedelta)
144
+ assert isinstance(refresh_token_expiry := request.app.state.refresh_token_expiry, timedelta)
145
+ if (refresh_token := request.cookies.get(PHOENIX_REFRESH_TOKEN_COOKIE_NAME)) is None:
146
+ raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Missing refresh token")
147
+ token_store: TokenStore = request.app.state.get_token_store()
148
+ refresh_token_claims = await token_store.read(Token(refresh_token))
149
+ if (
150
+ not isinstance(refresh_token_claims, RefreshTokenClaims)
151
+ or (refresh_token_id := refresh_token_claims.token_id) is None
152
+ or refresh_token_claims.subject is None
153
+ or (user_id := int(refresh_token_claims.subject)) is None
154
+ or (expiration_time := refresh_token_claims.expiration_time) is None
155
+ ):
156
+ raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Invalid refresh token")
157
+ if expiration_time.timestamp() < datetime.now().timestamp():
158
+ raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Expired refresh token")
159
+ await token_store.revoke(refresh_token_id)
160
+
161
+ if (
162
+ (access_token := request.cookies.get(PHOENIX_ACCESS_TOKEN_COOKIE_NAME)) is not None
163
+ and isinstance(
164
+ access_token_claims := await token_store.read(Token(access_token)), AccessTokenClaims
165
+ )
166
+ and (access_token_id := access_token_claims.token_id)
167
+ ):
168
+ await token_store.revoke(access_token_id)
169
+
170
+ async with request.app.state.db() as session:
171
+ if (
172
+ user := await session.scalar(
173
+ select(models.User).filter_by(id=user_id).options(joinedload(models.User.role))
174
+ )
175
+ ) is None:
176
+ raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="User not found")
177
+ access_token, refresh_token = await create_access_and_refresh_tokens(
178
+ token_store=token_store,
179
+ user=user,
180
+ access_token_expiry=access_token_expiry,
181
+ refresh_token_expiry=refresh_token_expiry,
182
+ )
183
+ response = Response(status_code=HTTP_204_NO_CONTENT)
184
+ response = set_access_token_cookie(
185
+ response=response, access_token=access_token, max_age=access_token_expiry
186
+ )
187
+ response = set_refresh_token_cookie(
188
+ response=response, refresh_token=refresh_token, max_age=refresh_token_expiry
189
+ )
190
+ return response
191
+
192
+
193
+ @router.post("/password-reset-email")
194
+ async def initiate_password_reset(request: Request) -> Response:
195
+ data = await request.json()
196
+ if not (email := data.get("email")):
197
+ raise MISSING_EMAIL
198
+ sender: EmailSender = request.app.state.email_sender
199
+ if sender is None:
200
+ raise SMTP_UNAVAILABLE
201
+ assert isinstance(token_expiry := request.app.state.password_reset_token_expiry, timedelta)
202
+ async with request.app.state.db() as session:
203
+ user = await session.scalar(
204
+ select(models.User)
205
+ .filter_by(email=email)
206
+ .options(
207
+ joinedload(models.User.password_reset_token).load_only(models.PasswordResetToken.id)
208
+ )
209
+ )
210
+ if user is None or user.auth_method != enums.AuthMethod.LOCAL.value:
211
+ # Withold privileged information
212
+ return Response(status_code=HTTP_204_NO_CONTENT)
213
+ token_store: TokenStore = request.app.state.get_token_store()
214
+ if user.password_reset_token:
215
+ await token_store.revoke(PasswordResetTokenId(user.password_reset_token.id))
216
+ password_reset_token_claims = PasswordResetTokenClaims(
217
+ subject=UserId(user.id),
218
+ issued_at=datetime.now(timezone.utc),
219
+ expiration_time=datetime.now(timezone.utc) + token_expiry,
220
+ )
221
+ token, _ = await token_store.create_password_reset_token(password_reset_token_claims)
222
+ url = urlparse(request.headers.get("referer") or get_base_url())
223
+ path = Path(get_env_host_root_path()) / "reset-password-with-token"
224
+ query_string = urlencode(dict(token=token))
225
+ components = (url.scheme, url.netloc, path.as_posix(), "", query_string, "")
226
+ reset_url = urlunparse(components)
227
+ await sender.send_password_reset_email(email, reset_url)
228
+ return Response(status_code=HTTP_204_NO_CONTENT)
229
+
230
+
231
+ @router.post("/password-reset")
232
+ async def reset_password(request: Request) -> Response:
233
+ data = await request.json()
234
+ if not (password := data.get("password")):
235
+ raise MISSING_PASSWORD
236
+ token_store: TokenStore = request.app.state.get_token_store()
237
+ if (
238
+ not (token := data.get("token"))
239
+ or not isinstance((claims := await token_store.read(token)), PasswordResetTokenClaims)
240
+ or not claims.expiration_time
241
+ or claims.expiration_time < datetime.now(timezone.utc)
242
+ ):
243
+ raise INVALID_TOKEN
244
+ assert (user_id := claims.subject)
245
+ async with request.app.state.db() as session:
246
+ user = await session.scalar(select(models.User).filter_by(id=int(user_id)))
247
+ if user is None or user.auth_method != enums.AuthMethod.LOCAL.value:
248
+ # Withold privileged information
249
+ return Response(status_code=HTTP_204_NO_CONTENT)
250
+ validate_password_format(password)
251
+ user.password_salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
252
+ loop = asyncio.get_running_loop()
253
+ user.password_hash = await loop.run_in_executor(
254
+ None, partial(compute_password_hash, password=password, salt=user.password_salt)
255
+ )
256
+ user.reset_password = False
257
+ async with request.app.state.db() as session:
258
+ session.add(user)
259
+ await session.flush()
260
+ response = Response(status_code=HTTP_204_NO_CONTENT)
261
+ assert (token_id := claims.token_id)
262
+ await token_store.revoke(token_id)
263
+ await token_store.log_out(UserId(user.id))
264
+ return response
265
+
266
+
267
+ LOGIN_FAILED_MESSAGE = "Invalid email and/or password"
268
+
269
+ MISSING_EMAIL = HTTPException(
270
+ status_code=HTTP_422_UNPROCESSABLE_ENTITY,
271
+ detail="Email required",
272
+ )
273
+ MISSING_PASSWORD = HTTPException(
274
+ status_code=HTTP_422_UNPROCESSABLE_ENTITY,
275
+ detail="Password required",
276
+ )
277
+ SMTP_UNAVAILABLE = HTTPException(
278
+ status_code=HTTP_503_SERVICE_UNAVAILABLE,
279
+ detail="SMTP server not configured",
280
+ )
281
+ INVALID_TOKEN = HTTPException(
282
+ status_code=HTTP_401_UNAUTHORIZED,
283
+ detail="Invalid token",
284
+ )
@@ -0,0 +1,26 @@
1
+ from fastapi import APIRouter, Depends
2
+ from fastapi.responses import FileResponse
3
+ from starlette.exceptions import HTTPException
4
+ from starlette.requests import Request
5
+
6
+ from phoenix.server.bearer_auth import is_authenticated
7
+
8
+
9
+ def create_embeddings_router(authentication_enabled: bool) -> APIRouter:
10
+ """
11
+ Instantiates a router for the embeddings API.
12
+ """
13
+ router = APIRouter(dependencies=[Depends(is_authenticated)] if authentication_enabled else [])
14
+
15
+ @router.get("/exports")
16
+ async def download_exported_file(request: Request, filename: str) -> FileResponse:
17
+ file = request.app.state.export_path / (filename + ".parquet")
18
+ if not file.is_file():
19
+ raise HTTPException(status_code=404)
20
+ return FileResponse(
21
+ path=file,
22
+ filename=file.name,
23
+ media_type="application/x-octet-stream",
24
+ )
25
+
26
+ return router