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