arize-phoenix 3.16.0__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.0.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 -247
  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 +13 -107
  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.0.dist-info/METADATA +0 -495
  293. arize_phoenix-3.16.0.dist-info/RECORD +0 -178
  294. phoenix/core/project.py +0 -617
  295. phoenix/core/traces.py +0 -100
  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.0.dist-info → arize_phoenix-7.7.0.dist-info}/licenses/IP_NOTICE +0 -0
  335. {arize_phoenix-3.16.0.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/db/engines.py ADDED
@@ -0,0 +1,208 @@
1
+ import asyncio
2
+ import json
3
+ from collections.abc import Callable
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from sqlite3 import Connection
7
+ from typing import Any
8
+
9
+ import aiosqlite
10
+ import numpy as np
11
+ import sqlalchemy
12
+ import sqlean
13
+ from sqlalchemy import URL, StaticPool, event, make_url
14
+ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
15
+ from typing_extensions import assert_never
16
+
17
+ from phoenix.config import LoggingMode, get_env_database_schema
18
+ from phoenix.db.helpers import SupportedSQLDialect
19
+ from phoenix.db.migrate import migrate_in_thread
20
+ from phoenix.db.models import init_models
21
+ from phoenix.settings import Settings
22
+
23
+ sqlean.extensions.enable("text", "stats")
24
+
25
+
26
+ def set_sqlite_pragma(connection: Connection, _: Any) -> None:
27
+ cursor = connection.cursor()
28
+ cursor.execute("PRAGMA foreign_keys = ON;")
29
+ cursor.execute("PRAGMA journal_mode = WAL;")
30
+ cursor.execute("PRAGMA synchronous = OFF;")
31
+ cursor.execute("PRAGMA cache_size = -32000;")
32
+ cursor.execute("PRAGMA busy_timeout = 10000;")
33
+ cursor.close()
34
+
35
+
36
+ def get_printable_db_url(connection_str: str) -> str:
37
+ return make_url(connection_str).render_as_string(hide_password=True)
38
+
39
+
40
+ def get_async_db_url(connection_str: str) -> URL:
41
+ """
42
+ Parses the database URL string and returns a URL object that is async
43
+ """
44
+ url = make_url(connection_str)
45
+ if not url.database:
46
+ raise ValueError("Failed to parse database from connection string")
47
+ backend = SupportedSQLDialect(url.get_backend_name())
48
+ if backend is SupportedSQLDialect.SQLITE:
49
+ return url.set(drivername="sqlite+aiosqlite")
50
+ elif backend is SupportedSQLDialect.POSTGRESQL:
51
+ url = url.set(drivername="postgresql+asyncpg")
52
+ # For some reason username and password cannot be parsed from the typical slot
53
+ # So we need to parse them out manually
54
+ if url.username and url.password:
55
+ url = url.set(
56
+ query={**url.query, "user": url.username, "password": url.password},
57
+ password=None,
58
+ username=None,
59
+ )
60
+ return url
61
+ else:
62
+ assert_never(backend)
63
+
64
+
65
+ def create_engine(
66
+ connection_str: str,
67
+ migrate: bool = True,
68
+ log_to_stdout: bool = False,
69
+ ) -> AsyncEngine:
70
+ """
71
+ Factory to create a SQLAlchemy engine from a URL string.
72
+ """
73
+ url = make_url(connection_str)
74
+ if not url.database:
75
+ raise ValueError("Failed to parse database from connection string")
76
+ backend = SupportedSQLDialect(url.get_backend_name())
77
+ url = get_async_db_url(url.render_as_string(hide_password=False))
78
+ # If Phoenix is run as an application, we want to pass log_migrations_to_stdout=False
79
+ # and let the configured sqlalchemy logger handle the migration logs
80
+ log_migrations_to_stdout = (
81
+ Settings.log_migrations and Settings.logging_mode != LoggingMode.STRUCTURED
82
+ )
83
+ if backend is SupportedSQLDialect.SQLITE:
84
+ return aio_sqlite_engine(
85
+ url=url,
86
+ migrate=migrate,
87
+ log_to_stdout=log_to_stdout,
88
+ log_migrations_to_stdout=log_migrations_to_stdout,
89
+ )
90
+ elif backend is SupportedSQLDialect.POSTGRESQL:
91
+ return aio_postgresql_engine(
92
+ url=url,
93
+ migrate=migrate,
94
+ log_to_stdout=log_to_stdout,
95
+ log_migrations_to_stdout=log_migrations_to_stdout,
96
+ )
97
+ else:
98
+ assert_never(backend)
99
+
100
+
101
+ def aio_sqlite_engine(
102
+ url: URL,
103
+ migrate: bool = True,
104
+ shared_cache: bool = True,
105
+ log_to_stdout: bool = False,
106
+ log_migrations_to_stdout: bool = True,
107
+ ) -> AsyncEngine:
108
+ database = url.database or ":memory:"
109
+ if database.startswith("file:"):
110
+ database = database[5:]
111
+ if database.startswith(":memory:") and shared_cache:
112
+ url = url.set(query={**url.query, "cache": "shared"}, database=":memory:")
113
+ database = url.render_as_string().partition("///")[-1]
114
+
115
+ def async_creator() -> aiosqlite.Connection:
116
+ conn = aiosqlite.Connection(
117
+ lambda: sqlean.connect(f"file:{database}", uri=True),
118
+ iter_chunk_size=64,
119
+ )
120
+ conn.daemon = True
121
+ return conn
122
+
123
+ engine = create_async_engine(
124
+ url=url,
125
+ echo=log_to_stdout,
126
+ json_serializer=_dumps,
127
+ async_creator=async_creator,
128
+ poolclass=StaticPool,
129
+ )
130
+ event.listen(engine.sync_engine, "connect", set_sqlite_pragma)
131
+ if not migrate:
132
+ return engine
133
+ if database.startswith(":memory:"):
134
+ try:
135
+ asyncio.get_running_loop()
136
+ except RuntimeError:
137
+ asyncio.run(init_models(engine))
138
+ else:
139
+ asyncio.create_task(init_models(engine))
140
+ else:
141
+ sync_engine = sqlalchemy.create_engine(
142
+ url=url.set(drivername="sqlite"),
143
+ echo=log_migrations_to_stdout,
144
+ json_serializer=_dumps,
145
+ creator=lambda: sqlean.connect(f"file:{database}", uri=True),
146
+ )
147
+ migrate_in_thread(sync_engine)
148
+ return engine
149
+
150
+
151
+ def set_postgresql_search_path(schema: str) -> Callable[[Connection, Any], None]:
152
+ def _(connection: Connection, _: Any) -> None:
153
+ cursor = connection.cursor()
154
+ cursor.execute(f"CREATE SCHEMA IF NOT EXISTS {schema};")
155
+ cursor.execute(f"SET search_path TO {schema};")
156
+
157
+ return _
158
+
159
+
160
+ def aio_postgresql_engine(
161
+ url: URL,
162
+ migrate: bool = True,
163
+ log_to_stdout: bool = False,
164
+ log_migrations_to_stdout: bool = True,
165
+ ) -> AsyncEngine:
166
+ url_query = dict(url.query)
167
+ sslmode = url_query.pop("sslmode", None) or url_query.pop("ssl", None)
168
+ engine = create_async_engine(
169
+ url=url.set(
170
+ # https://github.com/MagicStack/asyncpg/issues/737
171
+ query={**url_query, "ssl": sslmode} if sslmode else url_query,
172
+ ),
173
+ echo=log_to_stdout,
174
+ json_serializer=_dumps,
175
+ )
176
+ if not migrate:
177
+ return engine
178
+ sync_engine = sqlalchemy.create_engine(
179
+ url=url.set(
180
+ drivername="postgresql+psycopg",
181
+ query={**url_query, "sslmode": sslmode} if sslmode else url_query,
182
+ ),
183
+ echo=log_migrations_to_stdout,
184
+ json_serializer=_dumps,
185
+ )
186
+ if schema := get_env_database_schema():
187
+ event.listen(sync_engine, "connect", set_postgresql_search_path(schema))
188
+ migrate_in_thread(sync_engine)
189
+ return engine
190
+
191
+
192
+ def _dumps(obj: Any) -> str:
193
+ return json.dumps(obj, cls=_Encoder)
194
+
195
+
196
+ class _Encoder(json.JSONEncoder):
197
+ def default(self, obj: Any) -> Any:
198
+ if isinstance(obj, datetime):
199
+ return obj.isoformat()
200
+ elif isinstance(obj, Enum):
201
+ return obj.value
202
+ elif isinstance(obj, np.ndarray):
203
+ return list(obj)
204
+ elif isinstance(obj, np.integer):
205
+ return int(obj)
206
+ elif isinstance(obj, np.floating):
207
+ return float(obj)
208
+ return super().default(obj)
phoenix/db/enums.py ADDED
@@ -0,0 +1,20 @@
1
+ from collections.abc import Mapping
2
+ from enum import Enum
3
+
4
+ from sqlalchemy.orm import InstrumentedAttribute
5
+
6
+ from phoenix.db import models
7
+ from phoenix.db.models import AuthMethod
8
+
9
+ __all__ = ["AuthMethod", "UserRole", "COLUMN_ENUMS"]
10
+
11
+
12
+ class UserRole(Enum):
13
+ SYSTEM = "SYSTEM"
14
+ ADMIN = "ADMIN"
15
+ MEMBER = "MEMBER"
16
+
17
+
18
+ COLUMN_ENUMS: Mapping[InstrumentedAttribute[str], type[Enum]] = {
19
+ models.UserRole.name: UserRole,
20
+ }
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import secrets
5
+ from functools import partial
6
+
7
+ from sqlalchemy import (
8
+ distinct,
9
+ insert,
10
+ select,
11
+ )
12
+ from sqlalchemy.ext.asyncio import AsyncSession
13
+
14
+ from phoenix.auth import (
15
+ DEFAULT_ADMIN_EMAIL,
16
+ DEFAULT_ADMIN_USERNAME,
17
+ DEFAULT_SECRET_LENGTH,
18
+ DEFAULT_SYSTEM_EMAIL,
19
+ DEFAULT_SYSTEM_USERNAME,
20
+ compute_password_hash,
21
+ )
22
+ from phoenix.config import get_env_default_admin_initial_password
23
+ from phoenix.db import models
24
+ from phoenix.db.enums import COLUMN_ENUMS, UserRole
25
+ from phoenix.server.types import DbSessionFactory
26
+
27
+
28
+ class Facilitator:
29
+ """
30
+ Facilitates the creation of database records necessary for Phoenix to function. This includes
31
+ ensuring that all enum values are present in their respective tables, ensuring that all user
32
+ roles are present, and ensuring that the admin user has a password hash. These tasks will be
33
+ carried out as callbacks at the very beginning of Starlette's lifespan process.
34
+ """
35
+
36
+ def __init__(self, *, db: DbSessionFactory) -> None:
37
+ self._db = db
38
+
39
+ async def __call__(self) -> None:
40
+ async with self._db() as session:
41
+ for fn in (
42
+ _ensure_enums,
43
+ _ensure_user_roles,
44
+ ):
45
+ async with session.begin_nested():
46
+ await fn(session)
47
+
48
+
49
+ async def _ensure_enums(session: AsyncSession) -> None:
50
+ """
51
+ Ensure that all enum values are present in their respective tables. If any values are missing,
52
+ they will be added. If any values are present in the database but not in the enum, an error will
53
+ be raised. This function is idempotent: it will not add duplicate values to the database.
54
+ """
55
+ for column, enum in COLUMN_ENUMS.items():
56
+ table = column.class_
57
+ existing = set([_ async for _ in await session.stream_scalars(select(distinct(column)))])
58
+ expected = set(e.value for e in enum)
59
+ if unexpected := existing - expected:
60
+ raise ValueError(f"Unexpected values in {table.name}.{column.key}: {unexpected}")
61
+ if not (missing := expected - existing):
62
+ continue
63
+ await session.execute(insert(table), [{column.key: v} for v in missing])
64
+
65
+
66
+ async def _ensure_user_roles(session: AsyncSession) -> None:
67
+ """
68
+ Ensure that the system and admin roles are present in the database. If they are not, they will
69
+ be added. The system user will have the email "system@localhost" and the admin user will have
70
+ the email "admin@localhost".
71
+ """
72
+ role_ids = {
73
+ name: id_
74
+ async for name, id_ in await session.stream(
75
+ select(models.UserRole.name, models.UserRole.id)
76
+ )
77
+ }
78
+ existing_roles = [
79
+ name
80
+ async for name in await session.stream_scalars(
81
+ select(distinct(models.UserRole.name)).join_from(models.User, models.UserRole)
82
+ )
83
+ ]
84
+ if (system_role := UserRole.SYSTEM.value) not in existing_roles and (
85
+ system_role_id := role_ids.get(system_role)
86
+ ) is not None:
87
+ system_user = models.User(
88
+ user_role_id=system_role_id,
89
+ username=DEFAULT_SYSTEM_USERNAME,
90
+ email=DEFAULT_SYSTEM_EMAIL,
91
+ reset_password=False,
92
+ password_salt=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
93
+ password_hash=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
94
+ )
95
+ session.add(system_user)
96
+ if (admin_role := UserRole.ADMIN.value) not in existing_roles and (
97
+ admin_role_id := role_ids.get(admin_role)
98
+ ) is not None:
99
+ salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
100
+ password = get_env_default_admin_initial_password()
101
+ compute = partial(compute_password_hash, password=password, salt=salt)
102
+ loop = asyncio.get_running_loop()
103
+ hash_ = await loop.run_in_executor(None, compute)
104
+ admin_user = models.User(
105
+ user_role_id=admin_role_id,
106
+ username=DEFAULT_ADMIN_USERNAME,
107
+ email=DEFAULT_ADMIN_EMAIL,
108
+ password_salt=salt,
109
+ password_hash=hash_,
110
+ reset_password=True,
111
+ )
112
+ session.add(admin_user)
113
+ await session.flush()
phoenix/db/helpers.py ADDED
@@ -0,0 +1,159 @@
1
+ from collections.abc import Callable, Hashable, Iterable
2
+ from enum import Enum
3
+ from typing import Any, Optional, TypeVar
4
+
5
+ from openinference.semconv.trace import (
6
+ OpenInferenceSpanKindValues,
7
+ RerankerAttributes,
8
+ SpanAttributes,
9
+ )
10
+ from sqlalchemy import (
11
+ Integer,
12
+ Select,
13
+ SQLColumnExpression,
14
+ and_,
15
+ case,
16
+ distinct,
17
+ func,
18
+ select,
19
+ )
20
+ from typing_extensions import assert_never
21
+
22
+ from phoenix.db import models
23
+
24
+
25
+ class SupportedSQLDialect(Enum):
26
+ SQLITE = "sqlite"
27
+ POSTGRESQL = "postgresql"
28
+
29
+ @classmethod
30
+ def _missing_(cls, v: Any) -> "SupportedSQLDialect":
31
+ if isinstance(v, str) and v and v.isascii() and not v.islower():
32
+ return cls(v.lower())
33
+ raise ValueError(f"`{v}` is not a supported SQL backend/dialect.")
34
+
35
+
36
+ def num_docs_col(dialect: SupportedSQLDialect) -> SQLColumnExpression[Integer]:
37
+ if dialect is SupportedSQLDialect.POSTGRESQL:
38
+ array_length = func.jsonb_array_length
39
+ elif dialect is SupportedSQLDialect.SQLITE:
40
+ array_length = func.json_array_length
41
+ else:
42
+ assert_never(dialect)
43
+ retrieval_docs = models.Span.attributes[_RETRIEVAL_DOCUMENTS]
44
+ num_retrieval_docs = array_length(retrieval_docs)
45
+ reranker_docs = models.Span.attributes[_RERANKER_OUTPUT_DOCUMENTS]
46
+ num_reranker_docs = array_length(reranker_docs)
47
+ return case(
48
+ (
49
+ func.upper(models.Span.span_kind) == OpenInferenceSpanKindValues.RERANKER.value.upper(),
50
+ num_reranker_docs,
51
+ ),
52
+ else_=num_retrieval_docs,
53
+ ).label("num_docs")
54
+
55
+
56
+ _RETRIEVAL_DOCUMENTS = SpanAttributes.RETRIEVAL_DOCUMENTS.split(".")
57
+ _RERANKER_OUTPUT_DOCUMENTS = RerankerAttributes.RERANKER_OUTPUT_DOCUMENTS.split(".")
58
+
59
+
60
+ def get_eval_trace_ids_for_datasets(*dataset_ids: int) -> Select[tuple[Optional[str]]]:
61
+ return (
62
+ select(distinct(models.ExperimentRunAnnotation.trace_id))
63
+ .join(models.ExperimentRun)
64
+ .join_from(models.ExperimentRun, models.Experiment)
65
+ .where(models.Experiment.dataset_id.in_(set(dataset_ids)))
66
+ .where(models.ExperimentRunAnnotation.trace_id.isnot(None))
67
+ )
68
+
69
+
70
+ def get_project_names_for_datasets(*dataset_ids: int) -> Select[tuple[Optional[str]]]:
71
+ return (
72
+ select(distinct(models.Experiment.project_name))
73
+ .where(models.Experiment.dataset_id.in_(set(dataset_ids)))
74
+ .where(models.Experiment.project_name.isnot(None))
75
+ )
76
+
77
+
78
+ def get_eval_trace_ids_for_experiments(*experiment_ids: int) -> Select[tuple[Optional[str]]]:
79
+ return (
80
+ select(distinct(models.ExperimentRunAnnotation.trace_id))
81
+ .join(models.ExperimentRun)
82
+ .where(models.ExperimentRun.experiment_id.in_(set(experiment_ids)))
83
+ .where(models.ExperimentRunAnnotation.trace_id.isnot(None))
84
+ )
85
+
86
+
87
+ def get_project_names_for_experiments(*experiment_ids: int) -> Select[tuple[Optional[str]]]:
88
+ return (
89
+ select(distinct(models.Experiment.project_name))
90
+ .where(models.Experiment.id.in_(set(experiment_ids)))
91
+ .where(models.Experiment.project_name.isnot(None))
92
+ )
93
+
94
+
95
+ _AnyT = TypeVar("_AnyT")
96
+ _KeyT = TypeVar("_KeyT", bound=Hashable)
97
+
98
+
99
+ def dedup(
100
+ items: Iterable[_AnyT],
101
+ key: Callable[[_AnyT], _KeyT],
102
+ ) -> list[_AnyT]:
103
+ """
104
+ Discard subsequent duplicates after the first appearance in `items`.
105
+ """
106
+ ans = []
107
+ seen: set[_KeyT] = set()
108
+ for item in items:
109
+ if (k := key(item)) in seen:
110
+ continue
111
+ else:
112
+ ans.append(item)
113
+ seen.add(k)
114
+ return ans
115
+
116
+
117
+ def get_dataset_example_revisions(
118
+ dataset_version_id: int,
119
+ ) -> Select[tuple[models.DatasetExampleRevision]]:
120
+ version = (
121
+ select(
122
+ models.DatasetVersion.id,
123
+ models.DatasetVersion.dataset_id,
124
+ )
125
+ .filter_by(id=dataset_version_id)
126
+ .subquery()
127
+ )
128
+ table = models.DatasetExampleRevision
129
+ revision = (
130
+ select(
131
+ table.dataset_example_id,
132
+ func.max(table.dataset_version_id).label("dataset_version_id"),
133
+ )
134
+ .join_from(
135
+ table,
136
+ models.DatasetExample,
137
+ table.dataset_example_id == models.DatasetExample.id,
138
+ )
139
+ .join_from(
140
+ models.DatasetExample,
141
+ version,
142
+ models.DatasetExample.dataset_id == version.c.dataset_id,
143
+ )
144
+ .where(models.DatasetExample.dataset_id == version.c.dataset_id)
145
+ .where(table.dataset_version_id <= version.c.id)
146
+ .group_by(table.dataset_example_id)
147
+ .subquery()
148
+ )
149
+ return (
150
+ select(table)
151
+ .where(table.revision_kind != "DELETE")
152
+ .join(
153
+ revision,
154
+ onclause=and_(
155
+ revision.c.dataset_example_id == table.dataset_example_id,
156
+ revision.c.dataset_version_id == table.dataset_version_id,
157
+ ),
158
+ )
159
+ )
@@ -0,0 +1,2 @@
1
+ DEFAULT_RETRY_DELAY_SEC: float = 10
2
+ DEFAULT_RETRY_ALLOWANCE: int = 60