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.
- arize_phoenix-7.7.0.dist-info/METADATA +261 -0
- arize_phoenix-7.7.0.dist-info/RECORD +345 -0
- {arize_phoenix-3.16.1.dist-info → arize_phoenix-7.7.0.dist-info}/WHEEL +1 -1
- arize_phoenix-7.7.0.dist-info/entry_points.txt +3 -0
- phoenix/__init__.py +86 -14
- phoenix/auth.py +309 -0
- phoenix/config.py +675 -45
- phoenix/core/model.py +32 -30
- phoenix/core/model_schema.py +102 -109
- phoenix/core/model_schema_adapter.py +48 -45
- phoenix/datetime_utils.py +24 -3
- phoenix/db/README.md +54 -0
- phoenix/db/__init__.py +4 -0
- phoenix/db/alembic.ini +85 -0
- phoenix/db/bulk_inserter.py +294 -0
- phoenix/db/engines.py +208 -0
- phoenix/db/enums.py +20 -0
- phoenix/db/facilitator.py +113 -0
- phoenix/db/helpers.py +159 -0
- phoenix/db/insertion/constants.py +2 -0
- phoenix/db/insertion/dataset.py +227 -0
- phoenix/db/insertion/document_annotation.py +171 -0
- phoenix/db/insertion/evaluation.py +191 -0
- phoenix/db/insertion/helpers.py +98 -0
- phoenix/db/insertion/span.py +193 -0
- phoenix/db/insertion/span_annotation.py +158 -0
- phoenix/db/insertion/trace_annotation.py +158 -0
- phoenix/db/insertion/types.py +256 -0
- phoenix/db/migrate.py +86 -0
- phoenix/db/migrations/data_migration_scripts/populate_project_sessions.py +199 -0
- phoenix/db/migrations/env.py +114 -0
- phoenix/db/migrations/script.py.mako +26 -0
- phoenix/db/migrations/versions/10460e46d750_datasets.py +317 -0
- phoenix/db/migrations/versions/3be8647b87d8_add_token_columns_to_spans_table.py +126 -0
- phoenix/db/migrations/versions/4ded9e43755f_create_project_sessions_table.py +66 -0
- phoenix/db/migrations/versions/cd164e83824f_users_and_tokens.py +157 -0
- phoenix/db/migrations/versions/cf03bd6bae1d_init.py +280 -0
- phoenix/db/models.py +807 -0
- phoenix/exceptions.py +5 -1
- phoenix/experiments/__init__.py +6 -0
- phoenix/experiments/evaluators/__init__.py +29 -0
- phoenix/experiments/evaluators/base.py +158 -0
- phoenix/experiments/evaluators/code_evaluators.py +184 -0
- phoenix/experiments/evaluators/llm_evaluators.py +473 -0
- phoenix/experiments/evaluators/utils.py +236 -0
- phoenix/experiments/functions.py +772 -0
- phoenix/experiments/tracing.py +86 -0
- phoenix/experiments/types.py +726 -0
- phoenix/experiments/utils.py +25 -0
- phoenix/inferences/__init__.py +0 -0
- phoenix/{datasets → inferences}/errors.py +6 -5
- phoenix/{datasets → inferences}/fixtures.py +49 -42
- phoenix/{datasets/dataset.py → inferences/inferences.py} +121 -105
- phoenix/{datasets → inferences}/schema.py +11 -11
- phoenix/{datasets → inferences}/validation.py +13 -14
- phoenix/logging/__init__.py +3 -0
- phoenix/logging/_config.py +90 -0
- phoenix/logging/_filter.py +6 -0
- phoenix/logging/_formatter.py +69 -0
- phoenix/metrics/__init__.py +5 -4
- phoenix/metrics/binning.py +4 -3
- phoenix/metrics/metrics.py +2 -1
- phoenix/metrics/mixins.py +7 -6
- phoenix/metrics/retrieval_metrics.py +2 -1
- phoenix/metrics/timeseries.py +5 -4
- phoenix/metrics/wrappers.py +9 -3
- phoenix/pointcloud/clustering.py +5 -5
- phoenix/pointcloud/pointcloud.py +7 -5
- phoenix/pointcloud/projectors.py +5 -6
- phoenix/pointcloud/umap_parameters.py +53 -52
- phoenix/server/api/README.md +28 -0
- phoenix/server/api/auth.py +44 -0
- phoenix/server/api/context.py +152 -9
- phoenix/server/api/dataloaders/__init__.py +91 -0
- phoenix/server/api/dataloaders/annotation_summaries.py +139 -0
- phoenix/server/api/dataloaders/average_experiment_run_latency.py +54 -0
- phoenix/server/api/dataloaders/cache/__init__.py +3 -0
- phoenix/server/api/dataloaders/cache/two_tier_cache.py +68 -0
- phoenix/server/api/dataloaders/dataset_example_revisions.py +131 -0
- phoenix/server/api/dataloaders/dataset_example_spans.py +38 -0
- phoenix/server/api/dataloaders/document_evaluation_summaries.py +144 -0
- phoenix/server/api/dataloaders/document_evaluations.py +31 -0
- phoenix/server/api/dataloaders/document_retrieval_metrics.py +89 -0
- phoenix/server/api/dataloaders/experiment_annotation_summaries.py +79 -0
- phoenix/server/api/dataloaders/experiment_error_rates.py +58 -0
- phoenix/server/api/dataloaders/experiment_run_annotations.py +36 -0
- phoenix/server/api/dataloaders/experiment_run_counts.py +49 -0
- phoenix/server/api/dataloaders/experiment_sequence_number.py +44 -0
- phoenix/server/api/dataloaders/latency_ms_quantile.py +188 -0
- phoenix/server/api/dataloaders/min_start_or_max_end_times.py +85 -0
- phoenix/server/api/dataloaders/project_by_name.py +31 -0
- phoenix/server/api/dataloaders/record_counts.py +116 -0
- phoenix/server/api/dataloaders/session_io.py +79 -0
- phoenix/server/api/dataloaders/session_num_traces.py +30 -0
- phoenix/server/api/dataloaders/session_num_traces_with_error.py +32 -0
- phoenix/server/api/dataloaders/session_token_usages.py +41 -0
- phoenix/server/api/dataloaders/session_trace_latency_ms_quantile.py +55 -0
- phoenix/server/api/dataloaders/span_annotations.py +26 -0
- phoenix/server/api/dataloaders/span_dataset_examples.py +31 -0
- phoenix/server/api/dataloaders/span_descendants.py +57 -0
- phoenix/server/api/dataloaders/span_projects.py +33 -0
- phoenix/server/api/dataloaders/token_counts.py +124 -0
- phoenix/server/api/dataloaders/trace_by_trace_ids.py +25 -0
- phoenix/server/api/dataloaders/trace_root_spans.py +32 -0
- phoenix/server/api/dataloaders/user_roles.py +30 -0
- phoenix/server/api/dataloaders/users.py +33 -0
- phoenix/server/api/exceptions.py +48 -0
- phoenix/server/api/helpers/__init__.py +12 -0
- phoenix/server/api/helpers/dataset_helpers.py +217 -0
- phoenix/server/api/helpers/experiment_run_filters.py +763 -0
- phoenix/server/api/helpers/playground_clients.py +948 -0
- phoenix/server/api/helpers/playground_registry.py +70 -0
- phoenix/server/api/helpers/playground_spans.py +455 -0
- phoenix/server/api/input_types/AddExamplesToDatasetInput.py +16 -0
- phoenix/server/api/input_types/AddSpansToDatasetInput.py +14 -0
- phoenix/server/api/input_types/ChatCompletionInput.py +38 -0
- phoenix/server/api/input_types/ChatCompletionMessageInput.py +24 -0
- phoenix/server/api/input_types/ClearProjectInput.py +15 -0
- phoenix/server/api/input_types/ClusterInput.py +2 -2
- phoenix/server/api/input_types/CreateDatasetInput.py +12 -0
- phoenix/server/api/input_types/CreateSpanAnnotationInput.py +18 -0
- phoenix/server/api/input_types/CreateTraceAnnotationInput.py +18 -0
- phoenix/server/api/input_types/DataQualityMetricInput.py +5 -2
- phoenix/server/api/input_types/DatasetExampleInput.py +14 -0
- phoenix/server/api/input_types/DatasetSort.py +17 -0
- phoenix/server/api/input_types/DatasetVersionSort.py +16 -0
- phoenix/server/api/input_types/DeleteAnnotationsInput.py +7 -0
- phoenix/server/api/input_types/DeleteDatasetExamplesInput.py +13 -0
- phoenix/server/api/input_types/DeleteDatasetInput.py +7 -0
- phoenix/server/api/input_types/DeleteExperimentsInput.py +7 -0
- phoenix/server/api/input_types/DimensionFilter.py +4 -4
- phoenix/server/api/input_types/GenerativeModelInput.py +17 -0
- phoenix/server/api/input_types/Granularity.py +1 -1
- phoenix/server/api/input_types/InvocationParameters.py +162 -0
- phoenix/server/api/input_types/PatchAnnotationInput.py +19 -0
- phoenix/server/api/input_types/PatchDatasetExamplesInput.py +35 -0
- phoenix/server/api/input_types/PatchDatasetInput.py +14 -0
- phoenix/server/api/input_types/PerformanceMetricInput.py +5 -2
- phoenix/server/api/input_types/ProjectSessionSort.py +29 -0
- phoenix/server/api/input_types/SpanAnnotationSort.py +17 -0
- phoenix/server/api/input_types/SpanSort.py +134 -69
- phoenix/server/api/input_types/TemplateOptions.py +10 -0
- phoenix/server/api/input_types/TraceAnnotationSort.py +17 -0
- phoenix/server/api/input_types/UserRoleInput.py +9 -0
- phoenix/server/api/mutations/__init__.py +28 -0
- phoenix/server/api/mutations/api_key_mutations.py +167 -0
- phoenix/server/api/mutations/chat_mutations.py +593 -0
- phoenix/server/api/mutations/dataset_mutations.py +591 -0
- phoenix/server/api/mutations/experiment_mutations.py +75 -0
- phoenix/server/api/{types/ExportEventsMutation.py → mutations/export_events_mutations.py} +21 -18
- phoenix/server/api/mutations/project_mutations.py +57 -0
- phoenix/server/api/mutations/span_annotations_mutations.py +128 -0
- phoenix/server/api/mutations/trace_annotations_mutations.py +127 -0
- phoenix/server/api/mutations/user_mutations.py +329 -0
- phoenix/server/api/openapi/__init__.py +0 -0
- phoenix/server/api/openapi/main.py +17 -0
- phoenix/server/api/openapi/schema.py +16 -0
- phoenix/server/api/queries.py +738 -0
- phoenix/server/api/routers/__init__.py +11 -0
- phoenix/server/api/routers/auth.py +284 -0
- phoenix/server/api/routers/embeddings.py +26 -0
- phoenix/server/api/routers/oauth2.py +488 -0
- phoenix/server/api/routers/v1/__init__.py +64 -0
- phoenix/server/api/routers/v1/datasets.py +1017 -0
- phoenix/server/api/routers/v1/evaluations.py +362 -0
- phoenix/server/api/routers/v1/experiment_evaluations.py +115 -0
- phoenix/server/api/routers/v1/experiment_runs.py +167 -0
- phoenix/server/api/routers/v1/experiments.py +308 -0
- phoenix/server/api/routers/v1/pydantic_compat.py +78 -0
- phoenix/server/api/routers/v1/spans.py +267 -0
- phoenix/server/api/routers/v1/traces.py +208 -0
- phoenix/server/api/routers/v1/utils.py +95 -0
- phoenix/server/api/schema.py +44 -241
- phoenix/server/api/subscriptions.py +597 -0
- phoenix/server/api/types/Annotation.py +21 -0
- phoenix/server/api/types/AnnotationSummary.py +55 -0
- phoenix/server/api/types/AnnotatorKind.py +16 -0
- phoenix/server/api/types/ApiKey.py +27 -0
- phoenix/server/api/types/AuthMethod.py +9 -0
- phoenix/server/api/types/ChatCompletionMessageRole.py +11 -0
- phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +46 -0
- phoenix/server/api/types/Cluster.py +25 -24
- phoenix/server/api/types/CreateDatasetPayload.py +8 -0
- phoenix/server/api/types/DataQualityMetric.py +31 -13
- phoenix/server/api/types/Dataset.py +288 -63
- phoenix/server/api/types/DatasetExample.py +85 -0
- phoenix/server/api/types/DatasetExampleRevision.py +34 -0
- phoenix/server/api/types/DatasetVersion.py +14 -0
- phoenix/server/api/types/Dimension.py +32 -31
- phoenix/server/api/types/DocumentEvaluationSummary.py +9 -8
- phoenix/server/api/types/EmbeddingDimension.py +56 -49
- phoenix/server/api/types/Evaluation.py +25 -31
- phoenix/server/api/types/EvaluationSummary.py +30 -50
- phoenix/server/api/types/Event.py +20 -20
- phoenix/server/api/types/ExampleRevisionInterface.py +14 -0
- phoenix/server/api/types/Experiment.py +152 -0
- phoenix/server/api/types/ExperimentAnnotationSummary.py +13 -0
- phoenix/server/api/types/ExperimentComparison.py +17 -0
- phoenix/server/api/types/ExperimentRun.py +119 -0
- phoenix/server/api/types/ExperimentRunAnnotation.py +56 -0
- phoenix/server/api/types/GenerativeModel.py +9 -0
- phoenix/server/api/types/GenerativeProvider.py +85 -0
- phoenix/server/api/types/Inferences.py +80 -0
- phoenix/server/api/types/InferencesRole.py +23 -0
- phoenix/server/api/types/LabelFraction.py +7 -0
- phoenix/server/api/types/MimeType.py +2 -2
- phoenix/server/api/types/Model.py +54 -54
- phoenix/server/api/types/PerformanceMetric.py +8 -5
- phoenix/server/api/types/Project.py +407 -142
- phoenix/server/api/types/ProjectSession.py +139 -0
- phoenix/server/api/types/Segments.py +4 -4
- phoenix/server/api/types/Span.py +221 -176
- phoenix/server/api/types/SpanAnnotation.py +43 -0
- phoenix/server/api/types/SpanIOValue.py +15 -0
- phoenix/server/api/types/SystemApiKey.py +9 -0
- phoenix/server/api/types/TemplateLanguage.py +10 -0
- phoenix/server/api/types/TimeSeries.py +19 -15
- phoenix/server/api/types/TokenUsage.py +11 -0
- phoenix/server/api/types/Trace.py +154 -0
- phoenix/server/api/types/TraceAnnotation.py +45 -0
- phoenix/server/api/types/UMAPPoints.py +7 -7
- phoenix/server/api/types/User.py +60 -0
- phoenix/server/api/types/UserApiKey.py +45 -0
- phoenix/server/api/types/UserRole.py +15 -0
- phoenix/server/api/types/node.py +4 -112
- phoenix/server/api/types/pagination.py +156 -57
- phoenix/server/api/utils.py +34 -0
- phoenix/server/app.py +864 -115
- phoenix/server/bearer_auth.py +163 -0
- phoenix/server/dml_event.py +136 -0
- phoenix/server/dml_event_handler.py +256 -0
- phoenix/server/email/__init__.py +0 -0
- phoenix/server/email/sender.py +97 -0
- phoenix/server/email/templates/__init__.py +0 -0
- phoenix/server/email/templates/password_reset.html +19 -0
- phoenix/server/email/types.py +11 -0
- phoenix/server/grpc_server.py +102 -0
- phoenix/server/jwt_store.py +505 -0
- phoenix/server/main.py +305 -116
- phoenix/server/oauth2.py +52 -0
- phoenix/server/openapi/__init__.py +0 -0
- phoenix/server/prometheus.py +111 -0
- phoenix/server/rate_limiters.py +188 -0
- phoenix/server/static/.vite/manifest.json +87 -0
- phoenix/server/static/assets/components-Cy9nwIvF.js +2125 -0
- phoenix/server/static/assets/index-BKvHIxkk.js +113 -0
- phoenix/server/static/assets/pages-CUi2xCVQ.js +4449 -0
- phoenix/server/static/assets/vendor-DvC8cT4X.js +894 -0
- phoenix/server/static/assets/vendor-DxkFTwjz.css +1 -0
- phoenix/server/static/assets/vendor-arizeai-Do1793cv.js +662 -0
- phoenix/server/static/assets/vendor-codemirror-BzwZPyJM.js +24 -0
- phoenix/server/static/assets/vendor-recharts-_Jb7JjhG.js +59 -0
- phoenix/server/static/assets/vendor-shiki-Cl9QBraO.js +5 -0
- phoenix/server/static/assets/vendor-three-DwGkEfCM.js +2998 -0
- phoenix/server/telemetry.py +68 -0
- phoenix/server/templates/index.html +82 -23
- phoenix/server/thread_server.py +3 -3
- phoenix/server/types.py +275 -0
- phoenix/services.py +27 -18
- phoenix/session/client.py +743 -68
- phoenix/session/data_extractor.py +31 -7
- phoenix/session/evaluation.py +3 -9
- phoenix/session/session.py +263 -219
- phoenix/settings.py +22 -0
- phoenix/trace/__init__.py +2 -22
- phoenix/trace/attributes.py +338 -0
- phoenix/trace/dsl/README.md +116 -0
- phoenix/trace/dsl/filter.py +663 -213
- phoenix/trace/dsl/helpers.py +73 -21
- phoenix/trace/dsl/query.py +574 -201
- phoenix/trace/exporter.py +24 -19
- phoenix/trace/fixtures.py +368 -32
- phoenix/trace/otel.py +71 -219
- phoenix/trace/projects.py +3 -2
- phoenix/trace/schemas.py +33 -11
- phoenix/trace/span_evaluations.py +21 -16
- phoenix/trace/span_json_decoder.py +6 -4
- phoenix/trace/span_json_encoder.py +2 -2
- phoenix/trace/trace_dataset.py +47 -32
- phoenix/trace/utils.py +21 -4
- phoenix/utilities/__init__.py +0 -26
- phoenix/utilities/client.py +132 -0
- phoenix/utilities/deprecation.py +31 -0
- phoenix/utilities/error_handling.py +3 -2
- phoenix/utilities/json.py +109 -0
- phoenix/utilities/logging.py +8 -0
- phoenix/utilities/project.py +2 -2
- phoenix/utilities/re.py +49 -0
- phoenix/utilities/span_store.py +0 -23
- phoenix/utilities/template_formatters.py +99 -0
- phoenix/version.py +1 -1
- arize_phoenix-3.16.1.dist-info/METADATA +0 -495
- arize_phoenix-3.16.1.dist-info/RECORD +0 -178
- phoenix/core/project.py +0 -619
- phoenix/core/traces.py +0 -96
- phoenix/experimental/evals/__init__.py +0 -73
- phoenix/experimental/evals/evaluators.py +0 -413
- phoenix/experimental/evals/functions/__init__.py +0 -4
- phoenix/experimental/evals/functions/classify.py +0 -453
- phoenix/experimental/evals/functions/executor.py +0 -353
- phoenix/experimental/evals/functions/generate.py +0 -138
- phoenix/experimental/evals/functions/processing.py +0 -76
- phoenix/experimental/evals/models/__init__.py +0 -14
- phoenix/experimental/evals/models/anthropic.py +0 -175
- phoenix/experimental/evals/models/base.py +0 -170
- phoenix/experimental/evals/models/bedrock.py +0 -221
- phoenix/experimental/evals/models/litellm.py +0 -134
- phoenix/experimental/evals/models/openai.py +0 -448
- phoenix/experimental/evals/models/rate_limiters.py +0 -246
- phoenix/experimental/evals/models/vertex.py +0 -173
- phoenix/experimental/evals/models/vertexai.py +0 -186
- phoenix/experimental/evals/retrievals.py +0 -96
- phoenix/experimental/evals/templates/__init__.py +0 -50
- phoenix/experimental/evals/templates/default_templates.py +0 -472
- phoenix/experimental/evals/templates/template.py +0 -195
- phoenix/experimental/evals/utils/__init__.py +0 -172
- phoenix/experimental/evals/utils/threads.py +0 -27
- phoenix/server/api/helpers.py +0 -11
- phoenix/server/api/routers/evaluation_handler.py +0 -109
- phoenix/server/api/routers/span_handler.py +0 -70
- phoenix/server/api/routers/trace_handler.py +0 -60
- phoenix/server/api/types/DatasetRole.py +0 -23
- phoenix/server/static/index.css +0 -6
- phoenix/server/static/index.js +0 -7447
- phoenix/storage/span_store/__init__.py +0 -23
- phoenix/storage/span_store/text_file.py +0 -85
- phoenix/trace/dsl/missing.py +0 -60
- phoenix/trace/langchain/__init__.py +0 -3
- phoenix/trace/langchain/instrumentor.py +0 -35
- phoenix/trace/llama_index/__init__.py +0 -3
- phoenix/trace/llama_index/callback.py +0 -102
- phoenix/trace/openai/__init__.py +0 -3
- phoenix/trace/openai/instrumentor.py +0 -30
- {arize_phoenix-3.16.1.dist-info → arize_phoenix-7.7.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-3.16.1.dist-info → arize_phoenix-7.7.0.dist-info}/licenses/LICENSE +0 -0
- /phoenix/{datasets → db/insertion}/__init__.py +0 -0
- /phoenix/{experimental → db/migrations}/__init__.py +0 -0
- /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
|
+
)
|