arize-phoenix 4.4.4rc5__py3-none-any.whl → 4.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-4.4.4rc5.dist-info → arize_phoenix-4.5.0.dist-info}/METADATA +5 -5
- {arize_phoenix-4.4.4rc5.dist-info → arize_phoenix-4.5.0.dist-info}/RECORD +56 -117
- {arize_phoenix-4.4.4rc5.dist-info → arize_phoenix-4.5.0.dist-info}/WHEEL +1 -1
- phoenix/__init__.py +27 -0
- phoenix/config.py +7 -21
- phoenix/core/model.py +25 -25
- phoenix/core/model_schema.py +62 -64
- phoenix/core/model_schema_adapter.py +25 -27
- phoenix/db/bulk_inserter.py +14 -54
- phoenix/db/insertion/evaluation.py +6 -6
- phoenix/db/insertion/helpers.py +2 -13
- phoenix/db/migrations/versions/cf03bd6bae1d_init.py +28 -2
- phoenix/db/models.py +4 -236
- phoenix/inferences/fixtures.py +23 -23
- phoenix/inferences/inferences.py +7 -7
- phoenix/inferences/validation.py +1 -1
- phoenix/server/api/context.py +0 -18
- phoenix/server/api/dataloaders/__init__.py +0 -18
- phoenix/server/api/dataloaders/span_descendants.py +3 -2
- phoenix/server/api/routers/v1/__init__.py +2 -77
- phoenix/server/api/routers/v1/evaluations.py +2 -4
- phoenix/server/api/routers/v1/spans.py +1 -3
- phoenix/server/api/routers/v1/traces.py +4 -1
- phoenix/server/api/schema.py +303 -2
- phoenix/server/api/types/Cluster.py +19 -19
- phoenix/server/api/types/Dataset.py +63 -282
- phoenix/server/api/types/DatasetRole.py +23 -0
- phoenix/server/api/types/Dimension.py +29 -30
- phoenix/server/api/types/EmbeddingDimension.py +34 -40
- phoenix/server/api/types/Event.py +16 -16
- phoenix/server/api/{mutations/export_events_mutations.py → types/ExportEventsMutation.py} +14 -17
- phoenix/server/api/types/Model.py +42 -43
- phoenix/server/api/types/Project.py +12 -26
- phoenix/server/api/types/Span.py +2 -79
- phoenix/server/api/types/TimeSeries.py +6 -6
- phoenix/server/api/types/Trace.py +4 -15
- phoenix/server/api/types/UMAPPoints.py +1 -1
- phoenix/server/api/types/node.py +111 -5
- phoenix/server/api/types/pagination.py +52 -10
- phoenix/server/app.py +49 -101
- phoenix/server/main.py +27 -49
- phoenix/server/openapi/docs.py +0 -3
- phoenix/server/static/index.js +2595 -3523
- phoenix/server/templates/index.html +0 -1
- phoenix/services.py +15 -15
- phoenix/session/client.py +21 -438
- phoenix/session/session.py +37 -47
- phoenix/trace/exporter.py +9 -14
- phoenix/trace/fixtures.py +7 -133
- phoenix/trace/schemas.py +2 -1
- phoenix/trace/span_evaluations.py +3 -3
- phoenix/trace/trace_dataset.py +6 -6
- phoenix/version.py +1 -1
- phoenix/datasets/__init__.py +0 -0
- phoenix/datasets/evaluators/__init__.py +0 -18
- phoenix/datasets/evaluators/code_evaluators.py +0 -99
- phoenix/datasets/evaluators/llm_evaluators.py +0 -244
- phoenix/datasets/evaluators/utils.py +0 -292
- phoenix/datasets/experiments.py +0 -550
- phoenix/datasets/tracing.py +0 -85
- phoenix/datasets/types.py +0 -178
- phoenix/db/insertion/dataset.py +0 -237
- phoenix/db/migrations/types.py +0 -29
- phoenix/db/migrations/versions/10460e46d750_datasets.py +0 -291
- phoenix/server/api/dataloaders/dataset_example_revisions.py +0 -100
- phoenix/server/api/dataloaders/dataset_example_spans.py +0 -43
- phoenix/server/api/dataloaders/experiment_annotation_summaries.py +0 -85
- phoenix/server/api/dataloaders/experiment_error_rates.py +0 -43
- phoenix/server/api/dataloaders/experiment_run_counts.py +0 -42
- phoenix/server/api/dataloaders/experiment_sequence_number.py +0 -49
- phoenix/server/api/dataloaders/project_by_name.py +0 -31
- phoenix/server/api/dataloaders/span_projects.py +0 -33
- phoenix/server/api/dataloaders/trace_row_ids.py +0 -39
- phoenix/server/api/helpers/dataset_helpers.py +0 -179
- phoenix/server/api/input_types/AddExamplesToDatasetInput.py +0 -16
- phoenix/server/api/input_types/AddSpansToDatasetInput.py +0 -14
- phoenix/server/api/input_types/ClearProjectInput.py +0 -15
- phoenix/server/api/input_types/CreateDatasetInput.py +0 -12
- phoenix/server/api/input_types/DatasetExampleInput.py +0 -14
- phoenix/server/api/input_types/DatasetSort.py +0 -17
- phoenix/server/api/input_types/DatasetVersionSort.py +0 -16
- phoenix/server/api/input_types/DeleteDatasetExamplesInput.py +0 -13
- phoenix/server/api/input_types/DeleteDatasetInput.py +0 -7
- phoenix/server/api/input_types/DeleteExperimentsInput.py +0 -9
- phoenix/server/api/input_types/PatchDatasetExamplesInput.py +0 -35
- phoenix/server/api/input_types/PatchDatasetInput.py +0 -14
- phoenix/server/api/mutations/__init__.py +0 -13
- phoenix/server/api/mutations/auth.py +0 -11
- phoenix/server/api/mutations/dataset_mutations.py +0 -520
- phoenix/server/api/mutations/experiment_mutations.py +0 -65
- phoenix/server/api/mutations/project_mutations.py +0 -47
- phoenix/server/api/openapi/__init__.py +0 -0
- phoenix/server/api/openapi/main.py +0 -6
- phoenix/server/api/openapi/schema.py +0 -16
- phoenix/server/api/queries.py +0 -503
- phoenix/server/api/routers/v1/dataset_examples.py +0 -178
- phoenix/server/api/routers/v1/datasets.py +0 -965
- phoenix/server/api/routers/v1/experiment_evaluations.py +0 -66
- phoenix/server/api/routers/v1/experiment_runs.py +0 -108
- phoenix/server/api/routers/v1/experiments.py +0 -174
- phoenix/server/api/types/AnnotatorKind.py +0 -10
- phoenix/server/api/types/CreateDatasetPayload.py +0 -8
- phoenix/server/api/types/DatasetExample.py +0 -85
- phoenix/server/api/types/DatasetExampleRevision.py +0 -34
- phoenix/server/api/types/DatasetVersion.py +0 -14
- phoenix/server/api/types/ExampleRevisionInterface.py +0 -14
- phoenix/server/api/types/Experiment.py +0 -140
- phoenix/server/api/types/ExperimentAnnotationSummary.py +0 -13
- phoenix/server/api/types/ExperimentComparison.py +0 -19
- phoenix/server/api/types/ExperimentRun.py +0 -91
- phoenix/server/api/types/ExperimentRunAnnotation.py +0 -57
- phoenix/server/api/types/Inferences.py +0 -80
- phoenix/server/api/types/InferencesRole.py +0 -23
- phoenix/utilities/json.py +0 -61
- phoenix/utilities/re.py +0 -50
- {arize_phoenix-4.4.4rc5.dist-info → arize_phoenix-4.5.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-4.4.4rc5.dist-info → arize_phoenix-4.5.0.dist-info}/licenses/LICENSE +0 -0
- /phoenix/server/api/{helpers/__init__.py → helpers.py} +0 -0
phoenix/core/model_schema.py
CHANGED
|
@@ -48,7 +48,7 @@ from pandas.core.dtypes.common import (
|
|
|
48
48
|
from typing_extensions import TypeAlias, TypeGuard
|
|
49
49
|
from wrapt import ObjectProxy
|
|
50
50
|
|
|
51
|
-
from phoenix.config import
|
|
51
|
+
from phoenix.config import GENERATED_DATASET_NAME_PREFIX
|
|
52
52
|
from phoenix.datetime_utils import floor_to_minute
|
|
53
53
|
|
|
54
54
|
|
|
@@ -185,7 +185,7 @@ class RetrievalEmbedding(Embedding):
|
|
|
185
185
|
yield value
|
|
186
186
|
|
|
187
187
|
|
|
188
|
-
class
|
|
188
|
+
class DatasetRole(Enum):
|
|
189
189
|
"""A dataframe's role in a Model: primary or reference (as
|
|
190
190
|
baseline for drift).
|
|
191
191
|
"""
|
|
@@ -194,8 +194,8 @@ class InferencesRole(Enum):
|
|
|
194
194
|
REFERENCE = auto()
|
|
195
195
|
|
|
196
196
|
|
|
197
|
-
PRIMARY =
|
|
198
|
-
REFERENCE =
|
|
197
|
+
PRIMARY = DatasetRole.PRIMARY
|
|
198
|
+
REFERENCE = DatasetRole.REFERENCE
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
@dataclass(frozen=True, repr=False, eq=False)
|
|
@@ -381,7 +381,7 @@ class Dimension(Column, ABC):
|
|
|
381
381
|
# But we really want the role to be specified for a Dimension.
|
|
382
382
|
raise ValueError("role must be assigned")
|
|
383
383
|
|
|
384
|
-
def __getitem__(self, df_role:
|
|
384
|
+
def __getitem__(self, df_role: DatasetRole) -> "pd.Series[Any]":
|
|
385
385
|
if self._model is None:
|
|
386
386
|
return pd.Series(dtype=object)
|
|
387
387
|
model = cast(Model, self._model)
|
|
@@ -416,7 +416,7 @@ class ScalarDimension(Dimension):
|
|
|
416
416
|
if self._model is None or self.data_type is CONTINUOUS:
|
|
417
417
|
return ()
|
|
418
418
|
model = cast(Model, self._model)
|
|
419
|
-
return model.
|
|
419
|
+
return model.dimension_categories_from_all_datasets(self.name)
|
|
420
420
|
|
|
421
421
|
|
|
422
422
|
@dataclass(frozen=True)
|
|
@@ -582,7 +582,7 @@ class EventId(NamedTuple):
|
|
|
582
582
|
"""Identifies an event."""
|
|
583
583
|
|
|
584
584
|
row_id: int = 0
|
|
585
|
-
|
|
585
|
+
dataset_id: DatasetRole = PRIMARY
|
|
586
586
|
|
|
587
587
|
def __str__(self) -> str:
|
|
588
588
|
return ":".join(map(str, self))
|
|
@@ -625,7 +625,7 @@ class Events(ModelData):
|
|
|
625
625
|
self,
|
|
626
626
|
df: pd.DataFrame,
|
|
627
627
|
/,
|
|
628
|
-
role:
|
|
628
|
+
role: DatasetRole,
|
|
629
629
|
**kwargs: Any,
|
|
630
630
|
) -> None:
|
|
631
631
|
super().__init__(df, **kwargs)
|
|
@@ -676,7 +676,7 @@ class Events(ModelData):
|
|
|
676
676
|
return super().__getitem__(key)
|
|
677
677
|
|
|
678
678
|
|
|
679
|
-
class
|
|
679
|
+
class Dataset(Events):
|
|
680
680
|
"""pd.DataFrame wrapped with extra functions and metadata."""
|
|
681
681
|
|
|
682
682
|
def __init__(
|
|
@@ -701,13 +701,13 @@ class Inferences(Events):
|
|
|
701
701
|
friendly. Falls back to the role of the dataset if no name is provided.
|
|
702
702
|
"""
|
|
703
703
|
ds_name = self._self_name
|
|
704
|
-
if ds_name.startswith(
|
|
704
|
+
if ds_name.startswith(GENERATED_DATASET_NAME_PREFIX):
|
|
705
705
|
# The generated names are UUIDs so use the role as the name
|
|
706
|
-
return "primary" if self.role is
|
|
706
|
+
return "primary" if self.role is DatasetRole.PRIMARY else "reference"
|
|
707
707
|
return ds_name
|
|
708
708
|
|
|
709
709
|
@property
|
|
710
|
-
def role(self) ->
|
|
710
|
+
def role(self) -> DatasetRole:
|
|
711
711
|
return self._self_role
|
|
712
712
|
|
|
713
713
|
@property
|
|
@@ -746,14 +746,14 @@ class Model:
|
|
|
746
746
|
a column of NaNs.
|
|
747
747
|
"""
|
|
748
748
|
|
|
749
|
-
|
|
749
|
+
_datasets: Dict[DatasetRole, Dataset]
|
|
750
750
|
_dimensions: Dict[Name, Dimension]
|
|
751
751
|
_dim_names_by_role: Dict[DimensionRole, List[Name]]
|
|
752
|
-
_original_columns_by_role: Dict[
|
|
752
|
+
_original_columns_by_role: Dict[DatasetRole, "pd.Index[Any]"]
|
|
753
753
|
_default_timestamps_factory: _ConstantValueSeriesFactory
|
|
754
754
|
_nan_series_factory: _ConstantValueSeriesFactory
|
|
755
|
-
|
|
756
|
-
|
|
755
|
+
_dimension_categories_from_all_datasets: _Cache[Name, Tuple[str, ...]]
|
|
756
|
+
_dimension_min_max_from_all_datasets: _Cache[Name, Tuple[float, float]]
|
|
757
757
|
|
|
758
758
|
def __init__(
|
|
759
759
|
self,
|
|
@@ -769,12 +769,12 @@ class Model:
|
|
|
769
769
|
# memoization
|
|
770
770
|
object.__setattr__(
|
|
771
771
|
self,
|
|
772
|
-
"
|
|
772
|
+
"_dimension_categories_from_all_datasets",
|
|
773
773
|
_Cache[Name, "pd.Series[Any]"](),
|
|
774
774
|
)
|
|
775
775
|
object.__setattr__(
|
|
776
776
|
self,
|
|
777
|
-
"
|
|
777
|
+
"_dimension_min_max_from_all_datasets",
|
|
778
778
|
_Cache[Name, Tuple[float, float]](),
|
|
779
779
|
)
|
|
780
780
|
|
|
@@ -785,21 +785,21 @@ class Model:
|
|
|
785
785
|
str_col_dfs = _coerce_str_column_names(dfs)
|
|
786
786
|
padded_dfs = _add_padding(str_col_dfs, pd.DataFrame)
|
|
787
787
|
padded_df_names = _add_padding(df_names, _rand_str)
|
|
788
|
-
|
|
789
|
-
self.
|
|
790
|
-
zip(padded_dfs, padded_df_names,
|
|
788
|
+
datasets = starmap(
|
|
789
|
+
self._new_dataset,
|
|
790
|
+
zip(padded_dfs, padded_df_names, DatasetRole),
|
|
791
791
|
)
|
|
792
|
-
# Store
|
|
792
|
+
# Store datasets by role.
|
|
793
793
|
object.__setattr__(
|
|
794
794
|
self,
|
|
795
|
-
"
|
|
796
|
-
{
|
|
795
|
+
"_datasets",
|
|
796
|
+
{dataset.role: dataset for dataset in datasets},
|
|
797
797
|
)
|
|
798
798
|
# Preserve originals, useful for exporting.
|
|
799
799
|
object.__setattr__(
|
|
800
800
|
self,
|
|
801
801
|
"_original_columns_by_role",
|
|
802
|
-
{role:
|
|
802
|
+
{role: dataset.columns for role, dataset in self._datasets.items()},
|
|
803
803
|
)
|
|
804
804
|
|
|
805
805
|
object.__setattr__(
|
|
@@ -828,7 +828,7 @@ class Model:
|
|
|
828
828
|
(name, self._new_dimension(name, role=FEATURE))
|
|
829
829
|
for name in _get_omitted_column_names(
|
|
830
830
|
self._dimensions.values(),
|
|
831
|
-
self.
|
|
831
|
+
self._datasets.values(),
|
|
832
832
|
)
|
|
833
833
|
)
|
|
834
834
|
|
|
@@ -849,7 +849,7 @@ class Model:
|
|
|
849
849
|
data_type=(
|
|
850
850
|
_guess_data_type(
|
|
851
851
|
dataset.loc[:, dim.name]
|
|
852
|
-
for dataset in self.
|
|
852
|
+
for dataset in self._datasets.values()
|
|
853
853
|
if dim.name in dataset.columns
|
|
854
854
|
)
|
|
855
855
|
),
|
|
@@ -859,9 +859,9 @@ class Model:
|
|
|
859
859
|
# Add TIMESTAMP if missing.
|
|
860
860
|
# If needed, normalize the timestamps values.
|
|
861
861
|
# If needed, sort the dataframes by time.
|
|
862
|
-
for
|
|
862
|
+
for dataset_role, dataset in list(self._datasets.items()):
|
|
863
863
|
df = dataset.__wrapped__
|
|
864
|
-
df_original_columns = self._original_columns_by_role[
|
|
864
|
+
df_original_columns = self._original_columns_by_role[dataset_role]
|
|
865
865
|
|
|
866
866
|
# PREDICTION_ID
|
|
867
867
|
dim_pred_id = self._dimensions.get(
|
|
@@ -897,20 +897,20 @@ class Model:
|
|
|
897
897
|
df = df.set_index(dim_time.name, drop=False)
|
|
898
898
|
|
|
899
899
|
# Update dataset since its dataframe may have changed.
|
|
900
|
-
self.
|
|
901
|
-
df, name=dataset.name, role=
|
|
900
|
+
self._datasets[dataset_role] = self._new_dataset(
|
|
901
|
+
df, name=dataset.name, role=dataset_role
|
|
902
902
|
)
|
|
903
903
|
|
|
904
904
|
@cached_property
|
|
905
905
|
def is_empty(self) -> bool:
|
|
906
906
|
"""Returns True if the model has no data."""
|
|
907
|
-
return not any(map(len, self.
|
|
907
|
+
return not any(map(len, self._datasets.values()))
|
|
908
908
|
|
|
909
909
|
def export_rows_as_parquet_file(
|
|
910
910
|
self,
|
|
911
|
-
row_numbers: Mapping[
|
|
911
|
+
row_numbers: Mapping[DatasetRole, Iterable[int]],
|
|
912
912
|
parquet_file: BinaryIO,
|
|
913
|
-
cluster_ids: Optional[Mapping[
|
|
913
|
+
cluster_ids: Optional[Mapping[DatasetRole, Mapping[int, str]]] = None,
|
|
914
914
|
) -> None:
|
|
915
915
|
"""
|
|
916
916
|
Given row numbers, exports dataframe subset into parquet file.
|
|
@@ -921,31 +921,29 @@ class Model:
|
|
|
921
921
|
|
|
922
922
|
Parameters
|
|
923
923
|
----------
|
|
924
|
-
row_numbers: Mapping[
|
|
924
|
+
row_numbers: Mapping[DatasetRole, Iterable[int]]
|
|
925
925
|
mapping of dataset role to list of row numbers
|
|
926
926
|
parquet_file: file handle
|
|
927
927
|
output parquet file handle
|
|
928
|
-
cluster_ids: Optional[Mapping[
|
|
929
|
-
mapping of
|
|
928
|
+
cluster_ids: Optional[Mapping[DatasetRole, Mapping[int, str]]]
|
|
929
|
+
mapping of dataset role to mapping of row number to cluster id.
|
|
930
930
|
If cluster_ids is non-empty, a new column is inserted to the
|
|
931
931
|
dataframe containing the cluster IDs of each row in the exported
|
|
932
932
|
data. The name of the added column name is `__phoenix_cluster_id__`.
|
|
933
933
|
"""
|
|
934
934
|
export_dataframes = [pd.DataFrame()]
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
for inferences_role, numbers in row_numbers.items():
|
|
939
|
-
df = self._inference_sets[inferences_role]
|
|
935
|
+
model_has_multiple_datasets = sum(not df.empty for df in self._datasets.values()) > 1
|
|
936
|
+
for dataset_role, numbers in row_numbers.items():
|
|
937
|
+
df = self._datasets[dataset_role]
|
|
940
938
|
columns = [
|
|
941
939
|
df.columns.get_loc(column_name)
|
|
942
|
-
for column_name in self._original_columns_by_role[
|
|
940
|
+
for column_name in self._original_columns_by_role[dataset_role]
|
|
943
941
|
]
|
|
944
942
|
rows = pd.Series(sorted(set(numbers)))
|
|
945
943
|
filtered_df = df.iloc[rows, columns].reset_index(drop=True)
|
|
946
|
-
if
|
|
944
|
+
if model_has_multiple_datasets:
|
|
947
945
|
filtered_df["__phoenix_dataset_name__"] = df.display_name
|
|
948
|
-
if cluster_ids and (ids := cluster_ids.get(
|
|
946
|
+
if cluster_ids and (ids := cluster_ids.get(dataset_role)):
|
|
949
947
|
filtered_df["__phoenix_cluster_id__"] = rows.apply(ids.get)
|
|
950
948
|
export_dataframes.append(filtered_df)
|
|
951
949
|
pd.concat(export_dataframes).to_parquet(
|
|
@@ -979,24 +977,24 @@ class Model:
|
|
|
979
977
|
if not dim.is_dummy and isinstance(dim, EmbeddingDimension)
|
|
980
978
|
)
|
|
981
979
|
|
|
982
|
-
def
|
|
980
|
+
def dimension_categories_from_all_datasets(
|
|
983
981
|
self,
|
|
984
982
|
dimension_name: Name,
|
|
985
983
|
) -> Tuple[str, ...]:
|
|
986
984
|
dim = self[dimension_name]
|
|
987
985
|
if dim.data_type is CONTINUOUS:
|
|
988
986
|
return cast(Tuple[str, ...], ())
|
|
989
|
-
with self.
|
|
987
|
+
with self._dimension_categories_from_all_datasets() as cache:
|
|
990
988
|
try:
|
|
991
989
|
return cache[dimension_name]
|
|
992
990
|
except KeyError:
|
|
993
991
|
pass
|
|
994
992
|
categories_by_dataset = (
|
|
995
|
-
pd.Series(dim[role].unique()).dropna().astype(str) for role in
|
|
993
|
+
pd.Series(dim[role].unique()).dropna().astype(str) for role in DatasetRole
|
|
996
994
|
)
|
|
997
995
|
all_values_combined = chain.from_iterable(categories_by_dataset)
|
|
998
996
|
ans = tuple(np.sort(pd.Series(all_values_combined).unique()))
|
|
999
|
-
with self.
|
|
997
|
+
with self._dimension_categories_from_all_datasets() as cache:
|
|
1000
998
|
cache[dimension_name] = ans
|
|
1001
999
|
return ans
|
|
1002
1000
|
|
|
@@ -1007,24 +1005,24 @@ class Model:
|
|
|
1007
1005
|
dim = self[dimension_name]
|
|
1008
1006
|
if dim.data_type is not CONTINUOUS:
|
|
1009
1007
|
return (np.nan, np.nan)
|
|
1010
|
-
with self.
|
|
1008
|
+
with self._dimension_min_max_from_all_datasets() as cache:
|
|
1011
1009
|
try:
|
|
1012
1010
|
return cache[dimension_name]
|
|
1013
1011
|
except KeyError:
|
|
1014
1012
|
pass
|
|
1015
|
-
min_max_by_df = (_agg_min_max(dim[df_role]) for df_role in
|
|
1013
|
+
min_max_by_df = (_agg_min_max(dim[df_role]) for df_role in DatasetRole)
|
|
1016
1014
|
all_values_combined = chain.from_iterable(min_max_by_df)
|
|
1017
1015
|
min_max = _agg_min_max(pd.Series(all_values_combined))
|
|
1018
1016
|
ans = (min_max.min(), min_max.max())
|
|
1019
|
-
with self.
|
|
1017
|
+
with self._dimension_min_max_from_all_datasets() as cache:
|
|
1020
1018
|
cache[dimension_name] = ans
|
|
1021
1019
|
return ans
|
|
1022
1020
|
|
|
1023
1021
|
@overload
|
|
1024
|
-
def __getitem__(self, key: Type[
|
|
1022
|
+
def __getitem__(self, key: Type[Dataset]) -> Iterator[Dataset]: ...
|
|
1025
1023
|
|
|
1026
1024
|
@overload
|
|
1027
|
-
def __getitem__(self, key:
|
|
1025
|
+
def __getitem__(self, key: DatasetRole) -> Dataset: ...
|
|
1028
1026
|
|
|
1029
1027
|
@overload
|
|
1030
1028
|
def __getitem__(self, key: ColumnKey) -> Dimension: ...
|
|
@@ -1051,10 +1049,10 @@ class Model:
|
|
|
1051
1049
|
) -> Iterator[Dimension]: ...
|
|
1052
1050
|
|
|
1053
1051
|
def __getitem__(self, key: Any) -> Any:
|
|
1054
|
-
if key is
|
|
1055
|
-
return self.
|
|
1056
|
-
if isinstance(key,
|
|
1057
|
-
return self.
|
|
1052
|
+
if key is Dataset:
|
|
1053
|
+
return self._datasets.values()
|
|
1054
|
+
if isinstance(key, DatasetRole):
|
|
1055
|
+
return self._datasets[key]
|
|
1058
1056
|
if _is_column_key(key):
|
|
1059
1057
|
return self._get_dim(key)
|
|
1060
1058
|
if _is_multi_dimension_key(key):
|
|
@@ -1154,17 +1152,17 @@ class Model:
|
|
|
1154
1152
|
)
|
|
1155
1153
|
raise ValueError(f"invalid argument: {repr(obj)}")
|
|
1156
1154
|
|
|
1157
|
-
def
|
|
1155
|
+
def _new_dataset(
|
|
1158
1156
|
self,
|
|
1159
1157
|
df: pd.DataFrame,
|
|
1160
1158
|
/,
|
|
1161
1159
|
name: str,
|
|
1162
|
-
role:
|
|
1163
|
-
) ->
|
|
1164
|
-
"""Creates a new
|
|
1160
|
+
role: DatasetRole,
|
|
1161
|
+
) -> Dataset:
|
|
1162
|
+
"""Creates a new Dataset, setting the model weak reference to the
|
|
1165
1163
|
`self` Model instance.
|
|
1166
1164
|
"""
|
|
1167
|
-
return
|
|
1165
|
+
return Dataset(df, name=name, role=role, _model=proxy(self))
|
|
1168
1166
|
|
|
1169
1167
|
|
|
1170
1168
|
@dataclass(frozen=True)
|
|
@@ -1346,7 +1344,7 @@ def _series_uuid(length: int) -> "pd.Series[str]":
|
|
|
1346
1344
|
|
|
1347
1345
|
|
|
1348
1346
|
def _raise_if_too_many_dataframes(given: int) -> None:
|
|
1349
|
-
limit = len(
|
|
1347
|
+
limit = len(DatasetRole)
|
|
1350
1348
|
if not 0 < given <= limit:
|
|
1351
1349
|
raise ValueError(f"expected between 1 to {limit} dataframes, but {given} were given")
|
|
1352
1350
|
|
|
@@ -10,21 +10,21 @@ from phoenix import EmbeddingColumnNames, Inferences
|
|
|
10
10
|
from phoenix.core.model import _get_embedding_dimensions
|
|
11
11
|
from phoenix.core.model_schema import Embedding, Model, RetrievalEmbedding, Schema
|
|
12
12
|
from phoenix.inferences.schema import RetrievalEmbeddingColumnNames
|
|
13
|
-
from phoenix.inferences.schema import Schema as
|
|
13
|
+
from phoenix.inferences.schema import Schema as DatasetSchema
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
DatasetName: TypeAlias = str
|
|
16
16
|
ColumnName: TypeAlias = str
|
|
17
17
|
DisplayName: TypeAlias = str
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def create_model_from_datasets(*datasets: Optional[Inferences]) -> Model:
|
|
21
21
|
# TODO: move this validation into model_schema.Model.
|
|
22
|
-
if len(
|
|
22
|
+
if len(datasets) > 1 and datasets[0] is not None:
|
|
23
23
|
# Check that for each embedding dimension all vectors
|
|
24
|
-
# have the same length between
|
|
25
|
-
_ = _get_embedding_dimensions(
|
|
24
|
+
# have the same length between datasets.
|
|
25
|
+
_ = _get_embedding_dimensions(datasets[0], datasets[1])
|
|
26
26
|
|
|
27
|
-
named_dataframes: List[Tuple[
|
|
27
|
+
named_dataframes: List[Tuple[DatasetName, pd.DataFrame]] = []
|
|
28
28
|
prediction_ids: List[ColumnName] = []
|
|
29
29
|
timestamps: List[ColumnName] = []
|
|
30
30
|
prediction_labels: List[ColumnName] = []
|
|
@@ -37,35 +37,33 @@ def create_model_from_inferences(*inference_sets: Optional[Inferences]) -> Model
|
|
|
37
37
|
prompts: List[EmbeddingColumnNames] = []
|
|
38
38
|
responses: List[Union[str, EmbeddingColumnNames]] = []
|
|
39
39
|
|
|
40
|
-
for
|
|
41
|
-
df =
|
|
40
|
+
for dataset in filter(_is_dataset, datasets):
|
|
41
|
+
df = dataset.dataframe
|
|
42
42
|
# Coerce string column names at run time.
|
|
43
43
|
df = df.set_axis(
|
|
44
44
|
map(str, df.columns),
|
|
45
45
|
axis=1,
|
|
46
46
|
)
|
|
47
|
-
named_dataframes.append((
|
|
48
|
-
|
|
49
|
-
inferences.schema if inferences.schema is not None else InferencesSchema()
|
|
50
|
-
)
|
|
47
|
+
named_dataframes.append((dataset.name, df))
|
|
48
|
+
dataset_schema = dataset.schema if dataset.schema is not None else DatasetSchema()
|
|
51
49
|
for display_name, embedding in (
|
|
52
|
-
|
|
50
|
+
dataset_schema.embedding_feature_column_names or {}
|
|
53
51
|
).items():
|
|
54
52
|
if display_name not in embeddings:
|
|
55
53
|
embeddings[display_name] = embedding
|
|
56
|
-
if
|
|
57
|
-
prompts.append(
|
|
58
|
-
if
|
|
59
|
-
responses.append(
|
|
54
|
+
if dataset_schema.prompt_column_names is not None:
|
|
55
|
+
prompts.append(dataset_schema.prompt_column_names)
|
|
56
|
+
if dataset_schema.response_column_names is not None:
|
|
57
|
+
responses.append(dataset_schema.response_column_names)
|
|
60
58
|
for source, sink in (
|
|
61
|
-
([
|
|
62
|
-
([
|
|
63
|
-
([
|
|
64
|
-
([
|
|
65
|
-
([
|
|
66
|
-
([
|
|
67
|
-
(
|
|
68
|
-
(
|
|
59
|
+
([dataset_schema.prediction_id_column_name], prediction_ids),
|
|
60
|
+
([dataset_schema.timestamp_column_name], timestamps),
|
|
61
|
+
([dataset_schema.prediction_label_column_name], prediction_labels),
|
|
62
|
+
([dataset_schema.prediction_score_column_name], prediction_scores),
|
|
63
|
+
([dataset_schema.actual_label_column_name], actual_labels),
|
|
64
|
+
([dataset_schema.actual_score_column_name], actual_scores),
|
|
65
|
+
(dataset_schema.feature_column_names or (), features),
|
|
66
|
+
(dataset_schema.tag_column_names or (), tags),
|
|
69
67
|
):
|
|
70
68
|
# Coerce None to "" to simplify type checks.
|
|
71
69
|
sink.extend(map(lambda s: "" if s is None else str(s), source))
|
|
@@ -134,7 +132,7 @@ def create_model_from_inferences(*inference_sets: Optional[Inferences]) -> Model
|
|
|
134
132
|
)
|
|
135
133
|
|
|
136
134
|
|
|
137
|
-
def
|
|
135
|
+
def _is_dataset(obj: Optional[Inferences]) -> TypeGuard[Inferences]:
|
|
138
136
|
return type(obj) is Inferences
|
|
139
137
|
|
|
140
138
|
|
phoenix/db/bulk_inserter.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
-
from asyncio import Queue
|
|
4
3
|
from dataclasses import dataclass, field
|
|
5
4
|
from datetime import datetime, timezone
|
|
6
5
|
from itertools import islice
|
|
@@ -15,7 +14,6 @@ from typing import (
|
|
|
15
14
|
Optional,
|
|
16
15
|
Set,
|
|
17
16
|
Tuple,
|
|
18
|
-
cast,
|
|
19
17
|
)
|
|
20
18
|
|
|
21
19
|
from cachetools import LRUCache
|
|
@@ -24,11 +22,10 @@ from typing_extensions import TypeAlias
|
|
|
24
22
|
|
|
25
23
|
import phoenix.trace.v1 as pb
|
|
26
24
|
from phoenix.db.insertion.evaluation import (
|
|
27
|
-
|
|
25
|
+
EvaluationInsertionResult,
|
|
28
26
|
InsertEvaluationError,
|
|
29
27
|
insert_evaluation,
|
|
30
28
|
)
|
|
31
|
-
from phoenix.db.insertion.helpers import DataManipulation, DataManipulationEvent
|
|
32
29
|
from phoenix.db.insertion.span import SpanInsertionEvent, insert_span
|
|
33
30
|
from phoenix.server.api.dataloaders import CacheForDataLoaders
|
|
34
31
|
from phoenix.trace.schemas import Span
|
|
@@ -49,29 +46,23 @@ class BulkInserter:
|
|
|
49
46
|
db: Callable[[], AsyncContextManager[AsyncSession]],
|
|
50
47
|
*,
|
|
51
48
|
cache_for_dataloaders: Optional[CacheForDataLoaders] = None,
|
|
52
|
-
initial_batch_of_operations: Iterable[DataManipulation] = (),
|
|
53
49
|
initial_batch_of_spans: Optional[Iterable[Tuple[Span, str]]] = None,
|
|
54
50
|
initial_batch_of_evaluations: Optional[Iterable[pb.Evaluation]] = None,
|
|
55
51
|
sleep: float = 0.1,
|
|
56
|
-
|
|
57
|
-
max_queue_size: int = 1000,
|
|
52
|
+
max_num_per_transaction: int = 1000,
|
|
58
53
|
enable_prometheus: bool = False,
|
|
59
54
|
) -> None:
|
|
60
55
|
"""
|
|
61
56
|
:param db: A function to initiate a new database session.
|
|
62
57
|
:param initial_batch_of_spans: Initial batch of spans to insert.
|
|
63
58
|
:param sleep: The time to sleep between bulk insertions
|
|
64
|
-
:param
|
|
65
|
-
|
|
66
|
-
:param max_queue_size: The maximum length of the operations queue.
|
|
67
|
-
:param enable_prometheus: Whether Prometheus is enabled.
|
|
59
|
+
:param max_num_per_transaction: The maximum number of items to insert in a single
|
|
60
|
+
transaction. Multiple transactions will be used if there are more items in the batch.
|
|
68
61
|
"""
|
|
69
62
|
self._db = db
|
|
70
63
|
self._running = False
|
|
71
64
|
self._sleep = sleep
|
|
72
|
-
self.
|
|
73
|
-
self._operations: Optional[Queue[DataManipulation]] = None
|
|
74
|
-
self._max_queue_size = max_queue_size
|
|
65
|
+
self._max_num_per_transaction = max_num_per_transaction
|
|
75
66
|
self._spans: List[Tuple[Span, str]] = (
|
|
76
67
|
[] if initial_batch_of_spans is None else list(initial_batch_of_spans)
|
|
77
68
|
)
|
|
@@ -90,58 +81,27 @@ class BulkInserter:
|
|
|
90
81
|
|
|
91
82
|
async def __aenter__(
|
|
92
83
|
self,
|
|
93
|
-
) -> Tuple[
|
|
94
|
-
Callable[[Span, str], Awaitable[None]],
|
|
95
|
-
Callable[[pb.Evaluation], Awaitable[None]],
|
|
96
|
-
Callable[[DataManipulation], None],
|
|
97
|
-
]:
|
|
84
|
+
) -> Tuple[Callable[[Span, str], Awaitable[None]], Callable[[pb.Evaluation], Awaitable[None]]]:
|
|
98
85
|
self._running = True
|
|
99
|
-
self._operations = Queue(maxsize=self._max_queue_size)
|
|
100
86
|
self._task = asyncio.create_task(self._bulk_insert())
|
|
101
|
-
return
|
|
102
|
-
self._queue_span,
|
|
103
|
-
self._queue_evaluation,
|
|
104
|
-
self._enqueue_operation,
|
|
105
|
-
)
|
|
87
|
+
return self._queue_span, self._queue_evaluation
|
|
106
88
|
|
|
107
89
|
async def __aexit__(self, *args: Any) -> None:
|
|
108
|
-
self._operations = None
|
|
109
90
|
self._running = False
|
|
110
91
|
|
|
111
|
-
def _enqueue_operation(self, operation: DataManipulation) -> None:
|
|
112
|
-
cast("Queue[DataManipulation]", self._operations).put_nowait(operation)
|
|
113
|
-
|
|
114
92
|
async def _queue_span(self, span: Span, project_name: str) -> None:
|
|
115
93
|
self._spans.append((span, project_name))
|
|
116
94
|
|
|
117
95
|
async def _queue_evaluation(self, evaluation: pb.Evaluation) -> None:
|
|
118
96
|
self._evaluations.append(evaluation)
|
|
119
97
|
|
|
120
|
-
async def _process_events(self, events: Iterable[Optional[DataManipulationEvent]]) -> None: ...
|
|
121
|
-
|
|
122
98
|
async def _bulk_insert(self) -> None:
|
|
123
|
-
assert isinstance(self._operations, Queue)
|
|
124
99
|
spans_buffer, evaluations_buffer = None, None
|
|
125
100
|
# start first insert immediately if the inserter has not run recently
|
|
126
|
-
while self.
|
|
127
|
-
if
|
|
101
|
+
while self._spans or self._evaluations or self._running:
|
|
102
|
+
if not (self._spans or self._evaluations):
|
|
128
103
|
await asyncio.sleep(self._sleep)
|
|
129
104
|
continue
|
|
130
|
-
ops_remaining, events = self._max_ops_per_transaction, []
|
|
131
|
-
async with self._db() as session:
|
|
132
|
-
while ops_remaining and not self._operations.empty():
|
|
133
|
-
ops_remaining -= 1
|
|
134
|
-
op = await self._operations.get()
|
|
135
|
-
try:
|
|
136
|
-
async with session.begin_nested():
|
|
137
|
-
events.append(await op(session))
|
|
138
|
-
except Exception as e:
|
|
139
|
-
if self._enable_prometheus:
|
|
140
|
-
from phoenix.server.prometheus import BULK_LOADER_EXCEPTIONS
|
|
141
|
-
|
|
142
|
-
BULK_LOADER_EXCEPTIONS.inc()
|
|
143
|
-
logger.exception(str(e))
|
|
144
|
-
await self._process_events(events)
|
|
145
105
|
# It's important to grab the buffers at the same time so there's
|
|
146
106
|
# no race condition, since an eval insertion will fail if the span
|
|
147
107
|
# it references doesn't exist. Grabbing the eval buffer later may
|
|
@@ -170,11 +130,11 @@ class BulkInserter:
|
|
|
170
130
|
|
|
171
131
|
async def _insert_spans(self, spans: List[Tuple[Span, str]]) -> TransactionResult:
|
|
172
132
|
transaction_result = TransactionResult()
|
|
173
|
-
for i in range(0, len(spans), self.
|
|
133
|
+
for i in range(0, len(spans), self._max_num_per_transaction):
|
|
174
134
|
try:
|
|
175
135
|
start = perf_counter()
|
|
176
136
|
async with self._db() as session:
|
|
177
|
-
for span, project_name in islice(spans, i, i + self.
|
|
137
|
+
for span, project_name in islice(spans, i, i + self._max_num_per_transaction):
|
|
178
138
|
if self._enable_prometheus:
|
|
179
139
|
from phoenix.server.prometheus import BULK_LOADER_SPAN_INSERTIONS
|
|
180
140
|
|
|
@@ -209,16 +169,16 @@ class BulkInserter:
|
|
|
209
169
|
|
|
210
170
|
async def _insert_evaluations(self, evaluations: List[pb.Evaluation]) -> TransactionResult:
|
|
211
171
|
transaction_result = TransactionResult()
|
|
212
|
-
for i in range(0, len(evaluations), self.
|
|
172
|
+
for i in range(0, len(evaluations), self._max_num_per_transaction):
|
|
213
173
|
try:
|
|
214
174
|
start = perf_counter()
|
|
215
175
|
async with self._db() as session:
|
|
216
|
-
for evaluation in islice(evaluations, i, i + self.
|
|
176
|
+
for evaluation in islice(evaluations, i, i + self._max_num_per_transaction):
|
|
217
177
|
if self._enable_prometheus:
|
|
218
178
|
from phoenix.server.prometheus import BULK_LOADER_EVALUATION_INSERTIONS
|
|
219
179
|
|
|
220
180
|
BULK_LOADER_EVALUATION_INSERTIONS.inc()
|
|
221
|
-
result: Optional[
|
|
181
|
+
result: Optional[EvaluationInsertionResult] = None
|
|
222
182
|
try:
|
|
223
183
|
async with session.begin_nested():
|
|
224
184
|
result = await insert_evaluation(session, evaluation)
|
|
@@ -15,24 +15,24 @@ class InsertEvaluationError(PhoenixException):
|
|
|
15
15
|
pass
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class
|
|
18
|
+
class EvaluationInsertionResult(NamedTuple):
|
|
19
19
|
project_rowid: int
|
|
20
20
|
evaluation_name: str
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
class SpanEvaluationInsertionEvent(
|
|
23
|
+
class SpanEvaluationInsertionEvent(EvaluationInsertionResult): ...
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
class TraceEvaluationInsertionEvent(
|
|
26
|
+
class TraceEvaluationInsertionEvent(EvaluationInsertionResult): ...
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
class DocumentEvaluationInsertionEvent(
|
|
29
|
+
class DocumentEvaluationInsertionEvent(EvaluationInsertionResult): ...
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
async def insert_evaluation(
|
|
33
33
|
session: AsyncSession,
|
|
34
34
|
evaluation: pb.Evaluation,
|
|
35
|
-
) -> Optional[
|
|
35
|
+
) -> Optional[EvaluationInsertionResult]:
|
|
36
36
|
evaluation_name = evaluation.name
|
|
37
37
|
result = evaluation.result
|
|
38
38
|
label = result.label.value if result.HasField("label") else None
|
|
@@ -160,7 +160,7 @@ async def _insert_document_evaluation(
|
|
|
160
160
|
label: Optional[str],
|
|
161
161
|
score: Optional[float],
|
|
162
162
|
explanation: Optional[str],
|
|
163
|
-
) ->
|
|
163
|
+
) -> EvaluationInsertionResult:
|
|
164
164
|
dialect = SupportedSQLDialect(session.bind.dialect.name)
|
|
165
165
|
stmt = (
|
|
166
166
|
select(
|
phoenix/db/insertion/helpers.py
CHANGED
|
@@ -1,25 +1,14 @@
|
|
|
1
|
-
from abc import ABC
|
|
2
1
|
from enum import Enum, auto
|
|
3
|
-
from typing import Any,
|
|
2
|
+
from typing import Any, Mapping, Optional, Sequence
|
|
4
3
|
|
|
5
4
|
from sqlalchemy import Insert, insert
|
|
6
5
|
from sqlalchemy.dialects.postgresql import insert as insert_postgresql
|
|
7
6
|
from sqlalchemy.dialects.sqlite import insert as insert_sqlite
|
|
8
|
-
from
|
|
9
|
-
from typing_extensions import TypeAlias, assert_never
|
|
7
|
+
from typing_extensions import assert_never
|
|
10
8
|
|
|
11
9
|
from phoenix.db.helpers import SupportedSQLDialect
|
|
12
10
|
|
|
13
11
|
|
|
14
|
-
class DataManipulationEvent(ABC):
|
|
15
|
-
"""
|
|
16
|
-
Execution of DML (Data Manipulation Language) statements.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
DataManipulation: TypeAlias = Callable[[AsyncSession], Awaitable[Optional[DataManipulationEvent]]]
|
|
21
|
-
|
|
22
|
-
|
|
23
12
|
class OnConflict(Enum):
|
|
24
13
|
DO_NOTHING = auto()
|
|
25
14
|
DO_UPDATE = auto()
|