arize-phoenix 4.10.1__py3-none-any.whl → 4.10.2rc1__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.10.1.dist-info → arize_phoenix-4.10.2rc1.dist-info}/METADATA +4 -3
- {arize_phoenix-4.10.1.dist-info → arize_phoenix-4.10.2rc1.dist-info}/RECORD +27 -25
- phoenix/server/api/context.py +5 -7
- phoenix/server/api/dataloaders/__init__.py +2 -0
- phoenix/server/api/dataloaders/span_annotations.py +35 -0
- phoenix/server/api/openapi/main.py +18 -2
- phoenix/server/api/openapi/schema.py +12 -12
- phoenix/server/api/routers/v1/__init__.py +36 -83
- phoenix/server/api/routers/v1/dataset_examples.py +102 -123
- phoenix/server/api/routers/v1/datasets.py +389 -507
- phoenix/server/api/routers/v1/evaluations.py +74 -64
- phoenix/server/api/routers/v1/experiment_evaluations.py +67 -91
- phoenix/server/api/routers/v1/experiment_runs.py +97 -155
- phoenix/server/api/routers/v1/experiments.py +131 -181
- phoenix/server/api/routers/v1/spans.py +141 -173
- phoenix/server/api/routers/v1/traces.py +113 -128
- phoenix/server/api/routers/v1/utils.py +94 -0
- phoenix/server/api/types/Annotation.py +21 -0
- phoenix/server/api/types/Evaluation.py +4 -22
- phoenix/server/api/types/Span.py +15 -4
- phoenix/server/api/types/SpanAnnotation.py +4 -6
- phoenix/server/app.py +149 -174
- phoenix/server/thread_server.py +2 -2
- phoenix/version.py +1 -1
- phoenix/server/openapi/docs.py +0 -221
- {arize_phoenix-4.10.1.dist-info → arize_phoenix-4.10.2rc1.dist-info}/WHEEL +0 -0
- {arize_phoenix-4.10.1.dist-info → arize_phoenix-4.10.2rc1.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-4.10.1.dist-info → arize_phoenix-4.10.2rc1.dist-info}/licenses/LICENSE +0 -0
phoenix/server/app.py
CHANGED
|
@@ -19,25 +19,24 @@ from typing import (
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
import strawberry
|
|
22
|
+
from fastapi import APIRouter, FastAPI
|
|
23
|
+
from fastapi.middleware.gzip import GZipMiddleware
|
|
24
|
+
from fastapi.responses import FileResponse
|
|
25
|
+
from fastapi.utils import is_body_allowed_for_status_code
|
|
22
26
|
from sqlalchemy.ext.asyncio import (
|
|
23
27
|
AsyncEngine,
|
|
24
28
|
AsyncSession,
|
|
25
29
|
async_sessionmaker,
|
|
26
30
|
)
|
|
27
|
-
from starlette.applications import Starlette
|
|
28
|
-
from starlette.datastructures import QueryParams
|
|
29
|
-
from starlette.endpoints import HTTPEndpoint
|
|
30
31
|
from starlette.exceptions import HTTPException
|
|
31
32
|
from starlette.middleware import Middleware
|
|
32
33
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
33
34
|
from starlette.requests import Request
|
|
34
|
-
from starlette.responses import
|
|
35
|
-
from starlette.routing import Mount, Route
|
|
35
|
+
from starlette.responses import PlainTextResponse, Response
|
|
36
36
|
from starlette.staticfiles import StaticFiles
|
|
37
37
|
from starlette.templating import Jinja2Templates
|
|
38
38
|
from starlette.types import Scope, StatefulLifespan
|
|
39
|
-
from
|
|
40
|
-
from strawberry.asgi import GraphQL
|
|
39
|
+
from strawberry.fastapi import GraphQLRouter
|
|
41
40
|
from strawberry.schema import BaseSchema
|
|
42
41
|
from typing_extensions import TypeAlias
|
|
43
42
|
|
|
@@ -72,6 +71,7 @@ from phoenix.server.api.dataloaders import (
|
|
|
72
71
|
MinStartOrMaxEndTimeDataLoader,
|
|
73
72
|
ProjectByNameDataLoader,
|
|
74
73
|
RecordCountDataLoader,
|
|
74
|
+
SpanAnnotationsDataLoader,
|
|
75
75
|
SpanDescendantsDataLoader,
|
|
76
76
|
SpanEvaluationsDataLoader,
|
|
77
77
|
SpanProjectsDataLoader,
|
|
@@ -79,11 +79,10 @@ from phoenix.server.api.dataloaders import (
|
|
|
79
79
|
TraceEvaluationsDataLoader,
|
|
80
80
|
TraceRowIdsDataLoader,
|
|
81
81
|
)
|
|
82
|
-
from phoenix.server.api.
|
|
83
|
-
from phoenix.server.api.routers.v1 import
|
|
82
|
+
from phoenix.server.api.routers.v1 import REST_API_VERSION
|
|
83
|
+
from phoenix.server.api.routers.v1 import router as v1_router
|
|
84
84
|
from phoenix.server.api.schema import schema
|
|
85
85
|
from phoenix.server.grpc_server import GrpcServer
|
|
86
|
-
from phoenix.server.openapi.docs import get_swagger_ui_html
|
|
87
86
|
from phoenix.server.telemetry import initialize_opentelemetry_tracer_provider
|
|
88
87
|
from phoenix.trace.schemas import Span
|
|
89
88
|
|
|
@@ -92,6 +91,8 @@ if TYPE_CHECKING:
|
|
|
92
91
|
|
|
93
92
|
logger = logging.getLogger(__name__)
|
|
94
93
|
|
|
94
|
+
router = APIRouter(include_in_schema=False)
|
|
95
|
+
|
|
95
96
|
templates = Jinja2Templates(directory=SERVER_DIR / "templates")
|
|
96
97
|
|
|
97
98
|
|
|
@@ -156,115 +157,20 @@ class HeadersMiddleware(BaseHTTPMiddleware):
|
|
|
156
157
|
ProjectRowId: TypeAlias = int
|
|
157
158
|
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
cache_for_dataloaders: Optional[CacheForDataLoaders] = None,
|
|
170
|
-
read_only: bool = False,
|
|
171
|
-
) -> None:
|
|
172
|
-
self.db = db
|
|
173
|
-
self.model = model
|
|
174
|
-
self.corpus = corpus
|
|
175
|
-
self.export_path = export_path
|
|
176
|
-
self.streaming_last_updated_at = streaming_last_updated_at
|
|
177
|
-
self.cache_for_dataloaders = cache_for_dataloaders
|
|
178
|
-
self.read_only = read_only
|
|
179
|
-
super().__init__(schema, graphiql=graphiql)
|
|
180
|
-
|
|
181
|
-
async def get_context(
|
|
182
|
-
self,
|
|
183
|
-
request: Union[Request, WebSocket],
|
|
184
|
-
response: Optional[Response] = None,
|
|
185
|
-
) -> Context:
|
|
186
|
-
return Context(
|
|
187
|
-
request=request,
|
|
188
|
-
response=response,
|
|
189
|
-
db=self.db,
|
|
190
|
-
model=self.model,
|
|
191
|
-
corpus=self.corpus,
|
|
192
|
-
export_path=self.export_path,
|
|
193
|
-
streaming_last_updated_at=self.streaming_last_updated_at,
|
|
194
|
-
data_loaders=DataLoaders(
|
|
195
|
-
average_experiment_run_latency=AverageExperimentRunLatencyDataLoader(self.db),
|
|
196
|
-
dataset_example_revisions=DatasetExampleRevisionsDataLoader(self.db),
|
|
197
|
-
dataset_example_spans=DatasetExampleSpansDataLoader(self.db),
|
|
198
|
-
document_evaluation_summaries=DocumentEvaluationSummaryDataLoader(
|
|
199
|
-
self.db,
|
|
200
|
-
cache_map=self.cache_for_dataloaders.document_evaluation_summary
|
|
201
|
-
if self.cache_for_dataloaders
|
|
202
|
-
else None,
|
|
203
|
-
),
|
|
204
|
-
document_evaluations=DocumentEvaluationsDataLoader(self.db),
|
|
205
|
-
document_retrieval_metrics=DocumentRetrievalMetricsDataLoader(self.db),
|
|
206
|
-
evaluation_summaries=EvaluationSummaryDataLoader(
|
|
207
|
-
self.db,
|
|
208
|
-
cache_map=self.cache_for_dataloaders.evaluation_summary
|
|
209
|
-
if self.cache_for_dataloaders
|
|
210
|
-
else None,
|
|
211
|
-
),
|
|
212
|
-
experiment_annotation_summaries=ExperimentAnnotationSummaryDataLoader(self.db),
|
|
213
|
-
experiment_error_rates=ExperimentErrorRatesDataLoader(self.db),
|
|
214
|
-
experiment_run_counts=ExperimentRunCountsDataLoader(self.db),
|
|
215
|
-
experiment_sequence_number=ExperimentSequenceNumberDataLoader(self.db),
|
|
216
|
-
latency_ms_quantile=LatencyMsQuantileDataLoader(
|
|
217
|
-
self.db,
|
|
218
|
-
cache_map=self.cache_for_dataloaders.latency_ms_quantile
|
|
219
|
-
if self.cache_for_dataloaders
|
|
220
|
-
else None,
|
|
221
|
-
),
|
|
222
|
-
min_start_or_max_end_times=MinStartOrMaxEndTimeDataLoader(
|
|
223
|
-
self.db,
|
|
224
|
-
cache_map=self.cache_for_dataloaders.min_start_or_max_end_time
|
|
225
|
-
if self.cache_for_dataloaders
|
|
226
|
-
else None,
|
|
227
|
-
),
|
|
228
|
-
record_counts=RecordCountDataLoader(
|
|
229
|
-
self.db,
|
|
230
|
-
cache_map=self.cache_for_dataloaders.record_count
|
|
231
|
-
if self.cache_for_dataloaders
|
|
232
|
-
else None,
|
|
233
|
-
),
|
|
234
|
-
span_descendants=SpanDescendantsDataLoader(self.db),
|
|
235
|
-
span_evaluations=SpanEvaluationsDataLoader(self.db),
|
|
236
|
-
span_projects=SpanProjectsDataLoader(self.db),
|
|
237
|
-
token_counts=TokenCountDataLoader(
|
|
238
|
-
self.db,
|
|
239
|
-
cache_map=self.cache_for_dataloaders.token_count
|
|
240
|
-
if self.cache_for_dataloaders
|
|
241
|
-
else None,
|
|
242
|
-
),
|
|
243
|
-
trace_evaluations=TraceEvaluationsDataLoader(self.db),
|
|
244
|
-
trace_row_ids=TraceRowIdsDataLoader(self.db),
|
|
245
|
-
project_by_name=ProjectByNameDataLoader(self.db),
|
|
246
|
-
),
|
|
247
|
-
cache_for_dataloaders=self.cache_for_dataloaders,
|
|
248
|
-
read_only=self.read_only,
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
class Download(HTTPEndpoint):
|
|
253
|
-
path: Path
|
|
254
|
-
|
|
255
|
-
async def get(self, request: Request) -> FileResponse:
|
|
256
|
-
params = QueryParams(request.query_params)
|
|
257
|
-
file = self.path / (params.get("filename", "") + ".parquet")
|
|
258
|
-
if not file.is_file():
|
|
259
|
-
raise HTTPException(status_code=404)
|
|
260
|
-
return FileResponse(
|
|
261
|
-
path=file,
|
|
262
|
-
filename=file.name,
|
|
263
|
-
media_type="application/x-octet-stream",
|
|
264
|
-
)
|
|
160
|
+
@router.get("/exports")
|
|
161
|
+
async def download_exported_file(request: Request, filename: str) -> FileResponse:
|
|
162
|
+
file = request.app.state.export_path / (filename + ".parquet")
|
|
163
|
+
if not file.is_file():
|
|
164
|
+
raise HTTPException(status_code=404)
|
|
165
|
+
return FileResponse(
|
|
166
|
+
path=file,
|
|
167
|
+
filename=file.name,
|
|
168
|
+
media_type="application/x-octet-stream",
|
|
169
|
+
)
|
|
265
170
|
|
|
266
171
|
|
|
267
|
-
|
|
172
|
+
@router.get("/arize_phoenix_version")
|
|
173
|
+
async def version() -> PlainTextResponse:
|
|
268
174
|
return PlainTextResponse(f"{phoenix.__version__}")
|
|
269
175
|
|
|
270
176
|
|
|
@@ -286,9 +192,9 @@ def _lifespan(
|
|
|
286
192
|
enable_prometheus: bool = False,
|
|
287
193
|
clean_ups: Iterable[Callable[[], None]] = (),
|
|
288
194
|
read_only: bool = False,
|
|
289
|
-
) -> StatefulLifespan[
|
|
195
|
+
) -> StatefulLifespan[FastAPI]:
|
|
290
196
|
@contextlib.asynccontextmanager
|
|
291
|
-
async def lifespan(_:
|
|
197
|
+
async def lifespan(_: FastAPI) -> AsyncIterator[Dict[str, Any]]:
|
|
292
198
|
async with bulk_inserter as (
|
|
293
199
|
queue_span,
|
|
294
200
|
queue_evaluation,
|
|
@@ -310,16 +216,89 @@ def _lifespan(
|
|
|
310
216
|
return lifespan
|
|
311
217
|
|
|
312
218
|
|
|
219
|
+
@router.get("/healthz")
|
|
313
220
|
async def check_healthz(_: Request) -> PlainTextResponse:
|
|
314
221
|
return PlainTextResponse("OK")
|
|
315
222
|
|
|
316
223
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
224
|
+
def create_graphql_router(
|
|
225
|
+
*,
|
|
226
|
+
schema: BaseSchema,
|
|
227
|
+
db: Callable[[], AsyncContextManager[AsyncSession]],
|
|
228
|
+
model: Model,
|
|
229
|
+
export_path: Path,
|
|
230
|
+
corpus: Optional[Model] = None,
|
|
231
|
+
streaming_last_updated_at: Callable[[ProjectRowId], Optional[datetime]] = lambda _: None,
|
|
232
|
+
cache_for_dataloaders: Optional[CacheForDataLoaders] = None,
|
|
233
|
+
read_only: bool = False,
|
|
234
|
+
) -> GraphQLRouter: # type: ignore[type-arg]
|
|
235
|
+
context = Context(
|
|
236
|
+
db=db,
|
|
237
|
+
model=model,
|
|
238
|
+
corpus=corpus,
|
|
239
|
+
export_path=export_path,
|
|
240
|
+
streaming_last_updated_at=streaming_last_updated_at,
|
|
241
|
+
data_loaders=DataLoaders(
|
|
242
|
+
average_experiment_run_latency=AverageExperimentRunLatencyDataLoader(db),
|
|
243
|
+
dataset_example_revisions=DatasetExampleRevisionsDataLoader(db),
|
|
244
|
+
dataset_example_spans=DatasetExampleSpansDataLoader(db),
|
|
245
|
+
document_evaluation_summaries=DocumentEvaluationSummaryDataLoader(
|
|
246
|
+
db,
|
|
247
|
+
cache_map=cache_for_dataloaders.document_evaluation_summary
|
|
248
|
+
if cache_for_dataloaders
|
|
249
|
+
else None,
|
|
250
|
+
),
|
|
251
|
+
document_evaluations=DocumentEvaluationsDataLoader(db),
|
|
252
|
+
document_retrieval_metrics=DocumentRetrievalMetricsDataLoader(db),
|
|
253
|
+
evaluation_summaries=EvaluationSummaryDataLoader(
|
|
254
|
+
db,
|
|
255
|
+
cache_map=cache_for_dataloaders.evaluation_summary
|
|
256
|
+
if cache_for_dataloaders
|
|
257
|
+
else None,
|
|
258
|
+
),
|
|
259
|
+
experiment_annotation_summaries=ExperimentAnnotationSummaryDataLoader(db),
|
|
260
|
+
experiment_error_rates=ExperimentErrorRatesDataLoader(db),
|
|
261
|
+
experiment_run_counts=ExperimentRunCountsDataLoader(db),
|
|
262
|
+
experiment_sequence_number=ExperimentSequenceNumberDataLoader(db),
|
|
263
|
+
latency_ms_quantile=LatencyMsQuantileDataLoader(
|
|
264
|
+
db,
|
|
265
|
+
cache_map=cache_for_dataloaders.latency_ms_quantile
|
|
266
|
+
if cache_for_dataloaders
|
|
267
|
+
else None,
|
|
268
|
+
),
|
|
269
|
+
min_start_or_max_end_times=MinStartOrMaxEndTimeDataLoader(
|
|
270
|
+
db,
|
|
271
|
+
cache_map=cache_for_dataloaders.min_start_or_max_end_time
|
|
272
|
+
if cache_for_dataloaders
|
|
273
|
+
else None,
|
|
274
|
+
),
|
|
275
|
+
record_counts=RecordCountDataLoader(
|
|
276
|
+
db,
|
|
277
|
+
cache_map=cache_for_dataloaders.record_count if cache_for_dataloaders else None,
|
|
278
|
+
),
|
|
279
|
+
span_annotations=SpanAnnotationsDataLoader(db),
|
|
280
|
+
span_descendants=SpanDescendantsDataLoader(db),
|
|
281
|
+
span_evaluations=SpanEvaluationsDataLoader(db),
|
|
282
|
+
span_projects=SpanProjectsDataLoader(db),
|
|
283
|
+
token_counts=TokenCountDataLoader(
|
|
284
|
+
db,
|
|
285
|
+
cache_map=cache_for_dataloaders.token_count if cache_for_dataloaders else None,
|
|
286
|
+
),
|
|
287
|
+
trace_evaluations=TraceEvaluationsDataLoader(db),
|
|
288
|
+
trace_row_ids=TraceRowIdsDataLoader(db),
|
|
289
|
+
project_by_name=ProjectByNameDataLoader(db),
|
|
290
|
+
),
|
|
291
|
+
cache_for_dataloaders=cache_for_dataloaders,
|
|
292
|
+
read_only=read_only,
|
|
293
|
+
)
|
|
320
294
|
|
|
321
|
-
|
|
322
|
-
|
|
295
|
+
return GraphQLRouter(
|
|
296
|
+
schema,
|
|
297
|
+
graphiql=True,
|
|
298
|
+
context_getter=lambda: context,
|
|
299
|
+
include_in_schema=False,
|
|
300
|
+
prefix="/graphql",
|
|
301
|
+
)
|
|
323
302
|
|
|
324
303
|
|
|
325
304
|
class SessionFactory:
|
|
@@ -370,6 +349,18 @@ def instrument_engine_if_enabled(engine: AsyncEngine) -> List[Callable[[], None]
|
|
|
370
349
|
return instrumentation_cleanups
|
|
371
350
|
|
|
372
351
|
|
|
352
|
+
async def plain_text_http_exception_handler(request: Request, exc: HTTPException) -> Response:
|
|
353
|
+
"""
|
|
354
|
+
Overrides the default handler for HTTPExceptions to return a plain text
|
|
355
|
+
response instead of a JSON response. For the original source code, see
|
|
356
|
+
https://github.com/tiangolo/fastapi/blob/d3cdd3bbd14109f3b268df7ca496e24bb64593aa/fastapi/exception_handlers.py#L11
|
|
357
|
+
"""
|
|
358
|
+
headers = getattr(exc, "headers", None)
|
|
359
|
+
if not is_body_allowed_for_status_code(exc.status_code):
|
|
360
|
+
return Response(status_code=exc.status_code, headers=headers)
|
|
361
|
+
return PlainTextResponse(str(exc.detail), status_code=exc.status_code, headers=headers)
|
|
362
|
+
|
|
363
|
+
|
|
373
364
|
def create_app(
|
|
374
365
|
db: SessionFactory,
|
|
375
366
|
export_path: Path,
|
|
@@ -383,7 +374,7 @@ def create_app(
|
|
|
383
374
|
initial_evaluations: Optional[Iterable[pb.Evaluation]] = None,
|
|
384
375
|
serve_ui: bool = True,
|
|
385
376
|
clean_up_callbacks: List[Callable[[], None]] = [],
|
|
386
|
-
) ->
|
|
377
|
+
) -> FastAPI:
|
|
387
378
|
clean_ups: List[Callable[[], None]] = clean_up_callbacks # To be called at app shutdown.
|
|
388
379
|
initial_batch_of_spans: Iterable[Tuple[Span, str]] = (
|
|
389
380
|
()
|
|
@@ -425,7 +416,7 @@ def create_app(
|
|
|
425
416
|
|
|
426
417
|
strawberry_extensions.append(_OpenTelemetryExtension)
|
|
427
418
|
|
|
428
|
-
|
|
419
|
+
graphql_router = create_graphql_router(
|
|
429
420
|
db=db,
|
|
430
421
|
schema=strawberry.Schema(
|
|
431
422
|
query=schema.query,
|
|
@@ -436,7 +427,6 @@ def create_app(
|
|
|
436
427
|
model=model,
|
|
437
428
|
corpus=corpus,
|
|
438
429
|
export_path=export_path,
|
|
439
|
-
graphiql=True,
|
|
440
430
|
streaming_last_updated_at=bulk_inserter.last_updated_at,
|
|
441
431
|
cache_for_dataloaders=cache_for_dataloaders,
|
|
442
432
|
read_only=read_only,
|
|
@@ -447,7 +437,9 @@ def create_app(
|
|
|
447
437
|
prometheus_middlewares = [Middleware(PrometheusMiddleware)]
|
|
448
438
|
else:
|
|
449
439
|
prometheus_middlewares = []
|
|
450
|
-
app =
|
|
440
|
+
app = FastAPI(
|
|
441
|
+
title="Arize-Phoenix REST API",
|
|
442
|
+
version=REST_API_VERSION,
|
|
451
443
|
lifespan=_lifespan(
|
|
452
444
|
read_only=read_only,
|
|
453
445
|
bulk_inserter=bulk_inserter,
|
|
@@ -459,56 +451,39 @@ def create_app(
|
|
|
459
451
|
Middleware(HeadersMiddleware),
|
|
460
452
|
*prometheus_middlewares,
|
|
461
453
|
],
|
|
454
|
+
exception_handlers={HTTPException: plain_text_http_exception_handler},
|
|
462
455
|
debug=debug,
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
Route("/arize_phoenix_version", version),
|
|
467
|
-
Route("/healthz", check_healthz),
|
|
468
|
-
Route(
|
|
469
|
-
"/exports",
|
|
470
|
-
type(
|
|
471
|
-
"DownloadExports",
|
|
472
|
-
(Download,),
|
|
473
|
-
{"path": export_path},
|
|
474
|
-
),
|
|
475
|
-
),
|
|
476
|
-
Route(
|
|
477
|
-
"/docs",
|
|
478
|
-
api_docs,
|
|
479
|
-
),
|
|
480
|
-
Route(
|
|
481
|
-
"/graphql",
|
|
482
|
-
graphql,
|
|
483
|
-
),
|
|
484
|
-
]
|
|
485
|
-
+ (
|
|
486
|
-
[
|
|
487
|
-
Mount(
|
|
488
|
-
"/",
|
|
489
|
-
app=Static(
|
|
490
|
-
directory=SERVER_DIR / "static",
|
|
491
|
-
app_config=AppConfig(
|
|
492
|
-
has_inferences=model.is_empty is not True,
|
|
493
|
-
has_corpus=corpus is not None,
|
|
494
|
-
min_dist=umap_params.min_dist,
|
|
495
|
-
n_neighbors=umap_params.n_neighbors,
|
|
496
|
-
n_samples=umap_params.n_samples,
|
|
497
|
-
),
|
|
498
|
-
),
|
|
499
|
-
name="static",
|
|
500
|
-
),
|
|
501
|
-
]
|
|
502
|
-
if serve_ui
|
|
503
|
-
else []
|
|
504
|
-
),
|
|
456
|
+
swagger_ui_parameters={
|
|
457
|
+
"defaultModelsExpandDepth": -1, # hides the schema section in the Swagger UI
|
|
458
|
+
},
|
|
505
459
|
)
|
|
506
460
|
app.state.read_only = read_only
|
|
461
|
+
app.state.export_path = export_path
|
|
462
|
+
app.include_router(v1_router)
|
|
463
|
+
app.include_router(router)
|
|
464
|
+
app.include_router(graphql_router)
|
|
465
|
+
app.add_middleware(GZipMiddleware)
|
|
466
|
+
if serve_ui:
|
|
467
|
+
app.mount(
|
|
468
|
+
"/",
|
|
469
|
+
app=Static(
|
|
470
|
+
directory=SERVER_DIR / "static",
|
|
471
|
+
app_config=AppConfig(
|
|
472
|
+
has_inferences=model.is_empty is not True,
|
|
473
|
+
has_corpus=corpus is not None,
|
|
474
|
+
min_dist=umap_params.min_dist,
|
|
475
|
+
n_neighbors=umap_params.n_neighbors,
|
|
476
|
+
n_samples=umap_params.n_samples,
|
|
477
|
+
),
|
|
478
|
+
),
|
|
479
|
+
name="static",
|
|
480
|
+
)
|
|
481
|
+
|
|
507
482
|
app.state.db = db
|
|
508
483
|
if tracer_provider:
|
|
509
|
-
from opentelemetry.instrumentation.
|
|
484
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
510
485
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
clean_ups.append(
|
|
486
|
+
FastAPIInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
487
|
+
FastAPIInstrumentor.instrument_app(app, tracer_provider=tracer_provider)
|
|
488
|
+
clean_ups.append(FastAPIInstrumentor().uninstrument)
|
|
514
489
|
return app
|
phoenix/server/thread_server.py
CHANGED
|
@@ -4,7 +4,7 @@ from threading import Thread
|
|
|
4
4
|
from time import sleep, time
|
|
5
5
|
from typing import Generator
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from fastapi import FastAPI
|
|
8
8
|
from uvicorn import Config, Server
|
|
9
9
|
from uvicorn.config import LoopSetupType
|
|
10
10
|
|
|
@@ -24,7 +24,7 @@ class ThreadServer(Server):
|
|
|
24
24
|
|
|
25
25
|
def __init__(
|
|
26
26
|
self,
|
|
27
|
-
app:
|
|
27
|
+
app: FastAPI,
|
|
28
28
|
host: str,
|
|
29
29
|
port: int,
|
|
30
30
|
root_path: str,
|
phoenix/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "4.10.
|
|
1
|
+
__version__ = "4.10.2rc1"
|
phoenix/server/openapi/docs.py
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|