arize-phoenix 4.12.1rc1__py3-none-any.whl → 4.14.1__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-4.12.1rc1.dist-info → arize_phoenix-4.14.1.dist-info}/METADATA +12 -9
- {arize_phoenix-4.12.1rc1.dist-info → arize_phoenix-4.14.1.dist-info}/RECORD +48 -49
- phoenix/db/bulk_inserter.py +3 -1
- phoenix/experiments/evaluators/base.py +4 -0
- phoenix/experiments/evaluators/code_evaluators.py +80 -0
- phoenix/experiments/evaluators/llm_evaluators.py +77 -1
- phoenix/experiments/evaluators/utils.py +70 -21
- phoenix/experiments/functions.py +14 -14
- phoenix/server/api/context.py +7 -3
- phoenix/server/api/dataloaders/average_experiment_run_latency.py +23 -23
- phoenix/server/api/dataloaders/experiment_error_rates.py +30 -10
- phoenix/server/api/dataloaders/experiment_run_counts.py +18 -5
- phoenix/server/api/input_types/{CreateSpanAnnotationsInput.py → CreateSpanAnnotationInput.py} +4 -2
- phoenix/server/api/input_types/{CreateTraceAnnotationsInput.py → CreateTraceAnnotationInput.py} +4 -2
- phoenix/server/api/input_types/{PatchAnnotationsInput.py → PatchAnnotationInput.py} +4 -2
- phoenix/server/api/mutations/span_annotations_mutations.py +12 -6
- phoenix/server/api/mutations/trace_annotations_mutations.py +12 -6
- phoenix/server/api/openapi/main.py +2 -18
- phoenix/server/api/openapi/schema.py +12 -12
- phoenix/server/api/routers/v1/__init__.py +83 -36
- phoenix/server/api/routers/v1/dataset_examples.py +123 -102
- phoenix/server/api/routers/v1/datasets.py +506 -390
- phoenix/server/api/routers/v1/evaluations.py +66 -73
- phoenix/server/api/routers/v1/experiment_evaluations.py +91 -68
- phoenix/server/api/routers/v1/experiment_runs.py +155 -98
- phoenix/server/api/routers/v1/experiments.py +181 -132
- phoenix/server/api/routers/v1/spans.py +173 -144
- phoenix/server/api/routers/v1/traces.py +128 -115
- phoenix/server/api/types/Experiment.py +2 -2
- phoenix/server/api/types/Inferences.py +1 -2
- phoenix/server/api/types/Model.py +1 -2
- phoenix/server/app.py +177 -152
- phoenix/server/openapi/docs.py +221 -0
- phoenix/server/static/.vite/manifest.json +31 -31
- phoenix/server/static/assets/{components-C8sm_r1F.js → components-DeS0YEmv.js} +2 -2
- phoenix/server/static/assets/index-CQgXRwU0.js +100 -0
- phoenix/server/static/assets/{pages-bN7juCjh.js → pages-hdjlFZhO.js} +275 -198
- phoenix/server/static/assets/{vendor-CUDAPm8e.js → vendor-DPvSDRn3.js} +1 -1
- phoenix/server/static/assets/{vendor-arizeai-Do2HOmcL.js → vendor-arizeai-CkvPT67c.js} +2 -2
- phoenix/server/static/assets/{vendor-codemirror-CrdxOlMs.js → vendor-codemirror-Cqwpwlua.js} +1 -1
- phoenix/server/static/assets/{vendor-recharts-PKRvByVe.js → vendor-recharts-5jlNaZuF.js} +1 -1
- phoenix/server/thread_server.py +2 -2
- phoenix/session/client.py +9 -8
- phoenix/trace/dsl/filter.py +40 -25
- phoenix/version.py +1 -1
- phoenix/server/api/routers/v1/pydantic_compat.py +0 -78
- phoenix/server/api/routers/v1/utils.py +0 -95
- phoenix/server/static/assets/index-BEKPzgQs.js +0 -100
- {arize_phoenix-4.12.1rc1.dist-info → arize_phoenix-4.14.1.dist-info}/WHEEL +0 -0
- {arize_phoenix-4.12.1rc1.dist-info → arize_phoenix-4.14.1.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-4.12.1rc1.dist-info → arize_phoenix-4.14.1.dist-info}/licenses/LICENSE +0 -0
phoenix/server/app.py
CHANGED
|
@@ -21,24 +21,25 @@ from typing import (
|
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
import strawberry
|
|
24
|
-
from fastapi import APIRouter, FastAPI
|
|
25
|
-
from fastapi.middleware.gzip import GZipMiddleware
|
|
26
|
-
from fastapi.responses import FileResponse
|
|
27
|
-
from fastapi.utils import is_body_allowed_for_status_code
|
|
28
24
|
from sqlalchemy.ext.asyncio import (
|
|
29
25
|
AsyncEngine,
|
|
30
26
|
AsyncSession,
|
|
31
27
|
async_sessionmaker,
|
|
32
28
|
)
|
|
29
|
+
from starlette.applications import Starlette
|
|
30
|
+
from starlette.datastructures import QueryParams
|
|
31
|
+
from starlette.endpoints import HTTPEndpoint
|
|
33
32
|
from starlette.exceptions import HTTPException
|
|
34
33
|
from starlette.middleware import Middleware
|
|
35
34
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
36
35
|
from starlette.requests import Request
|
|
37
|
-
from starlette.responses import PlainTextResponse, Response
|
|
36
|
+
from starlette.responses import FileResponse, PlainTextResponse, Response
|
|
37
|
+
from starlette.routing import Mount, Route
|
|
38
38
|
from starlette.staticfiles import StaticFiles
|
|
39
39
|
from starlette.templating import Jinja2Templates
|
|
40
40
|
from starlette.types import Scope, StatefulLifespan
|
|
41
|
-
from
|
|
41
|
+
from starlette.websockets import WebSocket
|
|
42
|
+
from strawberry.asgi import GraphQL
|
|
42
43
|
from strawberry.schema import BaseSchema
|
|
43
44
|
from typing_extensions import TypeAlias
|
|
44
45
|
|
|
@@ -81,10 +82,11 @@ from phoenix.server.api.dataloaders import (
|
|
|
81
82
|
TraceEvaluationsDataLoader,
|
|
82
83
|
TraceRowIdsDataLoader,
|
|
83
84
|
)
|
|
84
|
-
from phoenix.server.api.
|
|
85
|
-
from phoenix.server.api.routers.v1 import
|
|
85
|
+
from phoenix.server.api.openapi.schema import OPENAPI_SCHEMA_GENERATOR
|
|
86
|
+
from phoenix.server.api.routers.v1 import V1_ROUTES
|
|
86
87
|
from phoenix.server.api.schema import schema
|
|
87
88
|
from phoenix.server.grpc_server import GrpcServer
|
|
89
|
+
from phoenix.server.openapi.docs import get_swagger_ui_html
|
|
88
90
|
from phoenix.server.telemetry import initialize_opentelemetry_tracer_provider
|
|
89
91
|
from phoenix.trace.schemas import Span
|
|
90
92
|
|
|
@@ -93,8 +95,6 @@ if TYPE_CHECKING:
|
|
|
93
95
|
|
|
94
96
|
logger = logging.getLogger(__name__)
|
|
95
97
|
|
|
96
|
-
router = APIRouter(include_in_schema=False)
|
|
97
|
-
|
|
98
98
|
templates = Jinja2Templates(directory=SERVER_DIR / "templates")
|
|
99
99
|
|
|
100
100
|
|
|
@@ -169,27 +169,122 @@ class HeadersMiddleware(BaseHTTPMiddleware):
|
|
|
169
169
|
) -> Response:
|
|
170
170
|
response = await call_next(request)
|
|
171
171
|
response.headers["x-colab-notebook-cache-control"] = "no-cache"
|
|
172
|
-
response.headers["Cache-Control"] = "no-store"
|
|
173
172
|
return response
|
|
174
173
|
|
|
175
174
|
|
|
176
175
|
ProjectRowId: TypeAlias = int
|
|
177
176
|
|
|
178
177
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
178
|
+
class GraphQLWithContext(GraphQL): # type: ignore
|
|
179
|
+
def __init__(
|
|
180
|
+
self,
|
|
181
|
+
schema: BaseSchema,
|
|
182
|
+
db: Callable[[], AsyncContextManager[AsyncSession]],
|
|
183
|
+
model: Model,
|
|
184
|
+
export_path: Path,
|
|
185
|
+
graphiql: bool = False,
|
|
186
|
+
corpus: Optional[Model] = None,
|
|
187
|
+
streaming_last_updated_at: Callable[[ProjectRowId], Optional[datetime]] = lambda _: None,
|
|
188
|
+
cache_for_dataloaders: Optional[CacheForDataLoaders] = None,
|
|
189
|
+
read_only: bool = False,
|
|
190
|
+
) -> None:
|
|
191
|
+
self.db = db
|
|
192
|
+
self.model = model
|
|
193
|
+
self.corpus = corpus
|
|
194
|
+
self.export_path = export_path
|
|
195
|
+
self.streaming_last_updated_at = streaming_last_updated_at
|
|
196
|
+
self.cache_for_dataloaders = cache_for_dataloaders
|
|
197
|
+
self.read_only = read_only
|
|
198
|
+
super().__init__(schema, graphiql=graphiql)
|
|
199
|
+
|
|
200
|
+
async def get_context(
|
|
201
|
+
self,
|
|
202
|
+
request: Union[Request, WebSocket],
|
|
203
|
+
response: Optional[Response] = None,
|
|
204
|
+
) -> Context:
|
|
205
|
+
return Context(
|
|
206
|
+
request=request,
|
|
207
|
+
response=response,
|
|
208
|
+
db=self.db,
|
|
209
|
+
model=self.model,
|
|
210
|
+
corpus=self.corpus,
|
|
211
|
+
export_path=self.export_path,
|
|
212
|
+
streaming_last_updated_at=self.streaming_last_updated_at,
|
|
213
|
+
data_loaders=DataLoaders(
|
|
214
|
+
average_experiment_run_latency=AverageExperimentRunLatencyDataLoader(self.db),
|
|
215
|
+
dataset_example_revisions=DatasetExampleRevisionsDataLoader(self.db),
|
|
216
|
+
dataset_example_spans=DatasetExampleSpansDataLoader(self.db),
|
|
217
|
+
document_evaluation_summaries=DocumentEvaluationSummaryDataLoader(
|
|
218
|
+
self.db,
|
|
219
|
+
cache_map=self.cache_for_dataloaders.document_evaluation_summary
|
|
220
|
+
if self.cache_for_dataloaders
|
|
221
|
+
else None,
|
|
222
|
+
),
|
|
223
|
+
document_evaluations=DocumentEvaluationsDataLoader(self.db),
|
|
224
|
+
document_retrieval_metrics=DocumentRetrievalMetricsDataLoader(self.db),
|
|
225
|
+
evaluation_summaries=EvaluationSummaryDataLoader(
|
|
226
|
+
self.db,
|
|
227
|
+
cache_map=self.cache_for_dataloaders.evaluation_summary
|
|
228
|
+
if self.cache_for_dataloaders
|
|
229
|
+
else None,
|
|
230
|
+
),
|
|
231
|
+
experiment_annotation_summaries=ExperimentAnnotationSummaryDataLoader(self.db),
|
|
232
|
+
experiment_error_rates=ExperimentErrorRatesDataLoader(self.db),
|
|
233
|
+
experiment_run_counts=ExperimentRunCountsDataLoader(self.db),
|
|
234
|
+
experiment_sequence_number=ExperimentSequenceNumberDataLoader(self.db),
|
|
235
|
+
latency_ms_quantile=LatencyMsQuantileDataLoader(
|
|
236
|
+
self.db,
|
|
237
|
+
cache_map=self.cache_for_dataloaders.latency_ms_quantile
|
|
238
|
+
if self.cache_for_dataloaders
|
|
239
|
+
else None,
|
|
240
|
+
),
|
|
241
|
+
min_start_or_max_end_times=MinStartOrMaxEndTimeDataLoader(
|
|
242
|
+
self.db,
|
|
243
|
+
cache_map=self.cache_for_dataloaders.min_start_or_max_end_time
|
|
244
|
+
if self.cache_for_dataloaders
|
|
245
|
+
else None,
|
|
246
|
+
),
|
|
247
|
+
record_counts=RecordCountDataLoader(
|
|
248
|
+
self.db,
|
|
249
|
+
cache_map=self.cache_for_dataloaders.record_count
|
|
250
|
+
if self.cache_for_dataloaders
|
|
251
|
+
else None,
|
|
252
|
+
),
|
|
253
|
+
span_descendants=SpanDescendantsDataLoader(self.db),
|
|
254
|
+
span_evaluations=SpanEvaluationsDataLoader(self.db),
|
|
255
|
+
span_projects=SpanProjectsDataLoader(self.db),
|
|
256
|
+
token_counts=TokenCountDataLoader(
|
|
257
|
+
self.db,
|
|
258
|
+
cache_map=self.cache_for_dataloaders.token_count
|
|
259
|
+
if self.cache_for_dataloaders
|
|
260
|
+
else None,
|
|
261
|
+
),
|
|
262
|
+
trace_evaluations=TraceEvaluationsDataLoader(self.db),
|
|
263
|
+
trace_row_ids=TraceRowIdsDataLoader(self.db),
|
|
264
|
+
project_by_name=ProjectByNameDataLoader(self.db),
|
|
265
|
+
span_annotations=SpanAnnotationsDataLoader(self.db),
|
|
266
|
+
),
|
|
267
|
+
cache_for_dataloaders=self.cache_for_dataloaders,
|
|
268
|
+
read_only=self.read_only,
|
|
269
|
+
)
|
|
270
|
+
|
|
189
271
|
|
|
272
|
+
class Download(HTTPEndpoint):
|
|
273
|
+
path: Path
|
|
190
274
|
|
|
191
|
-
|
|
192
|
-
|
|
275
|
+
async def get(self, request: Request) -> FileResponse:
|
|
276
|
+
params = QueryParams(request.query_params)
|
|
277
|
+
file = self.path / (params.get("filename", "") + ".parquet")
|
|
278
|
+
if not file.is_file():
|
|
279
|
+
raise HTTPException(status_code=404)
|
|
280
|
+
return FileResponse(
|
|
281
|
+
path=file,
|
|
282
|
+
filename=file.name,
|
|
283
|
+
media_type="application/x-octet-stream",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
async def version(_: Request) -> PlainTextResponse:
|
|
193
288
|
return PlainTextResponse(f"{phoenix.__version__}")
|
|
194
289
|
|
|
195
290
|
|
|
@@ -211,9 +306,9 @@ def _lifespan(
|
|
|
211
306
|
enable_prometheus: bool = False,
|
|
212
307
|
clean_ups: Iterable[Callable[[], None]] = (),
|
|
213
308
|
read_only: bool = False,
|
|
214
|
-
) -> StatefulLifespan[
|
|
309
|
+
) -> StatefulLifespan[Starlette]:
|
|
215
310
|
@contextlib.asynccontextmanager
|
|
216
|
-
async def lifespan(_:
|
|
311
|
+
async def lifespan(_: Starlette) -> AsyncIterator[Dict[str, Any]]:
|
|
217
312
|
async with bulk_inserter as (
|
|
218
313
|
queue_span,
|
|
219
314
|
queue_evaluation,
|
|
@@ -235,90 +330,16 @@ def _lifespan(
|
|
|
235
330
|
return lifespan
|
|
236
331
|
|
|
237
332
|
|
|
238
|
-
@router.get("/healthz")
|
|
239
333
|
async def check_healthz(_: Request) -> PlainTextResponse:
|
|
240
334
|
return PlainTextResponse("OK")
|
|
241
335
|
|
|
242
336
|
|
|
243
|
-
def
|
|
244
|
-
|
|
245
|
-
schema: BaseSchema,
|
|
246
|
-
db: Callable[[], AsyncContextManager[AsyncSession]],
|
|
247
|
-
model: Model,
|
|
248
|
-
export_path: Path,
|
|
249
|
-
corpus: Optional[Model] = None,
|
|
250
|
-
streaming_last_updated_at: Callable[[ProjectRowId], Optional[datetime]] = lambda _: None,
|
|
251
|
-
cache_for_dataloaders: Optional[CacheForDataLoaders] = None,
|
|
252
|
-
read_only: bool = False,
|
|
253
|
-
) -> GraphQLRouter: # type: ignore[type-arg]
|
|
254
|
-
def get_context() -> Context:
|
|
255
|
-
return Context(
|
|
256
|
-
db=db,
|
|
257
|
-
model=model,
|
|
258
|
-
corpus=corpus,
|
|
259
|
-
export_path=export_path,
|
|
260
|
-
streaming_last_updated_at=streaming_last_updated_at,
|
|
261
|
-
data_loaders=DataLoaders(
|
|
262
|
-
average_experiment_run_latency=AverageExperimentRunLatencyDataLoader(db),
|
|
263
|
-
dataset_example_revisions=DatasetExampleRevisionsDataLoader(db),
|
|
264
|
-
dataset_example_spans=DatasetExampleSpansDataLoader(db),
|
|
265
|
-
document_evaluation_summaries=DocumentEvaluationSummaryDataLoader(
|
|
266
|
-
db,
|
|
267
|
-
cache_map=cache_for_dataloaders.document_evaluation_summary
|
|
268
|
-
if cache_for_dataloaders
|
|
269
|
-
else None,
|
|
270
|
-
),
|
|
271
|
-
document_evaluations=DocumentEvaluationsDataLoader(db),
|
|
272
|
-
document_retrieval_metrics=DocumentRetrievalMetricsDataLoader(db),
|
|
273
|
-
evaluation_summaries=EvaluationSummaryDataLoader(
|
|
274
|
-
db,
|
|
275
|
-
cache_map=cache_for_dataloaders.evaluation_summary
|
|
276
|
-
if cache_for_dataloaders
|
|
277
|
-
else None,
|
|
278
|
-
),
|
|
279
|
-
experiment_annotation_summaries=ExperimentAnnotationSummaryDataLoader(db),
|
|
280
|
-
experiment_error_rates=ExperimentErrorRatesDataLoader(db),
|
|
281
|
-
experiment_run_counts=ExperimentRunCountsDataLoader(db),
|
|
282
|
-
experiment_sequence_number=ExperimentSequenceNumberDataLoader(db),
|
|
283
|
-
latency_ms_quantile=LatencyMsQuantileDataLoader(
|
|
284
|
-
db,
|
|
285
|
-
cache_map=cache_for_dataloaders.latency_ms_quantile
|
|
286
|
-
if cache_for_dataloaders
|
|
287
|
-
else None,
|
|
288
|
-
),
|
|
289
|
-
min_start_or_max_end_times=MinStartOrMaxEndTimeDataLoader(
|
|
290
|
-
db,
|
|
291
|
-
cache_map=cache_for_dataloaders.min_start_or_max_end_time
|
|
292
|
-
if cache_for_dataloaders
|
|
293
|
-
else None,
|
|
294
|
-
),
|
|
295
|
-
record_counts=RecordCountDataLoader(
|
|
296
|
-
db,
|
|
297
|
-
cache_map=cache_for_dataloaders.record_count if cache_for_dataloaders else None,
|
|
298
|
-
),
|
|
299
|
-
span_annotations=SpanAnnotationsDataLoader(db),
|
|
300
|
-
span_descendants=SpanDescendantsDataLoader(db),
|
|
301
|
-
span_evaluations=SpanEvaluationsDataLoader(db),
|
|
302
|
-
span_projects=SpanProjectsDataLoader(db),
|
|
303
|
-
token_counts=TokenCountDataLoader(
|
|
304
|
-
db,
|
|
305
|
-
cache_map=cache_for_dataloaders.token_count if cache_for_dataloaders else None,
|
|
306
|
-
),
|
|
307
|
-
trace_evaluations=TraceEvaluationsDataLoader(db),
|
|
308
|
-
trace_row_ids=TraceRowIdsDataLoader(db),
|
|
309
|
-
project_by_name=ProjectByNameDataLoader(db),
|
|
310
|
-
),
|
|
311
|
-
cache_for_dataloaders=cache_for_dataloaders,
|
|
312
|
-
read_only=read_only,
|
|
313
|
-
)
|
|
337
|
+
async def openapi_schema(request: Request) -> Response:
|
|
338
|
+
return OPENAPI_SCHEMA_GENERATOR.OpenAPIResponse(request=request)
|
|
314
339
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
context_getter=get_context,
|
|
319
|
-
include_in_schema=False,
|
|
320
|
-
prefix="/graphql",
|
|
321
|
-
)
|
|
340
|
+
|
|
341
|
+
async def api_docs(request: Request) -> Response:
|
|
342
|
+
return get_swagger_ui_html(openapi_url="/schema", title="arize-phoenix API")
|
|
322
343
|
|
|
323
344
|
|
|
324
345
|
class SessionFactory:
|
|
@@ -369,18 +390,6 @@ def instrument_engine_if_enabled(engine: AsyncEngine) -> List[Callable[[], None]
|
|
|
369
390
|
return instrumentation_cleanups
|
|
370
391
|
|
|
371
392
|
|
|
372
|
-
async def plain_text_http_exception_handler(request: Request, exc: HTTPException) -> Response:
|
|
373
|
-
"""
|
|
374
|
-
Overrides the default handler for HTTPExceptions to return a plain text
|
|
375
|
-
response instead of a JSON response. For the original source code, see
|
|
376
|
-
https://github.com/tiangolo/fastapi/blob/d3cdd3bbd14109f3b268df7ca496e24bb64593aa/fastapi/exception_handlers.py#L11
|
|
377
|
-
"""
|
|
378
|
-
headers = getattr(exc, "headers", None)
|
|
379
|
-
if not is_body_allowed_for_status_code(exc.status_code):
|
|
380
|
-
return Response(status_code=exc.status_code, headers=headers)
|
|
381
|
-
return PlainTextResponse(str(exc.detail), status_code=exc.status_code, headers=headers)
|
|
382
|
-
|
|
383
|
-
|
|
384
393
|
def create_app(
|
|
385
394
|
db: SessionFactory,
|
|
386
395
|
export_path: Path,
|
|
@@ -395,7 +404,7 @@ def create_app(
|
|
|
395
404
|
initial_evaluations: Optional[Iterable[pb.Evaluation]] = None,
|
|
396
405
|
serve_ui: bool = True,
|
|
397
406
|
clean_up_callbacks: List[Callable[[], None]] = [],
|
|
398
|
-
) ->
|
|
407
|
+
) -> Starlette:
|
|
399
408
|
clean_ups: List[Callable[[], None]] = clean_up_callbacks # To be called at app shutdown.
|
|
400
409
|
initial_batch_of_spans: Iterable[Tuple[Span, str]] = (
|
|
401
410
|
()
|
|
@@ -438,7 +447,7 @@ def create_app(
|
|
|
438
447
|
|
|
439
448
|
strawberry_extensions.append(_OpenTelemetryExtension)
|
|
440
449
|
|
|
441
|
-
|
|
450
|
+
graphql = GraphQLWithContext(
|
|
442
451
|
db=db,
|
|
443
452
|
schema=strawberry.Schema(
|
|
444
453
|
query=schema.query,
|
|
@@ -449,6 +458,7 @@ def create_app(
|
|
|
449
458
|
model=model,
|
|
450
459
|
corpus=corpus,
|
|
451
460
|
export_path=export_path,
|
|
461
|
+
graphiql=True,
|
|
452
462
|
streaming_last_updated_at=bulk_inserter.last_updated_at,
|
|
453
463
|
cache_for_dataloaders=cache_for_dataloaders,
|
|
454
464
|
read_only=read_only,
|
|
@@ -459,9 +469,7 @@ def create_app(
|
|
|
459
469
|
prometheus_middlewares = [Middleware(PrometheusMiddleware)]
|
|
460
470
|
else:
|
|
461
471
|
prometheus_middlewares = []
|
|
462
|
-
app =
|
|
463
|
-
title="Arize-Phoenix REST API",
|
|
464
|
-
version=REST_API_VERSION,
|
|
472
|
+
app = Starlette(
|
|
465
473
|
lifespan=_lifespan(
|
|
466
474
|
read_only=read_only,
|
|
467
475
|
bulk_inserter=bulk_inserter,
|
|
@@ -473,41 +481,58 @@ def create_app(
|
|
|
473
481
|
Middleware(HeadersMiddleware),
|
|
474
482
|
*prometheus_middlewares,
|
|
475
483
|
],
|
|
476
|
-
exception_handlers={HTTPException: plain_text_http_exception_handler},
|
|
477
484
|
debug=debug,
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
app.mount(
|
|
490
|
-
"/",
|
|
491
|
-
app=Static(
|
|
492
|
-
directory=SERVER_DIR / "static",
|
|
493
|
-
app_config=AppConfig(
|
|
494
|
-
has_inferences=model.is_empty is not True,
|
|
495
|
-
has_corpus=corpus is not None,
|
|
496
|
-
min_dist=umap_params.min_dist,
|
|
497
|
-
n_neighbors=umap_params.n_neighbors,
|
|
498
|
-
n_samples=umap_params.n_samples,
|
|
499
|
-
is_development=dev,
|
|
500
|
-
web_manifest_path=SERVER_DIR / "static" / ".vite" / "manifest.json",
|
|
485
|
+
routes=V1_ROUTES
|
|
486
|
+
+ [
|
|
487
|
+
Route("/schema", endpoint=openapi_schema, include_in_schema=False),
|
|
488
|
+
Route("/arize_phoenix_version", version),
|
|
489
|
+
Route("/healthz", check_healthz),
|
|
490
|
+
Route(
|
|
491
|
+
"/exports",
|
|
492
|
+
type(
|
|
493
|
+
"DownloadExports",
|
|
494
|
+
(Download,),
|
|
495
|
+
{"path": export_path},
|
|
501
496
|
),
|
|
502
497
|
),
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
498
|
+
Route(
|
|
499
|
+
"/docs",
|
|
500
|
+
api_docs,
|
|
501
|
+
),
|
|
502
|
+
Route(
|
|
503
|
+
"/graphql",
|
|
504
|
+
graphql,
|
|
505
|
+
),
|
|
506
|
+
]
|
|
507
|
+
+ (
|
|
508
|
+
[
|
|
509
|
+
Mount(
|
|
510
|
+
"/",
|
|
511
|
+
app=Static(
|
|
512
|
+
directory=SERVER_DIR / "static",
|
|
513
|
+
app_config=AppConfig(
|
|
514
|
+
has_inferences=model.is_empty is not True,
|
|
515
|
+
has_corpus=corpus is not None,
|
|
516
|
+
min_dist=umap_params.min_dist,
|
|
517
|
+
n_neighbors=umap_params.n_neighbors,
|
|
518
|
+
n_samples=umap_params.n_samples,
|
|
519
|
+
is_development=dev,
|
|
520
|
+
web_manifest_path=SERVER_DIR / "static" / ".vite" / "manifest.json",
|
|
521
|
+
),
|
|
522
|
+
),
|
|
523
|
+
name="static",
|
|
524
|
+
),
|
|
525
|
+
]
|
|
526
|
+
if serve_ui
|
|
527
|
+
else []
|
|
528
|
+
),
|
|
529
|
+
)
|
|
530
|
+
app.state.read_only = read_only
|
|
506
531
|
app.state.db = db
|
|
507
532
|
if tracer_provider:
|
|
508
|
-
from opentelemetry.instrumentation.
|
|
533
|
+
from opentelemetry.instrumentation.starlette import StarletteInstrumentor
|
|
509
534
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
clean_ups.append(
|
|
535
|
+
StarletteInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
536
|
+
StarletteInstrumentor.instrument_app(app, tracer_provider=tracer_provider)
|
|
537
|
+
clean_ups.append(StarletteInstrumentor().uninstrument)
|
|
513
538
|
return app
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
from starlette.responses import HTMLResponse
|
|
5
|
+
|
|
6
|
+
swagger_ui_default_parameters: Dict[str, Any] = {
|
|
7
|
+
"dom_id": "#swagger-ui",
|
|
8
|
+
"layout": "BaseLayout",
|
|
9
|
+
"deepLinking": True,
|
|
10
|
+
"showExtensions": True,
|
|
11
|
+
"showCommonExtensions": True,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_swagger_ui_html(
|
|
16
|
+
*,
|
|
17
|
+
openapi_url: str = "/schema",
|
|
18
|
+
title: str,
|
|
19
|
+
swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui-bundle.js",
|
|
20
|
+
swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui.css",
|
|
21
|
+
swagger_favicon_url: str = "/favicon.ico",
|
|
22
|
+
oauth2_redirect_url: Optional[str] = None,
|
|
23
|
+
init_oauth: Optional[str] = None,
|
|
24
|
+
swagger_ui_parameters: Optional[Dict[str, Any]] = None,
|
|
25
|
+
) -> HTMLResponse:
|
|
26
|
+
"""
|
|
27
|
+
Generate and return the HTML that loads Swagger UI for the interactive API
|
|
28
|
+
docs (normally served at `/docs`).
|
|
29
|
+
"""
|
|
30
|
+
current_swagger_ui_parameters = swagger_ui_default_parameters.copy()
|
|
31
|
+
if swagger_ui_parameters:
|
|
32
|
+
current_swagger_ui_parameters.update(swagger_ui_parameters)
|
|
33
|
+
|
|
34
|
+
html = f"""
|
|
35
|
+
<!DOCTYPE html>
|
|
36
|
+
<html>
|
|
37
|
+
<head>
|
|
38
|
+
<link type="text/css" rel="stylesheet" href="{swagger_css_url}">
|
|
39
|
+
<link rel="shortcut icon" href="{swagger_favicon_url}">
|
|
40
|
+
<title>{title}</title>
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
<div id="swagger-ui">
|
|
44
|
+
</div>
|
|
45
|
+
<script src="{swagger_js_url}"></script>
|
|
46
|
+
<style type="text/css">
|
|
47
|
+
div[id^="operations-private"]{{display:none}} #operations-tag-private{{display:none}}
|
|
48
|
+
</style>
|
|
49
|
+
<!-- `SwaggerUIBundle` is now available on the page -->
|
|
50
|
+
<script>
|
|
51
|
+
const ui = SwaggerUIBundle({{
|
|
52
|
+
url: '{openapi_url}',
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
for key, value in current_swagger_ui_parameters.items():
|
|
56
|
+
html += f"{json.dumps(key)}: {json.dumps(value)},\n"
|
|
57
|
+
|
|
58
|
+
if oauth2_redirect_url:
|
|
59
|
+
html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',"
|
|
60
|
+
|
|
61
|
+
html += """
|
|
62
|
+
presets: [
|
|
63
|
+
SwaggerUIBundle.presets.apis,
|
|
64
|
+
SwaggerUIBundle.SwaggerUIStandalonePreset
|
|
65
|
+
],
|
|
66
|
+
})"""
|
|
67
|
+
|
|
68
|
+
if init_oauth:
|
|
69
|
+
html += f"""
|
|
70
|
+
ui.initOAuth({json.dumps(init_oauth)})
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
html += """
|
|
74
|
+
</script>
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
|
77
|
+
"""
|
|
78
|
+
return HTMLResponse(html)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_redoc_html(
|
|
82
|
+
*,
|
|
83
|
+
openapi_url: str,
|
|
84
|
+
title: str,
|
|
85
|
+
redoc_js_url: str = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js",
|
|
86
|
+
redoc_favicon_url: str = "/favicon.ico",
|
|
87
|
+
with_google_fonts: bool = True,
|
|
88
|
+
) -> HTMLResponse:
|
|
89
|
+
"""
|
|
90
|
+
Generate and return the HTML response that loads ReDoc for the alternative
|
|
91
|
+
API docs (normally served at `/redoc`).
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
html = f"""
|
|
96
|
+
<!DOCTYPE html>
|
|
97
|
+
<html>
|
|
98
|
+
<head>
|
|
99
|
+
<title>{title}</title>
|
|
100
|
+
<!-- needed for adaptive design -->
|
|
101
|
+
<meta charset="utf-8"/>
|
|
102
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
103
|
+
"""
|
|
104
|
+
if with_google_fonts:
|
|
105
|
+
html += """
|
|
106
|
+
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
|
107
|
+
""" # noqa: E501
|
|
108
|
+
html += f"""
|
|
109
|
+
<link rel="shortcut icon" href="{redoc_favicon_url}">
|
|
110
|
+
<!--
|
|
111
|
+
ReDoc doesn't change outer page styles
|
|
112
|
+
-->
|
|
113
|
+
<style>
|
|
114
|
+
body {{
|
|
115
|
+
margin: 0;
|
|
116
|
+
padding: 0;
|
|
117
|
+
}}
|
|
118
|
+
</style>
|
|
119
|
+
</head>
|
|
120
|
+
<body>
|
|
121
|
+
<noscript>
|
|
122
|
+
ReDoc requires Javascript to function. Please enable it to browse the documentation.
|
|
123
|
+
</noscript>
|
|
124
|
+
<redoc spec-url="{openapi_url}"></redoc>
|
|
125
|
+
<script src="{redoc_js_url}"> </script>
|
|
126
|
+
</body>
|
|
127
|
+
</html>
|
|
128
|
+
"""
|
|
129
|
+
return HTMLResponse(html)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# Not needed now but copy-pasting for future reference
|
|
133
|
+
def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse:
|
|
134
|
+
"""
|
|
135
|
+
Generate the HTML response with the OAuth2 redirection for Swagger UI.
|
|
136
|
+
|
|
137
|
+
You normally don't need to use or change this.
|
|
138
|
+
"""
|
|
139
|
+
# copied from https://github.com/swagger-api/swagger-ui/blob/v4.14.0/dist/oauth2-redirect.html
|
|
140
|
+
html = """
|
|
141
|
+
<!doctype html>
|
|
142
|
+
<html lang="en-US">
|
|
143
|
+
<head>
|
|
144
|
+
<title>Swagger UI: OAuth2 Redirect</title>
|
|
145
|
+
</head>
|
|
146
|
+
<body>
|
|
147
|
+
<script>
|
|
148
|
+
'use strict';
|
|
149
|
+
function run () {
|
|
150
|
+
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
|
151
|
+
var sentState = oauth2.state;
|
|
152
|
+
var redirectUrl = oauth2.redirectUrl;
|
|
153
|
+
var isValid, qp, arr;
|
|
154
|
+
|
|
155
|
+
if (/code|token|error/.test(window.location.hash)) {
|
|
156
|
+
qp = window.location.hash.substring(1).replace('?', '&');
|
|
157
|
+
} else {
|
|
158
|
+
qp = location.search.substring(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
arr = qp.split("&");
|
|
162
|
+
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
|
|
163
|
+
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
|
164
|
+
function (key, value) {
|
|
165
|
+
return key === "" ? value : decodeURIComponent(value);
|
|
166
|
+
}
|
|
167
|
+
) : {};
|
|
168
|
+
|
|
169
|
+
isValid = qp.state === sentState;
|
|
170
|
+
|
|
171
|
+
if ((
|
|
172
|
+
oauth2.auth.schema.get("flow") === "accessCode" ||
|
|
173
|
+
oauth2.auth.schema.get("flow") === "authorizationCode" ||
|
|
174
|
+
oauth2.auth.schema.get("flow") === "authorization_code"
|
|
175
|
+
) && !oauth2.auth.code) {
|
|
176
|
+
if (!isValid) {
|
|
177
|
+
oauth2.errCb({
|
|
178
|
+
authId: oauth2.auth.name,
|
|
179
|
+
source: "auth",
|
|
180
|
+
level: "warning",
|
|
181
|
+
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (qp.code) {
|
|
186
|
+
delete oauth2.state;
|
|
187
|
+
oauth2.auth.code = qp.code;
|
|
188
|
+
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
|
189
|
+
} else {
|
|
190
|
+
let oauthErrorMsg;
|
|
191
|
+
if (qp.error) {
|
|
192
|
+
oauthErrorMsg = "["+qp.error+"]: " +
|
|
193
|
+
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
|
194
|
+
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
oauth2.errCb({
|
|
198
|
+
authId: oauth2.auth.name,
|
|
199
|
+
source: "auth",
|
|
200
|
+
level: "error",
|
|
201
|
+
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
|
206
|
+
}
|
|
207
|
+
window.close();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (document.readyState !== 'loading') {
|
|
211
|
+
run();
|
|
212
|
+
} else {
|
|
213
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
214
|
+
run();
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
</script>
|
|
218
|
+
</body>
|
|
219
|
+
</html>
|
|
220
|
+
""" # noqa: E501
|
|
221
|
+
return HTMLResponse(content=html)
|