arize-phoenix 12.3.0__py3-none-any.whl → 12.5.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-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/METADATA +2 -1
- {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/RECORD +73 -72
- phoenix/auth.py +27 -2
- phoenix/config.py +302 -53
- phoenix/db/README.md +546 -28
- phoenix/db/models.py +3 -3
- phoenix/server/api/auth.py +9 -0
- phoenix/server/api/context.py +2 -0
- phoenix/server/api/dataloaders/__init__.py +2 -0
- phoenix/server/api/dataloaders/dataset_dataset_splits.py +52 -0
- phoenix/server/api/input_types/ProjectSessionSort.py +158 -1
- phoenix/server/api/input_types/SpanSort.py +2 -1
- phoenix/server/api/input_types/UserRoleInput.py +1 -0
- phoenix/server/api/mutations/annotation_config_mutations.py +6 -6
- phoenix/server/api/mutations/api_key_mutations.py +13 -5
- phoenix/server/api/mutations/chat_mutations.py +3 -3
- phoenix/server/api/mutations/dataset_label_mutations.py +6 -6
- phoenix/server/api/mutations/dataset_mutations.py +8 -8
- phoenix/server/api/mutations/dataset_split_mutations.py +7 -7
- phoenix/server/api/mutations/experiment_mutations.py +2 -2
- phoenix/server/api/mutations/export_events_mutations.py +3 -3
- phoenix/server/api/mutations/model_mutations.py +4 -4
- phoenix/server/api/mutations/project_mutations.py +4 -4
- phoenix/server/api/mutations/project_session_annotations_mutations.py +4 -4
- phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +8 -4
- phoenix/server/api/mutations/prompt_label_mutations.py +7 -7
- phoenix/server/api/mutations/prompt_mutations.py +7 -7
- phoenix/server/api/mutations/prompt_version_tag_mutations.py +3 -3
- phoenix/server/api/mutations/span_annotations_mutations.py +5 -5
- phoenix/server/api/mutations/trace_annotations_mutations.py +4 -4
- phoenix/server/api/mutations/trace_mutations.py +3 -3
- phoenix/server/api/mutations/user_mutations.py +8 -5
- phoenix/server/api/routers/auth.py +23 -32
- phoenix/server/api/routers/oauth2.py +213 -24
- phoenix/server/api/routers/v1/__init__.py +18 -4
- phoenix/server/api/routers/v1/annotation_configs.py +19 -30
- phoenix/server/api/routers/v1/annotations.py +21 -22
- phoenix/server/api/routers/v1/datasets.py +86 -64
- phoenix/server/api/routers/v1/documents.py +2 -3
- phoenix/server/api/routers/v1/evaluations.py +12 -24
- phoenix/server/api/routers/v1/experiment_evaluations.py +2 -3
- phoenix/server/api/routers/v1/experiment_runs.py +16 -11
- phoenix/server/api/routers/v1/experiments.py +57 -22
- phoenix/server/api/routers/v1/projects.py +16 -50
- phoenix/server/api/routers/v1/prompts.py +30 -31
- phoenix/server/api/routers/v1/sessions.py +2 -5
- phoenix/server/api/routers/v1/spans.py +35 -26
- phoenix/server/api/routers/v1/traces.py +11 -19
- phoenix/server/api/routers/v1/users.py +13 -29
- phoenix/server/api/routers/v1/utils.py +3 -7
- phoenix/server/api/subscriptions.py +3 -3
- phoenix/server/api/types/Dataset.py +95 -6
- phoenix/server/api/types/Project.py +24 -68
- phoenix/server/app.py +3 -2
- phoenix/server/authorization.py +5 -4
- phoenix/server/bearer_auth.py +13 -5
- phoenix/server/jwt_store.py +8 -6
- phoenix/server/oauth2.py +172 -5
- phoenix/server/static/.vite/manifest.json +39 -39
- phoenix/server/static/assets/{components-Bs8eJEpU.js → components-cwdYEs7B.js} +501 -404
- phoenix/server/static/assets/{index-C6WEu5UP.js → index-Dc0vD1Rn.js} +1 -1
- phoenix/server/static/assets/{pages-D-n2pkoG.js → pages-BDkB3a_a.js} +577 -533
- phoenix/server/static/assets/{vendor-D2eEI-6h.js → vendor-Ce6GTAin.js} +1 -1
- phoenix/server/static/assets/{vendor-arizeai-kfOei7nf.js → vendor-arizeai-CSF-1Kc5.js} +1 -1
- phoenix/server/static/assets/{vendor-codemirror-1bq_t1Ec.js → vendor-codemirror-Bv8J_7an.js} +3 -3
- phoenix/server/static/assets/{vendor-recharts-DQ4xfrf4.js → vendor-recharts-DcLgzI7g.js} +1 -1
- phoenix/server/static/assets/{vendor-shiki-GGmcIQxA.js → vendor-shiki-BF8rh_7m.js} +1 -1
- phoenix/trace/attributes.py +80 -13
- phoenix/version.py +1 -1
- {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/WHEEL +0 -0
- {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,7 +8,6 @@ from fastapi import APIRouter, HTTPException, Path, Query
|
|
|
8
8
|
from pydantic import Field
|
|
9
9
|
from sqlalchemy import exists, select
|
|
10
10
|
from starlette.requests import Request
|
|
11
|
-
from starlette.status import HTTP_200_OK, HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY
|
|
12
11
|
from strawberry.relay import GlobalID
|
|
13
12
|
|
|
14
13
|
from phoenix.db import models
|
|
@@ -198,11 +197,11 @@ class SessionAnnotationsResponseBody(PaginatedResponseBody[SessionAnnotation]):
|
|
|
198
197
|
"/projects/{project_identifier}/span_annotations",
|
|
199
198
|
operation_id="listSpanAnnotationsBySpanIds",
|
|
200
199
|
summary="Get span annotations for a list of span_ids.",
|
|
201
|
-
status_code=
|
|
200
|
+
status_code=200,
|
|
202
201
|
responses=add_errors_to_responses(
|
|
203
202
|
[
|
|
204
|
-
{"status_code":
|
|
205
|
-
{"status_code":
|
|
203
|
+
{"status_code": 404, "description": "Project or spans not found"},
|
|
204
|
+
{"status_code": 422, "description": "Invalid parameters"},
|
|
206
205
|
]
|
|
207
206
|
),
|
|
208
207
|
)
|
|
@@ -240,7 +239,7 @@ async def list_span_annotations(
|
|
|
240
239
|
span_ids = list({*span_ids})
|
|
241
240
|
if len(span_ids) > MAX_SPAN_IDS:
|
|
242
241
|
raise HTTPException(
|
|
243
|
-
status_code=
|
|
242
|
+
status_code=422,
|
|
244
243
|
detail=f"Too many span_ids supplied: {len(span_ids)} (max {MAX_SPAN_IDS})",
|
|
245
244
|
)
|
|
246
245
|
|
|
@@ -248,7 +247,7 @@ async def list_span_annotations(
|
|
|
248
247
|
project = await _get_project_by_identifier(session, project_identifier)
|
|
249
248
|
if not project:
|
|
250
249
|
raise HTTPException(
|
|
251
|
-
status_code=
|
|
250
|
+
status_code=404,
|
|
252
251
|
detail=f"Project with identifier {project_identifier} not found",
|
|
253
252
|
)
|
|
254
253
|
|
|
@@ -280,7 +279,7 @@ async def list_span_annotations(
|
|
|
280
279
|
cursor_id = int(GlobalID.from_id(cursor).node_id)
|
|
281
280
|
except ValueError:
|
|
282
281
|
raise HTTPException(
|
|
283
|
-
status_code=
|
|
282
|
+
status_code=422,
|
|
284
283
|
detail="Invalid cursor value",
|
|
285
284
|
)
|
|
286
285
|
stmt = stmt.where(models.SpanAnnotation.id <= cursor_id)
|
|
@@ -310,7 +309,7 @@ async def list_span_annotations(
|
|
|
310
309
|
if not spans_exist:
|
|
311
310
|
raise HTTPException(
|
|
312
311
|
detail="None of the supplied span_ids exist in this project",
|
|
313
|
-
status_code=
|
|
312
|
+
status_code=404,
|
|
314
313
|
)
|
|
315
314
|
|
|
316
315
|
return SpanAnnotationsResponseBody(data=[], next_cursor=None)
|
|
@@ -343,11 +342,11 @@ async def list_span_annotations(
|
|
|
343
342
|
"/projects/{project_identifier}/trace_annotations",
|
|
344
343
|
operation_id="listTraceAnnotationsByTraceIds",
|
|
345
344
|
summary="Get trace annotations for a list of trace_ids.",
|
|
346
|
-
status_code=
|
|
345
|
+
status_code=200,
|
|
347
346
|
responses=add_errors_to_responses(
|
|
348
347
|
[
|
|
349
|
-
{"status_code":
|
|
350
|
-
{"status_code":
|
|
348
|
+
{"status_code": 404, "description": "Project or traces not found"},
|
|
349
|
+
{"status_code": 422, "description": "Invalid parameters"},
|
|
351
350
|
]
|
|
352
351
|
),
|
|
353
352
|
)
|
|
@@ -385,7 +384,7 @@ async def list_trace_annotations(
|
|
|
385
384
|
trace_ids = list({*trace_ids})
|
|
386
385
|
if len(trace_ids) > MAX_TRACE_IDS:
|
|
387
386
|
raise HTTPException(
|
|
388
|
-
status_code=
|
|
387
|
+
status_code=422,
|
|
389
388
|
detail=f"Too many trace_ids supplied: {len(trace_ids)} (max {MAX_TRACE_IDS})",
|
|
390
389
|
)
|
|
391
390
|
|
|
@@ -393,7 +392,7 @@ async def list_trace_annotations(
|
|
|
393
392
|
project = await _get_project_by_identifier(session, project_identifier)
|
|
394
393
|
if not project:
|
|
395
394
|
raise HTTPException(
|
|
396
|
-
status_code=
|
|
395
|
+
status_code=404,
|
|
397
396
|
detail=f"Project with identifier {project_identifier} not found",
|
|
398
397
|
)
|
|
399
398
|
|
|
@@ -424,7 +423,7 @@ async def list_trace_annotations(
|
|
|
424
423
|
cursor_id = int(GlobalID.from_id(cursor).node_id)
|
|
425
424
|
except ValueError:
|
|
426
425
|
raise HTTPException(
|
|
427
|
-
status_code=
|
|
426
|
+
status_code=422,
|
|
428
427
|
detail="Invalid cursor value",
|
|
429
428
|
)
|
|
430
429
|
stmt = stmt.where(models.TraceAnnotation.id <= cursor_id)
|
|
@@ -450,7 +449,7 @@ async def list_trace_annotations(
|
|
|
450
449
|
if not traces_exist:
|
|
451
450
|
raise HTTPException(
|
|
452
451
|
detail="None of the supplied trace_ids exist in this project",
|
|
453
|
-
status_code=
|
|
452
|
+
status_code=404,
|
|
454
453
|
)
|
|
455
454
|
|
|
456
455
|
return TraceAnnotationsResponseBody(data=[], next_cursor=None)
|
|
@@ -483,11 +482,11 @@ async def list_trace_annotations(
|
|
|
483
482
|
"/projects/{project_identifier}/session_annotations",
|
|
484
483
|
operation_id="listSessionAnnotationsBySessionIds",
|
|
485
484
|
summary="Get session annotations for a list of session_ids.",
|
|
486
|
-
status_code=
|
|
485
|
+
status_code=200,
|
|
487
486
|
responses=add_errors_to_responses(
|
|
488
487
|
[
|
|
489
|
-
{"status_code":
|
|
490
|
-
{"status_code":
|
|
488
|
+
{"status_code": 404, "description": "Project or sessions not found"},
|
|
489
|
+
{"status_code": 422, "description": "Invalid parameters"},
|
|
491
490
|
]
|
|
492
491
|
),
|
|
493
492
|
)
|
|
@@ -525,7 +524,7 @@ async def list_session_annotations(
|
|
|
525
524
|
session_ids = list({*session_ids})
|
|
526
525
|
if len(session_ids) > MAX_SESSION_IDS:
|
|
527
526
|
raise HTTPException(
|
|
528
|
-
status_code=
|
|
527
|
+
status_code=422,
|
|
529
528
|
detail=f"Too many session_ids supplied: {len(session_ids)} (max {MAX_SESSION_IDS})",
|
|
530
529
|
)
|
|
531
530
|
|
|
@@ -533,7 +532,7 @@ async def list_session_annotations(
|
|
|
533
532
|
project = await _get_project_by_identifier(session, project_identifier)
|
|
534
533
|
if not project:
|
|
535
534
|
raise HTTPException(
|
|
536
|
-
status_code=
|
|
535
|
+
status_code=404,
|
|
537
536
|
detail=f"Project with identifier {project_identifier} not found",
|
|
538
537
|
)
|
|
539
538
|
|
|
@@ -571,7 +570,7 @@ async def list_session_annotations(
|
|
|
571
570
|
cursor_id = int(GlobalID.from_id(cursor).node_id)
|
|
572
571
|
except ValueError:
|
|
573
572
|
raise HTTPException(
|
|
574
|
-
status_code=
|
|
573
|
+
status_code=422,
|
|
575
574
|
detail="Invalid cursor value",
|
|
576
575
|
)
|
|
577
576
|
stmt = stmt.where(models.ProjectSessionAnnotation.id <= cursor_id)
|
|
@@ -597,7 +596,7 @@ async def list_session_annotations(
|
|
|
597
596
|
if not sessions_exist:
|
|
598
597
|
raise HTTPException(
|
|
599
598
|
detail="None of the supplied session_ids exist in this project",
|
|
600
|
-
status_code=
|
|
599
|
+
status_code=404,
|
|
601
600
|
)
|
|
602
601
|
|
|
603
602
|
return SessionAnnotationsResponseBody(data=[], next_cursor=None)
|
|
@@ -23,14 +23,6 @@ from starlette.concurrency import run_in_threadpool
|
|
|
23
23
|
from starlette.datastructures import FormData, UploadFile
|
|
24
24
|
from starlette.requests import Request
|
|
25
25
|
from starlette.responses import Response
|
|
26
|
-
from starlette.status import (
|
|
27
|
-
HTTP_200_OK,
|
|
28
|
-
HTTP_204_NO_CONTENT,
|
|
29
|
-
HTTP_404_NOT_FOUND,
|
|
30
|
-
HTTP_409_CONFLICT,
|
|
31
|
-
HTTP_422_UNPROCESSABLE_ENTITY,
|
|
32
|
-
HTTP_429_TOO_MANY_REQUESTS,
|
|
33
|
-
)
|
|
34
26
|
from strawberry.relay import GlobalID
|
|
35
27
|
from typing_extensions import TypeAlias, assert_never
|
|
36
28
|
|
|
@@ -91,7 +83,7 @@ class ListDatasetsResponseBody(PaginatedResponseBody[Dataset]):
|
|
|
91
83
|
"/datasets",
|
|
92
84
|
operation_id="listDatasets",
|
|
93
85
|
summary="List datasets",
|
|
94
|
-
responses=add_errors_to_responses([
|
|
86
|
+
responses=add_errors_to_responses([422]),
|
|
95
87
|
)
|
|
96
88
|
async def list_datasets(
|
|
97
89
|
request: Request,
|
|
@@ -125,7 +117,7 @@ async def list_datasets(
|
|
|
125
117
|
except ValueError:
|
|
126
118
|
raise HTTPException(
|
|
127
119
|
detail=f"Invalid cursor format: {cursor}",
|
|
128
|
-
status_code=
|
|
120
|
+
status_code=422,
|
|
129
121
|
)
|
|
130
122
|
if name:
|
|
131
123
|
query = query.filter(models.Dataset.name == name)
|
|
@@ -164,11 +156,11 @@ async def list_datasets(
|
|
|
164
156
|
"/datasets/{id}",
|
|
165
157
|
operation_id="deleteDatasetById",
|
|
166
158
|
summary="Delete dataset by ID",
|
|
167
|
-
status_code=
|
|
159
|
+
status_code=204,
|
|
168
160
|
responses=add_errors_to_responses(
|
|
169
161
|
[
|
|
170
|
-
{"status_code":
|
|
171
|
-
{"status_code":
|
|
162
|
+
{"status_code": 404, "description": "Dataset not found"},
|
|
163
|
+
{"status_code": 422, "description": "Invalid dataset ID"},
|
|
172
164
|
]
|
|
173
165
|
),
|
|
174
166
|
)
|
|
@@ -182,11 +174,9 @@ async def delete_dataset(
|
|
|
182
174
|
DATASET_NODE_NAME,
|
|
183
175
|
)
|
|
184
176
|
except ValueError:
|
|
185
|
-
raise HTTPException(
|
|
186
|
-
detail=f"Invalid Dataset ID: {id}", status_code=HTTP_422_UNPROCESSABLE_ENTITY
|
|
187
|
-
)
|
|
177
|
+
raise HTTPException(detail=f"Invalid Dataset ID: {id}", status_code=422)
|
|
188
178
|
else:
|
|
189
|
-
raise HTTPException(detail="Missing Dataset ID", status_code=
|
|
179
|
+
raise HTTPException(detail="Missing Dataset ID", status_code=422)
|
|
190
180
|
project_names_stmt = get_project_names_for_datasets(dataset_id)
|
|
191
181
|
eval_trace_ids_stmt = get_eval_trace_ids_for_datasets(dataset_id)
|
|
192
182
|
stmt = (
|
|
@@ -196,7 +186,7 @@ async def delete_dataset(
|
|
|
196
186
|
project_names = await session.scalars(project_names_stmt)
|
|
197
187
|
eval_trace_ids = await session.scalars(eval_trace_ids_stmt)
|
|
198
188
|
if (await session.scalar(stmt)) is None:
|
|
199
|
-
raise HTTPException(detail="Dataset does not exist", status_code=
|
|
189
|
+
raise HTTPException(detail="Dataset does not exist", status_code=404)
|
|
200
190
|
tasks = BackgroundTasks()
|
|
201
191
|
tasks.add_task(delete_projects, request.app.state.db, *project_names)
|
|
202
192
|
tasks.add_task(delete_traces, request.app.state.db, *eval_trace_ids)
|
|
@@ -214,17 +204,21 @@ class GetDatasetResponseBody(ResponseBody[DatasetWithExampleCount]):
|
|
|
214
204
|
"/datasets/{id}",
|
|
215
205
|
operation_id="getDataset",
|
|
216
206
|
summary="Get dataset by ID",
|
|
217
|
-
responses=add_errors_to_responses([
|
|
207
|
+
responses=add_errors_to_responses([404]),
|
|
218
208
|
)
|
|
219
209
|
async def get_dataset(
|
|
220
210
|
request: Request, id: str = Path(description="The ID of the dataset")
|
|
221
211
|
) -> GetDatasetResponseBody:
|
|
222
|
-
|
|
212
|
+
try:
|
|
213
|
+
dataset_id = GlobalID.from_id(id)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
raise HTTPException(
|
|
216
|
+
detail=f"Invalid dataset ID format: {id}",
|
|
217
|
+
status_code=422,
|
|
218
|
+
) from e
|
|
223
219
|
|
|
224
220
|
if (type_name := dataset_id.type_name) != DATASET_NODE_NAME:
|
|
225
|
-
raise HTTPException(
|
|
226
|
-
detail=f"ID {dataset_id} refers to a f{type_name}", status_code=HTTP_404_NOT_FOUND
|
|
227
|
-
)
|
|
221
|
+
raise HTTPException(detail=f"ID {dataset_id} refers to a f{type_name}", status_code=404)
|
|
228
222
|
async with request.app.state.db() as session:
|
|
229
223
|
result = await session.execute(
|
|
230
224
|
select(models.Dataset, models.Dataset.example_count).filter(
|
|
@@ -235,9 +229,7 @@ async def get_dataset(
|
|
|
235
229
|
dataset = dataset_query[0] if dataset_query else None
|
|
236
230
|
example_count = dataset_query[1] if dataset_query else 0
|
|
237
231
|
if dataset is None:
|
|
238
|
-
raise HTTPException(
|
|
239
|
-
detail=f"Dataset with ID {dataset_id} not found", status_code=HTTP_404_NOT_FOUND
|
|
240
|
-
)
|
|
232
|
+
raise HTTPException(detail=f"Dataset with ID {dataset_id} not found", status_code=404)
|
|
241
233
|
|
|
242
234
|
dataset = DatasetWithExampleCount(
|
|
243
235
|
id=str(dataset_id),
|
|
@@ -266,7 +258,7 @@ class ListDatasetVersionsResponseBody(PaginatedResponseBody[DatasetVersion]):
|
|
|
266
258
|
"/datasets/{id}/versions",
|
|
267
259
|
operation_id="listDatasetVersionsByDatasetId",
|
|
268
260
|
summary="List dataset versions",
|
|
269
|
-
responses=add_errors_to_responses([
|
|
261
|
+
responses=add_errors_to_responses([422]),
|
|
270
262
|
)
|
|
271
263
|
async def list_dataset_versions(
|
|
272
264
|
request: Request,
|
|
@@ -288,12 +280,12 @@ async def list_dataset_versions(
|
|
|
288
280
|
except ValueError:
|
|
289
281
|
raise HTTPException(
|
|
290
282
|
detail=f"Invalid Dataset ID: {id}",
|
|
291
|
-
status_code=
|
|
283
|
+
status_code=422,
|
|
292
284
|
)
|
|
293
285
|
else:
|
|
294
286
|
raise HTTPException(
|
|
295
287
|
detail="Missing Dataset ID",
|
|
296
|
-
status_code=
|
|
288
|
+
status_code=422,
|
|
297
289
|
)
|
|
298
290
|
stmt = (
|
|
299
291
|
select(models.DatasetVersion)
|
|
@@ -309,7 +301,7 @@ async def list_dataset_versions(
|
|
|
309
301
|
except ValueError:
|
|
310
302
|
raise HTTPException(
|
|
311
303
|
detail=f"Invalid cursor: {cursor}",
|
|
312
|
-
status_code=
|
|
304
|
+
status_code=422,
|
|
313
305
|
)
|
|
314
306
|
max_dataset_version_id = (
|
|
315
307
|
select(models.DatasetVersion.id)
|
|
@@ -348,10 +340,10 @@ class UploadDatasetResponseBody(ResponseBody[UploadDatasetData]):
|
|
|
348
340
|
responses=add_errors_to_responses(
|
|
349
341
|
[
|
|
350
342
|
{
|
|
351
|
-
"status_code":
|
|
343
|
+
"status_code": 409,
|
|
352
344
|
"description": "Dataset of the same name already exists",
|
|
353
345
|
},
|
|
354
|
-
{"status_code":
|
|
346
|
+
{"status_code": 422, "description": "Invalid request body"},
|
|
355
347
|
]
|
|
356
348
|
),
|
|
357
349
|
# FastAPI cannot generate the request body portion of the OpenAPI schema for
|
|
@@ -414,7 +406,12 @@ async def upload_dataset(
|
|
|
414
406
|
description="If true, fulfill request synchronously and return JSON containing dataset_id.",
|
|
415
407
|
),
|
|
416
408
|
) -> Optional[UploadDatasetResponseBody]:
|
|
417
|
-
request_content_type = request.headers
|
|
409
|
+
request_content_type = request.headers.get("content-type")
|
|
410
|
+
if not request_content_type:
|
|
411
|
+
raise HTTPException(
|
|
412
|
+
detail="Missing content-type header",
|
|
413
|
+
status_code=400,
|
|
414
|
+
)
|
|
418
415
|
examples: Union[Examples, Awaitable[Examples]]
|
|
419
416
|
if request_content_type.startswith("application/json"):
|
|
420
417
|
try:
|
|
@@ -424,14 +421,14 @@ async def upload_dataset(
|
|
|
424
421
|
except ValueError as e:
|
|
425
422
|
raise HTTPException(
|
|
426
423
|
detail=str(e),
|
|
427
|
-
status_code=
|
|
424
|
+
status_code=422,
|
|
428
425
|
)
|
|
429
426
|
if action is DatasetAction.CREATE:
|
|
430
427
|
async with request.app.state.db() as session:
|
|
431
428
|
if await _check_table_exists(session, name):
|
|
432
429
|
raise HTTPException(
|
|
433
430
|
detail=f"Dataset with the same name already exists: {name=}",
|
|
434
|
-
status_code=
|
|
431
|
+
status_code=409,
|
|
435
432
|
)
|
|
436
433
|
elif request_content_type.startswith("multipart/form-data"):
|
|
437
434
|
async with request.form() as form:
|
|
@@ -448,14 +445,14 @@ async def upload_dataset(
|
|
|
448
445
|
except ValueError as e:
|
|
449
446
|
raise HTTPException(
|
|
450
447
|
detail=str(e),
|
|
451
|
-
status_code=
|
|
448
|
+
status_code=422,
|
|
452
449
|
)
|
|
453
450
|
if action is DatasetAction.CREATE:
|
|
454
451
|
async with request.app.state.db() as session:
|
|
455
452
|
if await _check_table_exists(session, name):
|
|
456
453
|
raise HTTPException(
|
|
457
454
|
detail=f"Dataset with the same name already exists: {name=}",
|
|
458
|
-
status_code=
|
|
455
|
+
status_code=409,
|
|
459
456
|
)
|
|
460
457
|
content = await file.read()
|
|
461
458
|
try:
|
|
@@ -472,12 +469,12 @@ async def upload_dataset(
|
|
|
472
469
|
except ValueError as e:
|
|
473
470
|
raise HTTPException(
|
|
474
471
|
detail=str(e),
|
|
475
|
-
status_code=
|
|
472
|
+
status_code=422,
|
|
476
473
|
)
|
|
477
474
|
else:
|
|
478
475
|
raise HTTPException(
|
|
479
476
|
detail="Invalid request Content-Type",
|
|
480
|
-
status_code=
|
|
477
|
+
status_code=422,
|
|
481
478
|
)
|
|
482
479
|
user_id: Optional[int] = None
|
|
483
480
|
if request.app.state.authentication_enabled and isinstance(request.user, PhoenixUser):
|
|
@@ -510,7 +507,7 @@ async def upload_dataset(
|
|
|
510
507
|
except QueueFull:
|
|
511
508
|
if isinstance(examples, Coroutine):
|
|
512
509
|
examples.close()
|
|
513
|
-
raise HTTPException(detail="Too many requests.", status_code=
|
|
510
|
+
raise HTTPException(detail="Too many requests.", status_code=429)
|
|
514
511
|
return None
|
|
515
512
|
|
|
516
513
|
|
|
@@ -711,7 +708,7 @@ class ListDatasetExamplesResponseBody(ResponseBody[ListDatasetExamplesData]):
|
|
|
711
708
|
"/datasets/{id}/examples",
|
|
712
709
|
operation_id="getDatasetExamples",
|
|
713
710
|
summary="Get examples from a dataset",
|
|
714
|
-
responses=add_errors_to_responses([
|
|
711
|
+
responses=add_errors_to_responses([404]),
|
|
715
712
|
)
|
|
716
713
|
async def get_dataset_examples(
|
|
717
714
|
request: Request,
|
|
@@ -723,18 +720,30 @@ async def get_dataset_examples(
|
|
|
723
720
|
),
|
|
724
721
|
),
|
|
725
722
|
) -> ListDatasetExamplesResponseBody:
|
|
726
|
-
|
|
727
|
-
|
|
723
|
+
try:
|
|
724
|
+
dataset_gid = GlobalID.from_id(id)
|
|
725
|
+
except Exception as e:
|
|
726
|
+
raise HTTPException(
|
|
727
|
+
detail=f"Invalid dataset ID format: {id}",
|
|
728
|
+
status_code=422,
|
|
729
|
+
) from e
|
|
730
|
+
|
|
731
|
+
if version_id:
|
|
732
|
+
try:
|
|
733
|
+
version_gid = GlobalID.from_id(version_id)
|
|
734
|
+
except Exception as e:
|
|
735
|
+
raise HTTPException(
|
|
736
|
+
detail=f"Invalid dataset version ID format: {version_id}",
|
|
737
|
+
status_code=422,
|
|
738
|
+
) from e
|
|
739
|
+
else:
|
|
740
|
+
version_gid = None
|
|
728
741
|
|
|
729
742
|
if (dataset_type := dataset_gid.type_name) != "Dataset":
|
|
730
|
-
raise HTTPException(
|
|
731
|
-
detail=f"ID {dataset_gid} refers to a {dataset_type}", status_code=HTTP_404_NOT_FOUND
|
|
732
|
-
)
|
|
743
|
+
raise HTTPException(detail=f"ID {dataset_gid} refers to a {dataset_type}", status_code=404)
|
|
733
744
|
|
|
734
745
|
if version_gid and (version_type := version_gid.type_name) != "DatasetVersion":
|
|
735
|
-
raise HTTPException(
|
|
736
|
-
detail=f"ID {version_gid} refers to a {version_type}", status_code=HTTP_404_NOT_FOUND
|
|
737
|
-
)
|
|
746
|
+
raise HTTPException(detail=f"ID {version_gid} refers to a {version_type}", status_code=404)
|
|
738
747
|
|
|
739
748
|
async with request.app.state.db() as session:
|
|
740
749
|
if (
|
|
@@ -744,7 +753,7 @@ async def get_dataset_examples(
|
|
|
744
753
|
) is None:
|
|
745
754
|
raise HTTPException(
|
|
746
755
|
detail=f"No dataset with id {dataset_gid} can be found.",
|
|
747
|
-
status_code=
|
|
756
|
+
status_code=404,
|
|
748
757
|
)
|
|
749
758
|
|
|
750
759
|
# Subquery to find the maximum created_at for each dataset_example_id
|
|
@@ -766,7 +775,7 @@ async def get_dataset_examples(
|
|
|
766
775
|
) is None:
|
|
767
776
|
raise HTTPException(
|
|
768
777
|
detail=f"No dataset version with id {version_id} can be found.",
|
|
769
|
-
status_code=
|
|
778
|
+
status_code=404,
|
|
770
779
|
)
|
|
771
780
|
# if a version_id is provided, filter the subquery to only include revisions from that
|
|
772
781
|
partial_subquery = partial_subquery.filter(
|
|
@@ -782,7 +791,7 @@ async def get_dataset_examples(
|
|
|
782
791
|
) is None:
|
|
783
792
|
raise HTTPException(
|
|
784
793
|
detail="Dataset has no versions.",
|
|
785
|
-
status_code=
|
|
794
|
+
status_code=404,
|
|
786
795
|
)
|
|
787
796
|
|
|
788
797
|
subquery = partial_subquery.subquery()
|
|
@@ -825,10 +834,10 @@ async def get_dataset_examples(
|
|
|
825
834
|
operation_id="getDatasetCsv",
|
|
826
835
|
summary="Download dataset examples as CSV file",
|
|
827
836
|
response_class=StreamingResponse,
|
|
828
|
-
status_code=
|
|
837
|
+
status_code=200,
|
|
829
838
|
responses={
|
|
830
|
-
**add_errors_to_responses([
|
|
831
|
-
**add_text_csv_content_to_responses(
|
|
839
|
+
**add_errors_to_responses([422]),
|
|
840
|
+
**add_text_csv_content_to_responses(200),
|
|
832
841
|
},
|
|
833
842
|
)
|
|
834
843
|
async def get_dataset_csv(
|
|
@@ -848,7 +857,7 @@ async def get_dataset_csv(
|
|
|
848
857
|
session=session, id=id, version_id=version_id
|
|
849
858
|
)
|
|
850
859
|
except ValueError as e:
|
|
851
|
-
raise HTTPException(detail=str(e), status_code=
|
|
860
|
+
raise HTTPException(detail=str(e), status_code=422)
|
|
852
861
|
content = await run_in_threadpool(_get_content_csv, examples)
|
|
853
862
|
encoded_dataset_name = urllib.parse.quote(dataset_name)
|
|
854
863
|
return Response(
|
|
@@ -868,7 +877,7 @@ async def get_dataset_csv(
|
|
|
868
877
|
responses=add_errors_to_responses(
|
|
869
878
|
[
|
|
870
879
|
{
|
|
871
|
-
"status_code":
|
|
880
|
+
"status_code": 422,
|
|
872
881
|
"description": "Invalid dataset or version ID",
|
|
873
882
|
}
|
|
874
883
|
]
|
|
@@ -891,7 +900,7 @@ async def get_dataset_jsonl_openai_ft(
|
|
|
891
900
|
session=session, id=id, version_id=version_id
|
|
892
901
|
)
|
|
893
902
|
except ValueError as e:
|
|
894
|
-
raise HTTPException(detail=str(e), status_code=
|
|
903
|
+
raise HTTPException(detail=str(e), status_code=422)
|
|
895
904
|
content = await run_in_threadpool(_get_content_jsonl_openai_ft, examples)
|
|
896
905
|
encoded_dataset_name = urllib.parse.quote(dataset_name)
|
|
897
906
|
response.headers["content-disposition"] = (
|
|
@@ -908,7 +917,7 @@ async def get_dataset_jsonl_openai_ft(
|
|
|
908
917
|
responses=add_errors_to_responses(
|
|
909
918
|
[
|
|
910
919
|
{
|
|
911
|
-
"status_code":
|
|
920
|
+
"status_code": 422,
|
|
912
921
|
"description": "Invalid dataset or version ID",
|
|
913
922
|
}
|
|
914
923
|
]
|
|
@@ -931,7 +940,7 @@ async def get_dataset_jsonl_openai_evals(
|
|
|
931
940
|
session=session, id=id, version_id=version_id
|
|
932
941
|
)
|
|
933
942
|
except ValueError as e:
|
|
934
|
-
raise HTTPException(detail=str(e), status_code=
|
|
943
|
+
raise HTTPException(detail=str(e), status_code=422)
|
|
935
944
|
content = await run_in_threadpool(_get_content_jsonl_openai_evals, examples)
|
|
936
945
|
encoded_dataset_name = urllib.parse.quote(dataset_name)
|
|
937
946
|
response.headers["content-disposition"] = (
|
|
@@ -1010,12 +1019,25 @@ def _get_content_jsonl_openai_evals(examples: list[models.DatasetExampleRevision
|
|
|
1010
1019
|
async def _get_db_examples(
|
|
1011
1020
|
*, session: Any, id: str, version_id: Optional[str]
|
|
1012
1021
|
) -> tuple[str, list[models.DatasetExampleRevision]]:
|
|
1013
|
-
|
|
1022
|
+
try:
|
|
1023
|
+
dataset_id = from_global_id_with_expected_type(GlobalID.from_id(id), DATASET_NODE_NAME)
|
|
1024
|
+
except Exception as e:
|
|
1025
|
+
raise HTTPException(
|
|
1026
|
+
detail=f"Invalid dataset ID format: {id}",
|
|
1027
|
+
status_code=422,
|
|
1028
|
+
) from e
|
|
1029
|
+
|
|
1014
1030
|
dataset_version_id: Optional[int] = None
|
|
1015
1031
|
if version_id:
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1032
|
+
try:
|
|
1033
|
+
dataset_version_id = from_global_id_with_expected_type(
|
|
1034
|
+
GlobalID.from_id(version_id), DATASET_VERSION_NODE_NAME
|
|
1035
|
+
)
|
|
1036
|
+
except Exception as e:
|
|
1037
|
+
raise HTTPException(
|
|
1038
|
+
detail=f"Invalid dataset version ID format: {version_id}",
|
|
1039
|
+
status_code=422,
|
|
1040
|
+
) from e
|
|
1019
1041
|
latest_version = (
|
|
1020
1042
|
select(
|
|
1021
1043
|
models.DatasetExampleRevision.dataset_example_id,
|
|
@@ -4,7 +4,6 @@ from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
4
4
|
from pydantic import Field
|
|
5
5
|
from sqlalchemy import select
|
|
6
6
|
from starlette.requests import Request
|
|
7
|
-
from starlette.status import HTTP_404_NOT_FOUND
|
|
8
7
|
from strawberry.relay import GlobalID
|
|
9
8
|
|
|
10
9
|
from phoenix.db import models
|
|
@@ -42,7 +41,7 @@ class AnnotateSpanDocumentsResponseBody(ResponseBody[list[InsertedSpanDocumentAn
|
|
|
42
41
|
responses=add_errors_to_responses(
|
|
43
42
|
[
|
|
44
43
|
{
|
|
45
|
-
"status_code":
|
|
44
|
+
"status_code": 404,
|
|
46
45
|
"description": "Span not found",
|
|
47
46
|
},
|
|
48
47
|
{
|
|
@@ -102,7 +101,7 @@ async def annotate_span_documents(
|
|
|
102
101
|
if missing_span_ids:
|
|
103
102
|
raise HTTPException(
|
|
104
103
|
detail=f"Spans with IDs {', '.join(missing_span_ids)} do not exist.",
|
|
105
|
-
status_code=
|
|
104
|
+
status_code=404,
|
|
106
105
|
)
|
|
107
106
|
|
|
108
107
|
# Validate that document positions are within bounds
|
|
@@ -15,12 +15,6 @@ from starlette.background import BackgroundTask
|
|
|
15
15
|
from starlette.datastructures import State
|
|
16
16
|
from starlette.requests import Request
|
|
17
17
|
from starlette.responses import Response, StreamingResponse
|
|
18
|
-
from starlette.status import (
|
|
19
|
-
HTTP_204_NO_CONTENT,
|
|
20
|
-
HTTP_404_NOT_FOUND,
|
|
21
|
-
HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
|
22
|
-
HTTP_422_UNPROCESSABLE_ENTITY,
|
|
23
|
-
)
|
|
24
18
|
from typing_extensions import TypeAlias
|
|
25
19
|
|
|
26
20
|
import phoenix.trace.v1 as pb
|
|
@@ -50,16 +44,16 @@ router = APIRouter(tags=["traces"], include_in_schema=True)
|
|
|
50
44
|
dependencies=[Depends(is_not_locked)],
|
|
51
45
|
operation_id="addEvaluations",
|
|
52
46
|
summary="Add span, trace, or document evaluations",
|
|
53
|
-
status_code=
|
|
47
|
+
status_code=204,
|
|
54
48
|
responses=add_errors_to_responses(
|
|
55
49
|
[
|
|
56
50
|
{
|
|
57
|
-
"status_code":
|
|
51
|
+
"status_code": 415,
|
|
58
52
|
"description": (
|
|
59
53
|
"Unsupported content type, only gzipped protobuf and pandas-arrow are supported"
|
|
60
54
|
),
|
|
61
55
|
},
|
|
62
|
-
|
|
56
|
+
422,
|
|
63
57
|
]
|
|
64
58
|
),
|
|
65
59
|
openapi_extra={
|
|
@@ -80,27 +74,21 @@ async def post_evaluations(
|
|
|
80
74
|
if content_type == "application/x-pandas-arrow":
|
|
81
75
|
return await _process_pyarrow(request)
|
|
82
76
|
if content_type != "application/x-protobuf":
|
|
83
|
-
raise HTTPException(
|
|
84
|
-
detail="Unsupported content type", status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
|
85
|
-
)
|
|
77
|
+
raise HTTPException(detail="Unsupported content type", status_code=415)
|
|
86
78
|
body = await request.body()
|
|
87
79
|
if content_encoding == "gzip":
|
|
88
80
|
body = gzip.decompress(body)
|
|
89
81
|
elif content_encoding:
|
|
90
|
-
raise HTTPException(
|
|
91
|
-
detail="Unsupported content encoding", status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
|
92
|
-
)
|
|
82
|
+
raise HTTPException(detail="Unsupported content encoding", status_code=415)
|
|
93
83
|
evaluation = pb.Evaluation()
|
|
94
84
|
try:
|
|
95
85
|
evaluation.ParseFromString(body)
|
|
96
86
|
except DecodeError:
|
|
97
|
-
raise HTTPException(
|
|
98
|
-
detail="Request body is invalid", status_code=HTTP_422_UNPROCESSABLE_ENTITY
|
|
99
|
-
)
|
|
87
|
+
raise HTTPException(detail="Request body is invalid", status_code=422)
|
|
100
88
|
if not evaluation.name.strip():
|
|
101
89
|
raise HTTPException(
|
|
102
90
|
detail="Evaluation name must not be blank/empty",
|
|
103
|
-
status_code=
|
|
91
|
+
status_code=422,
|
|
104
92
|
)
|
|
105
93
|
await request.state.enqueue_evaluation(evaluation)
|
|
106
94
|
return Response()
|
|
@@ -110,7 +98,7 @@ async def post_evaluations(
|
|
|
110
98
|
"/evaluations",
|
|
111
99
|
operation_id="getEvaluations",
|
|
112
100
|
summary="Get span, trace, or document evaluations from a project",
|
|
113
|
-
responses=add_errors_to_responses([
|
|
101
|
+
responses=add_errors_to_responses([404]),
|
|
114
102
|
)
|
|
115
103
|
async def get_evaluations(
|
|
116
104
|
request: Request,
|
|
@@ -149,7 +137,7 @@ async def get_evaluations(
|
|
|
149
137
|
and span_evals_dataframe.empty
|
|
150
138
|
and document_evals_dataframe.empty
|
|
151
139
|
):
|
|
152
|
-
return Response(status_code=
|
|
140
|
+
return Response(status_code=404)
|
|
153
141
|
|
|
154
142
|
evals = chain(
|
|
155
143
|
map(
|
|
@@ -179,7 +167,7 @@ async def _process_pyarrow(request: Request) -> Response:
|
|
|
179
167
|
except pa.ArrowInvalid:
|
|
180
168
|
raise HTTPException(
|
|
181
169
|
detail="Request body is not valid pyarrow",
|
|
182
|
-
status_code=
|
|
170
|
+
status_code=422,
|
|
183
171
|
)
|
|
184
172
|
try:
|
|
185
173
|
evaluations = Evaluations.from_pyarrow_reader(reader)
|
|
@@ -187,11 +175,11 @@ async def _process_pyarrow(request: Request) -> Response:
|
|
|
187
175
|
if isinstance(e, PhoenixEvaluationNameIsMissing):
|
|
188
176
|
raise HTTPException(
|
|
189
177
|
detail="Evaluation name must not be blank/empty",
|
|
190
|
-
status_code=
|
|
178
|
+
status_code=422,
|
|
191
179
|
)
|
|
192
180
|
raise HTTPException(
|
|
193
181
|
detail="Invalid data in request body",
|
|
194
|
-
status_code=
|
|
182
|
+
status_code=422,
|
|
195
183
|
)
|
|
196
184
|
return Response(background=BackgroundTask(_add_evaluations, request.state, evaluations))
|
|
197
185
|
|