arize-phoenix 4.7.2__py3-none-any.whl → 4.8.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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: arize-phoenix
3
- Version: 4.7.2
3
+ Version: 4.8.1
4
4
  Summary: AI Observability and Evaluation
5
5
  Project-URL: Documentation, https://docs.arize.com/phoenix/
6
6
  Project-URL: Issues, https://github.com/Arize-ai/phoenix/issues
@@ -5,7 +5,7 @@ phoenix/exceptions.py,sha256=n2L2KKuecrdflB9MsCdAYCiSEvGJptIsfRkXMoJle7A,169
5
5
  phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
6
6
  phoenix/services.py,sha256=aTxhcOA1pZHB6U-B3TEcp6fqDF5oT0xCUvEUNMZVTUQ,5175
7
7
  phoenix/settings.py,sha256=cO-qgis_S27nHirTobYI9hHPfZH18R--WMmxNdsVUwc,273
8
- phoenix/version.py,sha256=cmBVf5yWtMWpLVrALEkcO90h-q8n53EpZSufTL15jyo,22
8
+ phoenix/version.py,sha256=5LMFXHH6L0xgmxA0DlLrNZtG0vHgJ8uHcls9atNfDqI,22
9
9
  phoenix/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  phoenix/core/embedding_dimension.py,sha256=zKGbcvwOXgLf-yrJBpQyKtd-LEOPRKHnUToyAU8Owis,87
11
11
  phoenix/core/model.py,sha256=km_a--PBHOuA337ClRw9xqhOHhrUT6Rl9pz_zV0JYkQ,4843
@@ -16,7 +16,7 @@ phoenix/db/__init__.py,sha256=pDjEFXukHmJBM-1D8RjmXkvLsz85YWNxMQczt81ec3A,118
16
16
  phoenix/db/alembic.ini,sha256=p8DjVqGUs_tTx8oU56JP7qj-rMUebNFizItUSv_hPhs,3763
17
17
  phoenix/db/bulk_inserter.py,sha256=zbZGWZFDybKaGLGzpxgLwxAS5sC0_wXcvM0be4kUhh8,11286
18
18
  phoenix/db/engines.py,sha256=vLWaZlToMtDI7rJDxSidYkfOoojamxaZxaz8ND3zTus,4770
19
- phoenix/db/helpers.py,sha256=L2_jP1iIWpUREhKLYYb4_vf_6v_BiU1E73Z2PczGm6s,1589
19
+ phoenix/db/helpers.py,sha256=mTBhPzdy_aU9gD7hNzUZJkAnV77ko5CdaXyoWH3snPA,2982
20
20
  phoenix/db/migrate.py,sha256=MuhtNWnR24riROvarvKfbRb4_D5xuQi6P760vBUKl1E,2270
21
21
  phoenix/db/models.py,sha256=7DBWbxY3cx3ve2P1I0kkDKXzlt04zEFJuRPJWsVpH-I,20422
22
22
  phoenix/db/insertion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -71,6 +71,7 @@ phoenix/server/api/context.py,sha256=4jcy203Gtx38399FP21iU3HmFsq-50EKFJlX4IW2Los
71
71
  phoenix/server/api/interceptor.py,sha256=ykDnoC_apUd-llVli3m1CW18kNSIgjz2qZ6m5JmPDu8,1294
72
72
  phoenix/server/api/queries.py,sha256=wp5BlapuxDIoaQJm7mzG0dURfVxR32vXSJVC0JqG4_Y,19845
73
73
  phoenix/server/api/schema.py,sha256=BcxdqO5CSGqpKd-AAJHMjFlzaK9oJA8GJuxmMfcdjn4,434
74
+ phoenix/server/api/utils.py,sha256=Y1lGu8J8r8BSBX9OzffgewI8QMziovbG-ePDvZrrwGI,949
74
75
  phoenix/server/api/dataloaders/__init__.py,sha256=qehXL37vGdw7v5PFs3kbZVIuhuzrVNVeZACDQjYpwyo,4847
75
76
  phoenix/server/api/dataloaders/average_experiment_run_latency.py,sha256=RiO0AKC6Y5byafsV0zTJEIOt8Nudjte73f1T78cBe1k,1817
76
77
  phoenix/server/api/dataloaders/dataset_example_revisions.py,sha256=Vpr5IEKSR4QnAVxE5NM7u92fPNgeHQV2ieYc6JakCj0,3788
@@ -85,7 +86,7 @@ phoenix/server/api/dataloaders/experiment_run_counts.py,sha256=wxHv08aZELJ91KTjH
85
86
  phoenix/server/api/dataloaders/experiment_sequence_number.py,sha256=Va1KuoHOd-wzvrlKykoV4kLRFW4JsJvGp_DUI4HYZX4,1631
86
87
  phoenix/server/api/dataloaders/latency_ms_quantile.py,sha256=pEc7QjB2iiNOQm_Fmo99F5O_DKOJWgGmcnT0OADJzYE,7423
87
88
  phoenix/server/api/dataloaders/min_start_or_max_end_times.py,sha256=IoFX5PtSpvQdMk_7-oB8TpIse3Q4PMxep4qKggkHpzo,2902
88
- phoenix/server/api/dataloaders/project_by_name.py,sha256=ziYp8fZGy2duD-f_oTqNLwbrGmBQFesqlTEHBDMtZlw,1170
89
+ phoenix/server/api/dataloaders/project_by_name.py,sha256=W4q-ddeVnja2DNwgg5l8mV2GNADNJf1CTXNcJaBWdfE,1165
89
90
  phoenix/server/api/dataloaders/record_counts.py,sha256=mp3KlhwFw-Iy7o6bFxtJKC6B5kGinPIh5PxxNkrxf8o,4283
90
91
  phoenix/server/api/dataloaders/span_descendants.py,sha256=djqXXwupWaXkFdscoy-iILYijuxlqr8hJcv6JawsV6s,2162
91
92
  phoenix/server/api/dataloaders/span_evaluations.py,sha256=quXGyj_OYvHrcWfzXlYIv7C1OCZiL1K7cWU-YEAlsNA,1316
@@ -121,8 +122,8 @@ phoenix/server/api/input_types/TimeRange.py,sha256=yzx-gxj8mDeGLft1FzU_x1MVEgIG5
121
122
  phoenix/server/api/input_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
122
123
  phoenix/server/api/mutations/__init__.py,sha256=Jcz-pM6SklhEGKcjA7AIB2WJXGhnxGsghrIq131kyDo,502
123
124
  phoenix/server/api/mutations/auth.py,sha256=vPRFoj7J6PV6QeODewG4K0PhoOebS5AfMRpbi_wuhyQ,311
124
- phoenix/server/api/mutations/dataset_mutations.py,sha256=Zp2sFWyGyubILUQboR6bafRWafsfeRO2ffUWnkLlfgI,22532
125
- phoenix/server/api/mutations/experiment_mutations.py,sha256=Fw_yEdITGJ6A33M5JZ-2YnBTDoBqZUUFON6vy8JoVjE,2569
125
+ phoenix/server/api/mutations/dataset_mutations.py,sha256=CuKhxsYfvwVcdN_9EXhKxB6444BQfObzKzzyfAeg-n8,23199
126
+ phoenix/server/api/mutations/experiment_mutations.py,sha256=vV2lbJ7ccXZqe-LY7nXx6QxWqhKQE4UNZAFcML-KQ8I,3011
126
127
  phoenix/server/api/mutations/export_events_mutations.py,sha256=t_wYBxaqvBJYRoHslh3Bmoxmwlzoy0u8SsBKWIKN5hE,4028
127
128
  phoenix/server/api/mutations/project_mutations.py,sha256=3SVDCZqxB0Iv60cOwBL8c-rY3QUUPs8PXbp-C_K1mWY,2267
128
129
  phoenix/server/api/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -130,9 +131,9 @@ phoenix/server/api/openapi/main.py,sha256=WY0pj3B7siQyyYqKyhqnzWC7P8MtEtiukOBUjG
130
131
  phoenix/server/api/openapi/schema.py,sha256=uuSYe1Ecu72aXRgTNjyMu-9ZPE13DAHJPKtedS-MsSs,451
131
132
  phoenix/server/api/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
132
133
  phoenix/server/api/routers/utils.py,sha256=M41BoH-fl37izhRuN2aX7lWm7jOC20A_3uClv9TVUUY,583
133
- phoenix/server/api/routers/v1/__init__.py,sha256=vvdpUa2LJPWEg8HbvDm_ANkBAwubPIFPbbHi7elOUws,2808
134
+ phoenix/server/api/routers/v1/__init__.py,sha256=Ir5fsO6gQXW58HGm7s2sMUq0vya7mfcWneLcLJy6_q8,2895
134
135
  phoenix/server/api/routers/v1/dataset_examples.py,sha256=XfqOvDKF1oxb0pkeYfBycwwGt3LnSyyGdMLKC5VKoGQ,6690
135
- phoenix/server/api/routers/v1/datasets.py,sha256=f2gLG-geu-_wtEw4mKSzNWK2cFb5TYOyRL3tQ7Fl7Es,31544
136
+ phoenix/server/api/routers/v1/datasets.py,sha256=r0WcNxF8SKVa3-4rrTIg4Andwr4NmRmW1ybpKuxR9qw,33639
136
137
  phoenix/server/api/routers/v1/evaluations.py,sha256=8g6P_e2BweV3RDU0esFmpkb0L5fCwonQPXiJ0y6HLwg,9126
137
138
  phoenix/server/api/routers/v1/experiment_evaluations.py,sha256=H_psVyuGUQImo0oxdEAKAMQ-oyVwkVIq5yaMHzHIiPc,5455
138
139
  phoenix/server/api/routers/v1/experiment_runs.py,sha256=jy4SynmzdtQMoUzlowmG6wsVU14SsLAzfcW4JOhXjeQ,8154
@@ -202,7 +203,7 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
202
203
  phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
203
204
  phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
204
205
  phoenix/server/static/index.css,sha256=KKGpx4iwF91VGRm0YN-4cn8oC-oIqC6HecoPf0x3ZM8,1885
205
- phoenix/server/static/index.js,sha256=LmaCH-kjVuEBKvMhK3pOUxK12Wv26htH5juAzju9VWE,3524823
206
+ phoenix/server/static/index.js,sha256=sUkm_imwx5uXpCQxQEPmX-axDr094HE1wGdBznF8J_E,3524949
206
207
  phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
207
208
  phoenix/server/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
208
209
  phoenix/server/templates/index.html,sha256=S4z7qSoNSwnKFAH9r96AR-YJEyoKMd-VMWVlJ_IdzME,2039
@@ -221,9 +222,9 @@ phoenix/trace/otel.py,sha256=WA720jvRadiZBAKjsYoPyXzypHwbyEK2OZRVUwtbjB8,9976
221
222
  phoenix/trace/projects.py,sha256=2BwlNjFE-uwpqYtCu5YyBiYZk9wRPpM13vh3-Cv7GkA,2157
222
223
  phoenix/trace/schemas.py,sha256=Mjc6fD9OyeMnEk5wPPSbveqnNUYWK3p3BxpOvSGanHU,5950
223
224
  phoenix/trace/span_evaluations.py,sha256=GaADtJLi2njra4aYaie0BIwkSgdxPB_SNseglI4ykZA,13104
224
- phoenix/trace/span_json_decoder.py,sha256=IAFakPRqSMYxTPKYFMiXYxm7U-FipdN8_xbvapDS0Qc,3131
225
+ phoenix/trace/span_json_decoder.py,sha256=W6T1U0xndE2a-85O1QalJmStJK1zF1IoSU97h1RoWeY,3231
225
226
  phoenix/trace/span_json_encoder.py,sha256=tzSCIQJbeFBm33K68G8A5M12n_86tCDyuU0WAobxEz4,2010
226
- phoenix/trace/trace_dataset.py,sha256=CI9oma7sJTggUxvNHQR-uGIqrAvcSvkcDfyUBOeWjxo,13848
227
+ phoenix/trace/trace_dataset.py,sha256=9qDs8T3QQOTN9fHZzyeJlAvR-yW7tkq1OvV0xWILgzA,13917
227
228
  phoenix/trace/utils.py,sha256=7LurVGXn245cjj4MJsc7v6jq4DSJkpK6YGBfIaSywuw,1307
228
229
  phoenix/trace/dsl/README.md,sha256=ihmP9zGUC5V-TDbzKla76LuyDqPDQIBUH2BORwxNI68,2902
229
230
  phoenix/trace/dsl/__init__.py,sha256=WIQIjJg362XD3s50OsPJJ0xbDsGp41bSv7vDllLrPuA,144
@@ -247,8 +248,8 @@ phoenix/utilities/logging.py,sha256=lDXd6EGaamBNcQxL4vP1au9-i_SXe0OraUDiJOcszSw,
247
248
  phoenix/utilities/project.py,sha256=qWsvKnG1oKhOFUowXf9qiOL2ia7jaFe_ijFFHEt8GJo,431
248
249
  phoenix/utilities/re.py,sha256=PDve_OLjRTM8yQQJHC8-n3HdIONi7aNils3ZKRZ5uBM,2045
249
250
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
250
- arize_phoenix-4.7.2.dist-info/METADATA,sha256=m2l2uSMpqkXD-NpYdOBqsKlNF5jMUoLoSjnxo9is9h0,11451
251
- arize_phoenix-4.7.2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
252
- arize_phoenix-4.7.2.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
253
- arize_phoenix-4.7.2.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
254
- arize_phoenix-4.7.2.dist-info/RECORD,,
251
+ arize_phoenix-4.8.1.dist-info/METADATA,sha256=cr_RMue_eqzBxlqGELzqha7zvv0EF_J9qfc7grNpekg,11451
252
+ arize_phoenix-4.8.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
253
+ arize_phoenix-4.8.1.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
254
+ arize_phoenix-4.8.1.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
255
+ arize_phoenix-4.8.1.dist-info/RECORD,,
phoenix/db/helpers.py CHANGED
@@ -1,12 +1,12 @@
1
1
  from enum import Enum
2
- from typing import Any
2
+ from typing import Any, Optional, Tuple
3
3
 
4
4
  from openinference.semconv.trace import (
5
5
  OpenInferenceSpanKindValues,
6
6
  RerankerAttributes,
7
7
  SpanAttributes,
8
8
  )
9
- from sqlalchemy import Integer, SQLColumnExpression, case, func
9
+ from sqlalchemy import Integer, Select, SQLColumnExpression, case, distinct, func, select
10
10
  from typing_extensions import assert_never
11
11
 
12
12
  from phoenix.db import models
@@ -45,3 +45,38 @@ def num_docs_col(dialect: SupportedSQLDialect) -> SQLColumnExpression[Integer]:
45
45
 
46
46
  _RETRIEVAL_DOCUMENTS = SpanAttributes.RETRIEVAL_DOCUMENTS.split(".")
47
47
  _RERANKER_OUTPUT_DOCUMENTS = RerankerAttributes.RERANKER_OUTPUT_DOCUMENTS.split(".")
48
+
49
+
50
+ def get_eval_trace_ids_for_datasets(*dataset_ids: int) -> Select[Tuple[Optional[str]]]:
51
+ return (
52
+ select(distinct(models.ExperimentRunAnnotation.trace_id))
53
+ .join(models.ExperimentRun)
54
+ .join_from(models.ExperimentRun, models.Experiment)
55
+ .where(models.Experiment.dataset_id.in_(set(dataset_ids)))
56
+ .where(models.ExperimentRunAnnotation.trace_id.isnot(None))
57
+ )
58
+
59
+
60
+ def get_project_names_for_datasets(*dataset_ids: int) -> Select[Tuple[Optional[str]]]:
61
+ return (
62
+ select(distinct(models.Experiment.project_name))
63
+ .where(models.Experiment.dataset_id.in_(set(dataset_ids)))
64
+ .where(models.Experiment.project_name.isnot(None))
65
+ )
66
+
67
+
68
+ def get_eval_trace_ids_for_experiments(*experiment_ids: int) -> Select[Tuple[Optional[str]]]:
69
+ return (
70
+ select(distinct(models.ExperimentRunAnnotation.trace_id))
71
+ .join(models.ExperimentRun)
72
+ .where(models.ExperimentRun.experiment_id.in_(set(experiment_ids)))
73
+ .where(models.ExperimentRunAnnotation.trace_id.isnot(None))
74
+ )
75
+
76
+
77
+ def get_project_names_for_experiments(*experiment_ids: int) -> Select[Tuple[Optional[str]]]:
78
+ return (
79
+ select(distinct(models.Experiment.project_name))
80
+ .where(models.Experiment.id.in_(set(experiment_ids)))
81
+ .where(models.Experiment.project_name.isnot(None))
82
+ )
@@ -28,4 +28,4 @@ class ProjectByNameDataLoader(DataLoader[Key, Result]):
28
28
  async for project in data:
29
29
  projects_by_name[project.name] = project
30
30
 
31
- return [projects_by_name[project_name] for project_name in project_names]
31
+ return [projects_by_name.get(project_name) for project_name in keys]
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  from datetime import datetime
2
3
  from typing import Any, Dict
3
4
 
@@ -10,6 +11,7 @@ from strawberry import UNSET
10
11
  from strawberry.types import Info
11
12
 
12
13
  from phoenix.db import models
14
+ from phoenix.db.helpers import get_eval_trace_ids_for_datasets, get_project_names_for_datasets
13
15
  from phoenix.server.api.context import Context
14
16
  from phoenix.server.api.helpers.dataset_helpers import (
15
17
  get_dataset_example_input,
@@ -30,6 +32,7 @@ from phoenix.server.api.types.Dataset import Dataset, to_gql_dataset
30
32
  from phoenix.server.api.types.DatasetExample import DatasetExample
31
33
  from phoenix.server.api.types.node import from_global_id_with_expected_type
32
34
  from phoenix.server.api.types.Span import Span
35
+ from phoenix.server.api.utils import delete_projects, delete_traces
33
36
 
34
37
 
35
38
  @strawberry.type
@@ -274,21 +277,28 @@ class DatasetMutationMixin:
274
277
  info: Info[Context, None],
275
278
  input: DeleteDatasetInput,
276
279
  ) -> DatasetMutationPayload:
277
- dataset_id = input.dataset_id
278
- dataset_rowid = from_global_id_with_expected_type(
279
- global_id=dataset_id, expected_type_name=Dataset.__name__
280
+ try:
281
+ dataset_id = from_global_id_with_expected_type(
282
+ global_id=input.dataset_id,
283
+ expected_type_name=Dataset.__name__,
284
+ )
285
+ except ValueError:
286
+ raise ValueError(f"Unknown dataset: {input.dataset_id}")
287
+ project_names_stmt = get_project_names_for_datasets(dataset_id)
288
+ eval_trace_ids_stmt = get_eval_trace_ids_for_datasets(dataset_id)
289
+ stmt = (
290
+ delete(models.Dataset).where(models.Dataset.id == dataset_id).returning(models.Dataset)
280
291
  )
281
-
282
292
  async with info.context.db() as session:
283
- delete_result = await session.execute(
284
- delete(models.Dataset)
285
- .where(models.Dataset.id == dataset_rowid)
286
- .returning(models.Dataset)
287
- )
288
- if not (datasets := delete_result.first()):
289
- raise ValueError(f"Unknown dataset: {dataset_id}")
290
-
291
- dataset = datasets[0]
293
+ project_names = await session.scalars(project_names_stmt)
294
+ eval_trace_ids = await session.scalars(eval_trace_ids_stmt)
295
+ if not (dataset := await session.scalar(stmt)):
296
+ raise ValueError(f"Unknown dataset: {input.dataset_id}")
297
+ await asyncio.gather(
298
+ delete_projects(info.context.db, *project_names),
299
+ delete_traces(info.context.db, *eval_trace_ids),
300
+ return_exceptions=True,
301
+ )
292
302
  return DatasetMutationPayload(dataset=to_gql_dataset(dataset))
293
303
 
294
304
  @strawberry.mutation(permission_classes=[IsAuthenticated]) # type: ignore
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  from typing import List
2
3
 
3
4
  import strawberry
@@ -6,11 +7,13 @@ from strawberry.relay import GlobalID
6
7
  from strawberry.types import Info
7
8
 
8
9
  from phoenix.db import models
10
+ from phoenix.db.helpers import get_eval_trace_ids_for_experiments, get_project_names_for_experiments
9
11
  from phoenix.server.api.context import Context
10
12
  from phoenix.server.api.input_types.DeleteExperimentsInput import DeleteExperimentsInput
11
13
  from phoenix.server.api.mutations.auth import IsAuthenticated
12
14
  from phoenix.server.api.types.Experiment import Experiment, to_gql_experiment
13
15
  from phoenix.server.api.types.node import from_global_id_with_expected_type
16
+ from phoenix.server.api.utils import delete_projects, delete_traces
14
17
 
15
18
 
16
19
  @strawberry.type
@@ -30,7 +33,11 @@ class ExperimentMutationMixin:
30
33
  from_global_id_with_expected_type(experiment_id, Experiment.__name__)
31
34
  for experiment_id in input.experiment_ids
32
35
  ]
36
+ project_names_stmt = get_project_names_for_experiments(*experiment_ids)
37
+ eval_trace_ids_stmt = get_eval_trace_ids_for_experiments(*experiment_ids)
33
38
  async with info.context.db() as session:
39
+ project_names = await session.scalars(project_names_stmt)
40
+ eval_trace_ids = await session.scalars(eval_trace_ids_stmt)
34
41
  savepoint = await session.begin_nested()
35
42
  experiments = {
36
43
  experiment.id: experiment
@@ -54,10 +61,11 @@ class ExperimentMutationMixin:
54
61
  ]
55
62
  )
56
63
  )
57
- if project_names := set(filter(bool, (e.project_name for e in experiments.values()))):
58
- await session.execute(
59
- delete(models.Project).where(models.Project.name.in_(project_names))
60
- )
64
+ await asyncio.gather(
65
+ delete_projects(info.context.db, *project_names),
66
+ delete_traces(info.context.db, *eval_trace_ids),
67
+ return_exceptions=True,
68
+ )
61
69
  return ExperimentMutationPayload(
62
70
  experiments=[
63
71
  to_gql_experiment(experiments[experiment_id]) for experiment_id in experiment_ids
@@ -44,6 +44,7 @@ V1_ROUTES = [
44
44
  Route("/v1/spans", spans.get_spans_handler, methods=["GET"]),
45
45
  Route("/v1/datasets/upload", datasets.post_datasets_upload, methods=["POST"]),
46
46
  Route("/v1/datasets", datasets.list_datasets, methods=["GET"]),
47
+ Route("/v1/datasets/{id:str}", datasets.delete_dataset_by_id, methods=["DELETE"]),
47
48
  Route("/v1/datasets/{id:str}", datasets.get_dataset_by_id, methods=["GET"]),
48
49
  Route("/v1/datasets/{id:str}/csv", datasets.get_dataset_csv, methods=["GET"]),
49
50
  Route(
@@ -26,13 +26,15 @@ from typing import (
26
26
 
27
27
  import pandas as pd
28
28
  import pyarrow as pa
29
- from sqlalchemy import and_, func, select
29
+ from sqlalchemy import and_, delete, func, select
30
30
  from sqlalchemy.ext.asyncio import AsyncSession
31
+ from starlette.background import BackgroundTasks
31
32
  from starlette.concurrency import run_in_threadpool
32
33
  from starlette.datastructures import FormData, UploadFile
33
34
  from starlette.requests import Request
34
35
  from starlette.responses import JSONResponse, Response
35
36
  from starlette.status import (
37
+ HTTP_204_NO_CONTENT,
36
38
  HTTP_404_NOT_FOUND,
37
39
  HTTP_409_CONFLICT,
38
40
  HTTP_422_UNPROCESSABLE_ENTITY,
@@ -42,6 +44,7 @@ from strawberry.relay import GlobalID
42
44
  from typing_extensions import TypeAlias, assert_never
43
45
 
44
46
  from phoenix.db import models
47
+ from phoenix.db.helpers import get_eval_trace_ids_for_datasets, get_project_names_for_datasets
45
48
  from phoenix.db.insertion.dataset import (
46
49
  DatasetAction,
47
50
  DatasetExampleAdditionEvent,
@@ -52,6 +55,7 @@ from phoenix.server.api.types.Dataset import Dataset
52
55
  from phoenix.server.api.types.DatasetExample import DatasetExample
53
56
  from phoenix.server.api.types.DatasetVersion import DatasetVersion
54
57
  from phoenix.server.api.types.node import from_global_id_with_expected_type
58
+ from phoenix.server.api.utils import delete_projects, delete_traces
55
59
 
56
60
  logger = logging.getLogger(__name__)
57
61
 
@@ -163,6 +167,60 @@ async def list_datasets(request: Request) -> Response:
163
167
  return JSONResponse(content={"next_cursor": next_cursor, "data": data})
164
168
 
165
169
 
170
+ async def delete_dataset_by_id(request: Request) -> Response:
171
+ """
172
+ summary: Delete dataset by ID
173
+ operationId: deleteDatasetById
174
+ tags:
175
+ - datasets
176
+ parameters:
177
+ - in: path
178
+ name: id
179
+ required: true
180
+ schema:
181
+ type: string
182
+ responses:
183
+ 204:
184
+ description: Success
185
+ 403:
186
+ description: Forbidden
187
+ 404:
188
+ description: Dataset not found
189
+ 422:
190
+ description: Dataset ID is invalid
191
+ """
192
+ if id_ := request.path_params.get("id"):
193
+ try:
194
+ dataset_id = from_global_id_with_expected_type(
195
+ GlobalID.from_id(id_),
196
+ Dataset.__name__,
197
+ )
198
+ except ValueError:
199
+ return Response(
200
+ content=f"Invalid Dataset ID: {id_}",
201
+ status_code=HTTP_422_UNPROCESSABLE_ENTITY,
202
+ )
203
+ else:
204
+ return Response(
205
+ content="Missing Dataset ID",
206
+ status_code=HTTP_422_UNPROCESSABLE_ENTITY,
207
+ )
208
+ project_names_stmt = get_project_names_for_datasets(dataset_id)
209
+ eval_trace_ids_stmt = get_eval_trace_ids_for_datasets(dataset_id)
210
+ stmt = (
211
+ delete(models.Dataset).where(models.Dataset.id == dataset_id).returning(models.Dataset.id)
212
+ )
213
+ async with request.app.state.db() as session:
214
+ project_names = await session.scalars(project_names_stmt)
215
+ eval_trace_ids = await session.scalars(eval_trace_ids_stmt)
216
+ if (await session.scalar(stmt)) is None:
217
+ return Response(content="Dataset does not exist", status_code=HTTP_404_NOT_FOUND)
218
+ tasks = BackgroundTasks()
219
+ tasks.add_task(delete_projects, request.app.state.db, *project_names)
220
+ tasks.add_task(delete_traces, request.app.state.db, *eval_trace_ids)
221
+ return Response(status_code=HTTP_204_NO_CONTENT, background=tasks)
222
+
223
+
166
224
  async def get_dataset_by_id(request: Request) -> Response:
167
225
  """
168
226
  summary: Get dataset by ID
@@ -0,0 +1,36 @@
1
+ from typing import AsyncContextManager, Callable, List
2
+
3
+ from sqlalchemy import delete
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+ from phoenix.db import models
7
+
8
+
9
+ async def delete_projects(
10
+ db: Callable[[], AsyncContextManager[AsyncSession]],
11
+ *project_names: str,
12
+ ) -> List[int]:
13
+ if not project_names:
14
+ return []
15
+ stmt = (
16
+ delete(models.Project)
17
+ .where(models.Project.name.in_(set(project_names)))
18
+ .returning(models.Project.id)
19
+ )
20
+ async with db() as session:
21
+ return list(await session.scalars(stmt))
22
+
23
+
24
+ async def delete_traces(
25
+ db: Callable[[], AsyncContextManager[AsyncSession]],
26
+ *trace_ids: str,
27
+ ) -> List[int]:
28
+ if not trace_ids:
29
+ return []
30
+ stmt = (
31
+ delete(models.Trace)
32
+ .where(models.Trace.trace_id.in_(set(trace_ids)))
33
+ .returning(models.Trace.id)
34
+ )
35
+ async with db() as session:
36
+ return list(await session.scalars(stmt))
@@ -7750,7 +7750,7 @@ fragment DatasetsTable_datasets on Query {
7750
7750
  __typename
7751
7751
  }
7752
7752
  }
7753
- `}}}();bat.hash="d98c682a2c908030c2f772209a4a25b8";var mat=bat;var Q0=P(ie()),Iat=P(wt());function pat(t){let{datasetId:e,datasetName:n,onDatasetDelete:i,onDatasetDeleteError:r}=t,[a,o]=(0,Q0.useState)(null),[l,s]=(0,Iat.useMutation)(mat),c=(0,Q0.useCallback)(()=>{(0,Q0.startTransition)(()=>{l({variables:{datasetId:e},onCompleted:()=>{i()},onError:g=>{r(g)}})})},[l,e,i,r]),u=(0,Q0.useCallback)(()=>{o(B(Mn,{size:"S",title:"Delete Dataset",children:[h(De,{padding:"size-200",children:h(me,{color:"danger",children:`Are you sure you want to delete dataset ${n}? This cannot be undone.`})}),h(De,{paddingEnd:"size-200",paddingTop:"size-100",paddingBottom:"size-100",borderTopColor:"light",borderTopWidth:"thin",children:h(ge,{direction:"row",justifyContent:"end",children:h(At,{variant:"danger",onClick:()=>{c(),o(null)},children:"Delete Dataset"})})})]}))},[c,n]);return B("div",{onClick:g=>{g.preventDefault(),g.stopPropagation()},children:[h(Gc,{align:"end",buttonSize:"compact",isDisabled:s,onAction:g=>{switch(g){case"deleteDataset":u();break}},children:h(Ht,{children:B(ge,{direction:"row",gap:"size-75",justifyContent:"start",alignItems:"center",children:[h(ke,{svg:h(Pe.TrashOutline,{})}),h(me,{children:"Delete"})]})},"deleteDataset")}),h(Fn,{type:"modal",isDismissable:!0,onDismiss:()=>o(null),children:a})]})}var hat=100;function cGn(t){let e=t.id;if(e!=="createdAt"&&e!=="name")throw new Error("Invalid sort column");return{col:e,dir:t.desc?"desc":"asc"}}function Cat(t){let[e,n]=(0,cd.useState)([]),i=(0,cd.useRef)(null),r=Zn(),a=Zo(),o=dm(),{data:l,loadNext:s,hasNext:c,isLoadingNext:u,refetch:g}=(0,fat.usePaginationFragment)(dat,t.query),d=(0,cd.useMemo)(()=>l.datasets.edges.map(f=>f.node),[l]),b=cd.default.useCallback(f=>{if(f){let{scrollHeight:C,scrollTop:v,clientHeight:x}=f;C-v-x<300&&!u&&c&&s(hat)}},[c,u,s]),m=Gr({columns:[{header:"name",accessorKey:"name",cell:({row:f})=>{let v=f.original.experimentCount>0?`${f.original.id}/experiments`:`${f.original.id}/examples`;return h(_l,{to:v,children:f.original.name})}},{header:"description",accessorKey:"description",enableSorting:!1},{header:"created at",accessorKey:"createdAt",cell:sd},{header:"example count",accessorKey:"exampleCount",enableSorting:!1,meta:{textAlign:"right"}},{header:"experiment count",accessorKey:"experimentCount",enableSorting:!1,meta:{textAlign:"right"}},{header:"",id:"actions",enableSorting:!1,size:10,cell:({row:f})=>h(pat,{datasetId:f.original.id,datasetName:f.original.name,onDatasetDelete:()=>{a({title:"Dataset deleted",message:`${f.original.name} has been successfully deleted.`}),g({},{fetchPolicy:"store-and-network"})},onDatasetDeleteError:C=>{o({title:"Dataset deletion failed",message:C.message})}})}],data:d,state:{sorting:e},getCoreRowModel:Wr(),getSortedRowModel:Zg(),onSortingChange:n,manualSorting:!0});(0,cd.useEffect)(()=>{let f=e[0];(0,cd.startTransition)(()=>{g({sort:f?cGn(f):{col:"createdAt",dir:"desc"},after:null,first:hat},{fetchPolicy:"store-and-network"})})},[e,g]);let I=m.getRowModel().rows,p=I.length===0;return h("div",{css:$`
7753
+ `}}}();bat.hash="d98c682a2c908030c2f772209a4a25b8";var mat=bat;var Q0=P(ie()),Iat=P(wt());function pat(t){let{datasetId:e,datasetName:n,onDatasetDelete:i,onDatasetDeleteError:r}=t,[a,o]=(0,Q0.useState)(null),[l,s]=(0,Iat.useMutation)(mat),c=(0,Q0.useCallback)(()=>{(0,Q0.startTransition)(()=>{l({variables:{datasetId:e},onCompleted:()=>{i()},onError:g=>{r(g)}})})},[l,e,i,r]),u=(0,Q0.useCallback)(()=>{o(B(Mn,{size:"S",title:"Delete Dataset",children:[h(De,{padding:"size-200",children:h(me,{color:"danger",children:`Are you sure you want to delete dataset ${n}? This will also delete all associated experiments and traces, and it cannot be undone.`})}),h(De,{paddingEnd:"size-200",paddingTop:"size-100",paddingBottom:"size-100",borderTopColor:"light",borderTopWidth:"thin",children:h(ge,{direction:"row",justifyContent:"end",children:h(At,{variant:"danger",onClick:()=>{c(),o(null)},children:"Delete Dataset"})})})]}))},[c,n]);return B("div",{onClick:g=>{g.preventDefault(),g.stopPropagation()},children:[h(Gc,{align:"end",buttonSize:"compact",isDisabled:s,onAction:g=>{switch(g){case"deleteDataset":u();break}},children:h(Ht,{children:B(ge,{direction:"row",gap:"size-75",justifyContent:"start",alignItems:"center",children:[h(ke,{svg:h(Pe.TrashOutline,{})}),h(me,{children:"Delete"})]})},"deleteDataset")}),h(Fn,{type:"modal",isDismissable:!0,onDismiss:()=>o(null),children:a})]})}var hat=100;function cGn(t){let e=t.id;if(e!=="createdAt"&&e!=="name")throw new Error("Invalid sort column");return{col:e,dir:t.desc?"desc":"asc"}}function Cat(t){let[e,n]=(0,cd.useState)([]),i=(0,cd.useRef)(null),r=Zn(),a=Zo(),o=dm(),{data:l,loadNext:s,hasNext:c,isLoadingNext:u,refetch:g}=(0,fat.usePaginationFragment)(dat,t.query),d=(0,cd.useMemo)(()=>l.datasets.edges.map(f=>f.node),[l]),b=cd.default.useCallback(f=>{if(f){let{scrollHeight:C,scrollTop:v,clientHeight:x}=f;C-v-x<300&&!u&&c&&s(hat)}},[c,u,s]),m=Gr({columns:[{header:"name",accessorKey:"name",cell:({row:f})=>{let v=f.original.experimentCount>0?`${f.original.id}/experiments`:`${f.original.id}/examples`;return h(_l,{to:v,children:f.original.name})}},{header:"description",accessorKey:"description",enableSorting:!1},{header:"created at",accessorKey:"createdAt",cell:sd},{header:"example count",accessorKey:"exampleCount",enableSorting:!1,meta:{textAlign:"right"}},{header:"experiment count",accessorKey:"experimentCount",enableSorting:!1,meta:{textAlign:"right"}},{header:"",id:"actions",enableSorting:!1,size:10,cell:({row:f})=>h(pat,{datasetId:f.original.id,datasetName:f.original.name,onDatasetDelete:()=>{a({title:"Dataset deleted",message:`${f.original.name} has been successfully deleted.`}),g({},{fetchPolicy:"store-and-network"})},onDatasetDeleteError:C=>{o({title:"Dataset deletion failed",message:C.message})}})}],data:d,state:{sorting:e},getCoreRowModel:Wr(),getSortedRowModel:Zg(),onSortingChange:n,manualSorting:!0});(0,cd.useEffect)(()=>{let f=e[0];(0,cd.startTransition)(()=>{g({sort:f?cGn(f):{col:"createdAt",dir:"desc"},after:null,first:hat},{fetchPolicy:"store-and-network"})})},[e,g]);let I=m.getRowModel().rows,p=I.length===0;return h("div",{css:$`
7754
7754
  flex: 1 1 auto;
7755
7755
  overflow: auto;
7756
7756
  `,onScroll:f=>b(f.target),ref:i,children:B("table",{css:wg,children:[h("thead",{children:m.getHeaderGroups().map(f=>h("tr",{children:f.headers.map(C=>h("th",{colSpan:C.colSpan,children:C.isPlaceholder?null:B("div",{className:C.column.getCanSort()?"cursor-pointer":"",onClick:C.column.getToggleSortingHandler(),style:{textAlign:C.column.columnDef.meta?.textAlign},children:[Hn(C.column.columnDef.header,C.getContext()),C.column.getIsSorted()?h(ke,{className:"sort-icon",svg:C.column.getIsSorted()==="asc"?h(Pe.ArrowUpFilled,{}):h(Pe.ArrowDownFilled,{})}):null]})},C.id))},f.id))}),p?h(Ul,{}):h("tbody",{children:I.map(f=>h("tr",{onClick:()=>{let v=f.original.experimentCount>0?`${f.original.id}/experiments`:`${f.original.id}/examples`;r(v)},children:f.getVisibleCells().map(C=>h("td",{align:C.column.columnDef.meta?.textAlign,children:Hn(C.column.columnDef.cell,C.getContext())},C.id))},f.id))})]})})}function yat(){return h(K0.Suspense,{fallback:h(Qr,{}),children:h(uGn,{})})}function uGn(){let[t,e]=(0,K0.useState)(0),n=(0,Aat.useLazyLoadQuery)(oat,{},{fetchKey:t}),i=(0,K0.useCallback)(()=>{e(r=>r+1)},[e]);return B(ge,{direction:"column",height:"100%",children:[h(De,{padding:"size-200",borderBottomWidth:"thin",borderBottomColor:"dark",flex:"none",children:B(ge,{direction:"row",justifyContent:"space-between",children:[h(dn,{level:1,children:"Datasets"}),h(gGn,{onDatasetCreated:i})]})}),h(Cat,{query:n})]})}function gGn({onDatasetCreated:t}){let e=Zn(),n=Zo(),i=dm(),[r,a]=(0,K0.useState)(null),o=()=>{a(h(Mn,{size:"S",title:"New Dataset",children:h(rY,{onDatasetCreated:s=>{n({title:"Dataset created",message:`${s.name} has been successfully created.`,action:{text:"Go to Dataset",onClick:()=>{e(`/datasets/${s.id}`)}}}),a(null),t()},onDatasetCreateError:s=>{i({title:"Dataset creation failed",message:s.message})}})}))},l=()=>{a(h(Mn,{size:"M",title:"New Dataset from CSV",children:h(lat,{onDatasetCreated:s=>{n({title:"Dataset created",message:`${s.name} has been successfully created.`,action:{text:"Go to Dataset",onClick:()=>{e(`/datasets/${s.id}`)}}}),a(null),t()},onDatasetCreateError:s=>{i({title:"Dataset creation failed",message:s.message})}})}))};return B($t,{children:[B(Gc,{buttonText:"Create Dataset",align:"end",icon:h(ke,{svg:h(Pe.DatabaseOutline,{})}),onAction:s=>{switch(s){case"newDataset":o();break;case"datasetFromCSV":l();break}},children:[h(Ht,{children:"New Dataset"},"newDataset"),h(Ht,{children:"Dataset from CSV"},"datasetFromCSV")]}),h(Fn,{type:"modal",isDismissable:!0,onDismiss:()=>a(null),children:r})]})}var tA=P(ie());var uY=P(ie());var vat=function(){var t=[{defaultValue:null,kind:"LocalArgument",name:"datasetId"}],e=[{kind:"Variable",name:"id",variableName:"datasetId"}],n={alias:null,args:null,kind:"ScalarField",name:"id",storageKey:null},i={kind:"InlineFragment",selections:[{alias:"latestVersions",args:[{kind:"Literal",name:"first",value:1},{kind:"Literal",name:"sort",value:{col:"createdAt",dir:"desc"}}],concreteType:"DatasetVersionConnection",kind:"LinkedField",name:"versions",plural:!1,selections:[{alias:null,args:null,concreteType:"DatasetVersionEdge",kind:"LinkedField",name:"edges",plural:!0,selections:[{alias:"version",args:null,concreteType:"DatasetVersion",kind:"LinkedField",name:"node",plural:!1,selections:[n,{alias:null,args:null,kind:"ScalarField",name:"description",storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"createdAt",storageKey:null}],storageKey:null}],storageKey:null}],storageKey:'versions(first:1,sort:{"col":"createdAt","dir":"desc"})'}],type:"Dataset",abstractKey:null};return{fragment:{argumentDefinitions:t,kind:"Fragment",metadata:null,name:"datasetStore_latestVersionQuery",selections:[{alias:"dataset",args:e,concreteType:null,kind:"LinkedField",name:"node",plural:!1,selections:[n,i],storageKey:null}],type:"Query",abstractKey:null},kind:"Request",operation:{argumentDefinitions:t,kind:"Operation",name:"datasetStore_latestVersionQuery",selections:[{alias:"dataset",args:e,concreteType:null,kind:"LinkedField",name:"node",plural:!1,selections:[{alias:null,args:null,kind:"ScalarField",name:"__typename",storageKey:null},n,i],storageKey:null}]},params:{cacheID:"d7075240e4dae8871997e57616c05302",id:null,metadata:{},name:"datasetStore_latestVersionQuery",operationKind:"query",text:`query datasetStore_latestVersionQuery(
@@ -8152,7 +8152,7 @@ fragment ExperimentsTableFragment on Dataset {
8152
8152
  __typename
8153
8153
  }
8154
8154
  }
8155
- `}}}();Vot.hash="4ab310746eb51c6a3fcb799d2c972762";var Eot=Vot;var a3=P(ie()),Jot=P(wt());function Hot(t){let e=Zn(),[n,i]=(0,a3.useState)(null),[r,a]=(0,Jot.useMutation)(Eot),{datasetId:o,selectedExperiments:l,onClearSelection:s,onExperimentsDeleted:c}=t,u=l.length!==1,g=Zo(),d=dm(),b=(0,a3.useCallback)(()=>{r({variables:{input:{experimentIds:l.map(I=>I.id)}},onCompleted:()=>{g({title:"Examples Deleted",message:`${l.length} experiment${u?"s":""} have been deleted.`}),c(),s()},onError:I=>{d({title:"An error occurred",message:`Failed to delete examples: ${I.message}`})}})},[r,u,d,g,s,c,l]),m=(0,a3.useCallback)(()=>{i(B(Mn,{size:"S",title:"Delete Experiments",children:[h(De,{padding:"size-200",children:h(me,{color:"danger",children:"Are you sure you want to delete these experiments? This cannot be undone."})}),h(De,{paddingEnd:"size-200",paddingTop:"size-100",paddingBottom:"size-100",borderTopColor:"light",borderTopWidth:"thin",children:h(ge,{direction:"row",justifyContent:"end",children:h(At,{variant:"danger",onClick:()=>{b(),i(null)},children:"Delete Experiments"})})})]}))},[b]);return B("div",{css:$`
8155
+ `}}}();Vot.hash="4ab310746eb51c6a3fcb799d2c972762";var Eot=Vot;var a3=P(ie()),Jot=P(wt());function Hot(t){let e=Zn(),[n,i]=(0,a3.useState)(null),[r,a]=(0,Jot.useMutation)(Eot),{datasetId:o,selectedExperiments:l,onClearSelection:s,onExperimentsDeleted:c}=t,u=l.length!==1,g=Zo(),d=dm(),b=(0,a3.useCallback)(()=>{r({variables:{input:{experimentIds:l.map(I=>I.id)}},onCompleted:()=>{g({title:"Examples Deleted",message:`${l.length} experiment${u?"s":""} have been deleted.`}),c(),s()},onError:I=>{d({title:"An error occurred",message:`Failed to delete examples: ${I.message}`})}})},[r,u,d,g,s,c,l]),m=(0,a3.useCallback)(()=>{i(B(Mn,{size:"S",title:"Delete Experiments",children:[h(De,{padding:"size-200",children:h(me,{color:"danger",children:"Are you sure you want to delete these experiments? This will also delete all associated annotations and traces, and it cannot be undone."})}),h(De,{paddingEnd:"size-200",paddingTop:"size-100",paddingBottom:"size-100",borderTopColor:"light",borderTopWidth:"thin",children:h(ge,{direction:"row",justifyContent:"end",children:h(At,{variant:"danger",onClick:()=>{b(),i(null)},children:"Delete Experiments"})})})]}))},[b]);return B("div",{css:$`
8156
8156
  position: absolute;
8157
8157
  bottom: var(--ac-global-dimension-size-400);
8158
8158
  left: 50%;
@@ -80,7 +80,9 @@ def json_to_span(data: Dict[str, Any]) -> Any:
80
80
  attributes=event.get("attributes") or {},
81
81
  timestamp=datetime.fromisoformat(event["timestamp"]),
82
82
  )
83
- for event in data["events"]
83
+ for event in (
84
+ data["events"] if isinstance(data["events"], list) else json.loads(data["events"])
85
+ )
84
86
  ]
85
87
  data["conversation"] = (
86
88
  SpanConversationAttributes(**data["conversation"])
@@ -5,6 +5,7 @@ from typing import Any, Iterable, Iterator, List, Optional, Tuple, Union, cast
5
5
  from uuid import UUID, uuid4
6
6
  from warnings import warn
7
7
 
8
+ import numpy as np
8
9
  import pandas as pd
9
10
  from openinference.semconv.trace import (
10
11
  DocumentAttributes,
@@ -60,6 +61,7 @@ def normalize_dataframe(dataframe: DataFrame) -> "DataFrame":
60
61
  # Convert the start and end times to datetime
61
62
  dataframe["start_time"] = normalize_timestamps(dataframe["start_time"])
62
63
  dataframe["end_time"] = normalize_timestamps(dataframe["end_time"])
64
+ dataframe = dataframe.replace({np.nan: None})
63
65
  return dataframe
64
66
 
65
67
 
phoenix/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "4.7.2"
1
+ __version__ = "4.8.1"