arize-phoenix 12.4.0__py3-none-any.whl → 12.6.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.4.0.dist-info → arize_phoenix-12.6.0.dist-info}/METADATA +1 -1
- {arize_phoenix-12.4.0.dist-info → arize_phoenix-12.6.0.dist-info}/RECORD +62 -60
- phoenix/auth.py +8 -2
- 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/ExperimentRunSort.py +237 -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 +2 -2
- phoenix/server/api/routers/v1/__init__.py +16 -1
- phoenix/server/api/routers/v1/annotation_configs.py +7 -1
- phoenix/server/api/routers/v1/datasets.py +48 -8
- phoenix/server/api/routers/v1/experiment_runs.py +7 -1
- phoenix/server/api/routers/v1/experiments.py +41 -5
- phoenix/server/api/routers/v1/projects.py +3 -31
- phoenix/server/api/routers/v1/users.py +0 -7
- phoenix/server/api/subscriptions.py +3 -3
- phoenix/server/api/types/Dataset.py +95 -6
- phoenix/server/api/types/Experiment.py +60 -25
- phoenix/server/api/types/Project.py +24 -68
- phoenix/server/app.py +2 -0
- phoenix/server/authorization.py +3 -1
- phoenix/server/bearer_auth.py +9 -0
- phoenix/server/jwt_store.py +8 -6
- phoenix/server/static/.vite/manifest.json +44 -44
- phoenix/server/static/assets/{components-BvsExS75.js → components-CboqzKQ9.js} +520 -397
- phoenix/server/static/assets/{index-iq8WDxat.js → index-CYYGI5-x.js} +2 -2
- phoenix/server/static/assets/{pages-Ckg4SLQ9.js → pages-DdlUeKi2.js} +616 -604
- phoenix/server/static/assets/vendor-CQ4tN9P7.js +918 -0
- phoenix/server/static/assets/vendor-arizeai-Cb1ncvYH.js +106 -0
- phoenix/server/static/assets/{vendor-codemirror-1bq_t1Ec.js → vendor-codemirror-CckmKopH.js} +3 -3
- phoenix/server/static/assets/{vendor-recharts-DQ4xfrf4.js → vendor-recharts-BC1ysIKu.js} +1 -1
- phoenix/server/static/assets/{vendor-shiki-GGmcIQxA.js → vendor-shiki-B45T-YxN.js} +1 -1
- phoenix/server/static/assets/vendor-three-BtCyLs1w.js +3840 -0
- phoenix/version.py +1 -1
- phoenix/server/static/assets/vendor-D2eEI-6h.js +0 -914
- phoenix/server/static/assets/vendor-arizeai-kfOei7nf.js +0 -156
- phoenix/server/static/assets/vendor-three-BLWp5bic.js +0 -2998
- {arize_phoenix-12.4.0.dist-info → arize_phoenix-12.6.0.dist-info}/WHEEL +0 -0
- {arize_phoenix-12.4.0.dist-info → arize_phoenix-12.6.0.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-12.4.0.dist-info → arize_phoenix-12.6.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-12.4.0.dist-info → arize_phoenix-12.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import operator
|
|
2
|
+
from enum import Enum, auto
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import strawberry
|
|
6
|
+
from sqlalchemy import ColumnElement, Select, and_, func, literal, or_, select, tuple_
|
|
7
|
+
from sqlalchemy.sql.selectable import NamedFromClause
|
|
8
|
+
from strawberry import Maybe
|
|
9
|
+
from typing_extensions import assert_never
|
|
10
|
+
|
|
11
|
+
from phoenix.db import models
|
|
12
|
+
from phoenix.server.api.types.pagination import (
|
|
13
|
+
Cursor,
|
|
14
|
+
CursorSortColumn,
|
|
15
|
+
CursorSortColumnDataType,
|
|
16
|
+
CursorSortColumnValue,
|
|
17
|
+
)
|
|
18
|
+
from phoenix.server.api.types.SortDir import SortDir
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@strawberry.enum
|
|
22
|
+
class ExperimentRunMetric(Enum):
|
|
23
|
+
latencyMs = auto()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@strawberry.input(one_of=True)
|
|
27
|
+
class ExperimentRunColumn:
|
|
28
|
+
metric: Maybe[ExperimentRunMetric]
|
|
29
|
+
annotation_name: Maybe[str]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@strawberry.input(description="The sort key and direction for experiment run connections")
|
|
33
|
+
class ExperimentRunSort:
|
|
34
|
+
col: ExperimentRunColumn
|
|
35
|
+
dir: SortDir
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_experiment_run_cursor(
|
|
39
|
+
run: models.ExperimentRun, annotation_score: Optional[float], sort: Optional[ExperimentRunSort]
|
|
40
|
+
) -> Cursor:
|
|
41
|
+
sort_column: Optional[CursorSortColumn] = None
|
|
42
|
+
if sort:
|
|
43
|
+
if sort.col.metric:
|
|
44
|
+
metric = sort.col.metric.value
|
|
45
|
+
assert metric is not None
|
|
46
|
+
if metric is ExperimentRunMetric.latencyMs:
|
|
47
|
+
sort_column = CursorSortColumn(
|
|
48
|
+
type=CursorSortColumnDataType.FLOAT,
|
|
49
|
+
value=run.latency_ms,
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
assert_never(metric)
|
|
53
|
+
elif sort.col.annotation_name:
|
|
54
|
+
data_type = (
|
|
55
|
+
CursorSortColumnDataType.FLOAT
|
|
56
|
+
if annotation_score is not None
|
|
57
|
+
else CursorSortColumnDataType.NULL
|
|
58
|
+
)
|
|
59
|
+
sort_column = CursorSortColumn(
|
|
60
|
+
type=data_type,
|
|
61
|
+
value=annotation_score,
|
|
62
|
+
)
|
|
63
|
+
return Cursor(rowid=run.id, sort_column=sort_column)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def add_order_by_and_page_start_to_query(
|
|
67
|
+
query: Select[Any],
|
|
68
|
+
sort: Optional[ExperimentRunSort],
|
|
69
|
+
experiment_rowid: int,
|
|
70
|
+
after_experiment_run_rowid: Optional[int],
|
|
71
|
+
after_sort_column_value: Optional[CursorSortColumnValue] = None,
|
|
72
|
+
) -> Select[Any]:
|
|
73
|
+
mean_annotation_scores: Optional[NamedFromClause] = None
|
|
74
|
+
if sort and sort.col.annotation_name:
|
|
75
|
+
annotation_name = sort.col.annotation_name.value
|
|
76
|
+
assert annotation_name is not None
|
|
77
|
+
mean_annotation_scores = _get_mean_annotation_scores_subquery(annotation_name)
|
|
78
|
+
order_by_columns = _get_order_by_columns(
|
|
79
|
+
sort=sort, experiment_rowid=experiment_rowid, mean_annotation_scores=mean_annotation_scores
|
|
80
|
+
)
|
|
81
|
+
query = query.order_by(*order_by_columns)
|
|
82
|
+
if after_experiment_run_rowid is not None:
|
|
83
|
+
query = _add_after_expression(
|
|
84
|
+
query=query,
|
|
85
|
+
sort=sort,
|
|
86
|
+
experiment_run_rowid=after_experiment_run_rowid,
|
|
87
|
+
after_sort_column_value=after_sort_column_value,
|
|
88
|
+
mean_annotation_scores=mean_annotation_scores,
|
|
89
|
+
)
|
|
90
|
+
query = _add_joins_and_selects_to_query(
|
|
91
|
+
query=query,
|
|
92
|
+
sort=sort,
|
|
93
|
+
mean_annotation_scores=mean_annotation_scores,
|
|
94
|
+
)
|
|
95
|
+
return query
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _get_order_by_columns(
|
|
99
|
+
sort: Optional[ExperimentRunSort],
|
|
100
|
+
experiment_rowid: int,
|
|
101
|
+
mean_annotation_scores: Optional[NamedFromClause],
|
|
102
|
+
) -> tuple[ColumnElement[Any], ...]:
|
|
103
|
+
if not sort:
|
|
104
|
+
# Ideally, this would sort the runs by (example_id, repetition_number),
|
|
105
|
+
# but this would require making the cursor more complex or adding an additional query
|
|
106
|
+
# to handle the after cursor.
|
|
107
|
+
return (models.ExperimentRun.id.asc(),)
|
|
108
|
+
sort_direction = sort.dir
|
|
109
|
+
if sort.col.metric:
|
|
110
|
+
metric = sort.col.metric.value
|
|
111
|
+
assert metric is not None
|
|
112
|
+
if metric is ExperimentRunMetric.latencyMs:
|
|
113
|
+
if sort_direction is SortDir.asc:
|
|
114
|
+
return (models.ExperimentRun.latency_ms.asc(), models.ExperimentRun.id.asc())
|
|
115
|
+
else:
|
|
116
|
+
return (models.ExperimentRun.latency_ms.desc(), models.ExperimentRun.id.desc())
|
|
117
|
+
else:
|
|
118
|
+
assert_never(metric)
|
|
119
|
+
elif sort.col.annotation_name:
|
|
120
|
+
annotation_name = sort.col.annotation_name.value
|
|
121
|
+
assert annotation_name is not None
|
|
122
|
+
assert mean_annotation_scores is not None
|
|
123
|
+
if sort_direction is SortDir.asc:
|
|
124
|
+
return (
|
|
125
|
+
mean_annotation_scores.c.score.asc().nulls_last(),
|
|
126
|
+
models.ExperimentRun.id.asc(),
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
return (
|
|
130
|
+
mean_annotation_scores.c.score.desc().nulls_last(),
|
|
131
|
+
models.ExperimentRun.id.desc(),
|
|
132
|
+
)
|
|
133
|
+
raise NotImplementedError
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _add_after_expression(
|
|
137
|
+
query: Select[Any],
|
|
138
|
+
sort: Optional[ExperimentRunSort],
|
|
139
|
+
experiment_run_rowid: int,
|
|
140
|
+
after_sort_column_value: Optional[CursorSortColumnValue],
|
|
141
|
+
mean_annotation_scores: Optional[NamedFromClause],
|
|
142
|
+
) -> Select[Any]:
|
|
143
|
+
if not sort:
|
|
144
|
+
# Ideally, this would return the runs sorted by (example_id, repetition_number),
|
|
145
|
+
# but this would require making the cursor more complex or adding an additional query.
|
|
146
|
+
return query.where(models.ExperimentRun.id > literal(experiment_run_rowid))
|
|
147
|
+
sort_direction = sort.dir
|
|
148
|
+
compare_fn = operator.gt if sort_direction is SortDir.asc else operator.lt
|
|
149
|
+
if sort.col.metric:
|
|
150
|
+
metric = sort.col.metric.value
|
|
151
|
+
assert metric is not None
|
|
152
|
+
if metric is ExperimentRunMetric.latencyMs:
|
|
153
|
+
assert after_sort_column_value is not None
|
|
154
|
+
return query.where(
|
|
155
|
+
compare_fn(
|
|
156
|
+
tuple_(models.ExperimentRun.latency_ms, models.ExperimentRun.id),
|
|
157
|
+
tuple_(
|
|
158
|
+
literal(after_sort_column_value),
|
|
159
|
+
literal(experiment_run_rowid),
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
assert_never(metric)
|
|
165
|
+
elif sort.col.annotation_name:
|
|
166
|
+
annotation_name = sort.col.annotation_name.value
|
|
167
|
+
assert annotation_name is not None
|
|
168
|
+
assert mean_annotation_scores is not None
|
|
169
|
+
if after_sort_column_value is None:
|
|
170
|
+
return query.where(
|
|
171
|
+
and_(
|
|
172
|
+
compare_fn(models.ExperimentRun.id, literal(experiment_run_rowid)),
|
|
173
|
+
mean_annotation_scores.c.score.is_(None),
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
return query.where(
|
|
178
|
+
or_(
|
|
179
|
+
compare_fn(
|
|
180
|
+
tuple_(mean_annotation_scores.c.score, models.ExperimentRun.id),
|
|
181
|
+
tuple_(
|
|
182
|
+
literal(after_sort_column_value),
|
|
183
|
+
literal(experiment_run_rowid),
|
|
184
|
+
),
|
|
185
|
+
),
|
|
186
|
+
mean_annotation_scores.c.score.is_(None),
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
raise NotImplementedError
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _get_mean_annotation_scores_subquery(annotation_name: str) -> NamedFromClause:
|
|
193
|
+
return (
|
|
194
|
+
select(
|
|
195
|
+
func.avg(models.ExperimentRunAnnotation.score).label("score"),
|
|
196
|
+
models.ExperimentRunAnnotation.experiment_run_id.label("experiment_run_id"),
|
|
197
|
+
)
|
|
198
|
+
.select_from(models.ExperimentRunAnnotation)
|
|
199
|
+
.join(
|
|
200
|
+
models.ExperimentRun,
|
|
201
|
+
models.ExperimentRunAnnotation.experiment_run_id == models.ExperimentRun.id,
|
|
202
|
+
)
|
|
203
|
+
.where(models.ExperimentRunAnnotation.name == annotation_name)
|
|
204
|
+
.group_by(models.ExperimentRunAnnotation.experiment_run_id)
|
|
205
|
+
.subquery()
|
|
206
|
+
.alias("mean_annotation_scores")
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _add_joins_and_selects_to_query(
|
|
211
|
+
query: Select[tuple[models.ExperimentRun]],
|
|
212
|
+
sort: Optional[ExperimentRunSort],
|
|
213
|
+
mean_annotation_scores: Optional[NamedFromClause],
|
|
214
|
+
) -> Select[tuple[models.ExperimentRun]]:
|
|
215
|
+
if not sort:
|
|
216
|
+
return query
|
|
217
|
+
if sort.col.metric:
|
|
218
|
+
metric = sort.col.metric.value
|
|
219
|
+
assert metric is not None
|
|
220
|
+
if metric is ExperimentRunMetric.latencyMs:
|
|
221
|
+
return query
|
|
222
|
+
else:
|
|
223
|
+
assert_never(metric)
|
|
224
|
+
elif sort.col.annotation_name:
|
|
225
|
+
annotation_name = sort.col.annotation_name.value
|
|
226
|
+
assert annotation_name is not None
|
|
227
|
+
assert mean_annotation_scores is not None
|
|
228
|
+
query = query.join(
|
|
229
|
+
mean_annotation_scores,
|
|
230
|
+
mean_annotation_scores.c.experiment_run_id == models.ExperimentRun.id,
|
|
231
|
+
isouter=True,
|
|
232
|
+
)
|
|
233
|
+
query = query.add_columns(
|
|
234
|
+
mean_annotation_scores.c.score.label("score")
|
|
235
|
+
) # the score must be in the select so that the value can be included in the cursor
|
|
236
|
+
return query
|
|
237
|
+
raise NotImplementedError
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
1
2
|
from enum import Enum, auto
|
|
3
|
+
from typing import Any, Optional
|
|
2
4
|
|
|
3
5
|
import strawberry
|
|
6
|
+
from sqlalchemy import and_, desc, func, nulls_last, select
|
|
7
|
+
from sqlalchemy.orm import InstrumentedAttribute
|
|
8
|
+
from sqlalchemy.sql.expression import Select
|
|
9
|
+
from strawberry import UNSET
|
|
4
10
|
from typing_extensions import assert_never
|
|
5
11
|
|
|
12
|
+
from phoenix.db import models
|
|
13
|
+
from phoenix.db.helpers import truncate_name
|
|
6
14
|
from phoenix.server.api.types.pagination import CursorSortColumnDataType
|
|
7
15
|
from phoenix.server.api.types.SortDir import SortDir
|
|
8
16
|
|
|
@@ -15,6 +23,29 @@ class ProjectSessionColumn(Enum):
|
|
|
15
23
|
numTraces = auto()
|
|
16
24
|
costTotal = auto()
|
|
17
25
|
|
|
26
|
+
@property
|
|
27
|
+
def column_name(self) -> str:
|
|
28
|
+
return truncate_name(f"{self.name}_project_session_sort_column")
|
|
29
|
+
|
|
30
|
+
def as_orm_expression(self, joined_table: Optional[Any] = None) -> Any:
|
|
31
|
+
expr: Any
|
|
32
|
+
if self is ProjectSessionColumn.startTime:
|
|
33
|
+
expr = models.ProjectSession.start_time
|
|
34
|
+
elif self is ProjectSessionColumn.endTime:
|
|
35
|
+
expr = models.ProjectSession.end_time
|
|
36
|
+
elif self is ProjectSessionColumn.tokenCountTotal:
|
|
37
|
+
assert joined_table is not None
|
|
38
|
+
expr = joined_table.c.key
|
|
39
|
+
elif self is ProjectSessionColumn.numTraces:
|
|
40
|
+
assert joined_table is not None
|
|
41
|
+
expr = joined_table.c.key
|
|
42
|
+
elif self is ProjectSessionColumn.costTotal:
|
|
43
|
+
assert joined_table is not None
|
|
44
|
+
expr = joined_table.c.key
|
|
45
|
+
else:
|
|
46
|
+
assert_never(self)
|
|
47
|
+
return expr.label(self.column_name)
|
|
48
|
+
|
|
18
49
|
@property
|
|
19
50
|
def data_type(self) -> CursorSortColumnDataType:
|
|
20
51
|
if self is ProjectSessionColumn.tokenCountTotal or self is ProjectSessionColumn.numTraces:
|
|
@@ -25,8 +56,134 @@ class ProjectSessionColumn(Enum):
|
|
|
25
56
|
return CursorSortColumnDataType.FLOAT
|
|
26
57
|
assert_never(self)
|
|
27
58
|
|
|
59
|
+
def join_tables(self, stmt: Select[Any]) -> tuple[Select[Any], Any]:
|
|
60
|
+
"""
|
|
61
|
+
If needed, joins tables required for the sort column.
|
|
62
|
+
"""
|
|
63
|
+
if self is ProjectSessionColumn.tokenCountTotal:
|
|
64
|
+
sort_subq = (
|
|
65
|
+
select(
|
|
66
|
+
models.Trace.project_session_rowid.label("id"),
|
|
67
|
+
func.sum(models.Span.cumulative_llm_token_count_total).label("key"),
|
|
68
|
+
)
|
|
69
|
+
.join_from(models.Trace, models.Span)
|
|
70
|
+
.where(models.Span.parent_id.is_(None))
|
|
71
|
+
.group_by(models.Trace.project_session_rowid)
|
|
72
|
+
).subquery()
|
|
73
|
+
stmt = stmt.join(sort_subq, models.ProjectSession.id == sort_subq.c.id)
|
|
74
|
+
return stmt, sort_subq
|
|
75
|
+
if self is ProjectSessionColumn.numTraces:
|
|
76
|
+
sort_subq = (
|
|
77
|
+
select(
|
|
78
|
+
models.Trace.project_session_rowid.label("id"),
|
|
79
|
+
func.count(models.Trace.id).label("key"),
|
|
80
|
+
).group_by(models.Trace.project_session_rowid)
|
|
81
|
+
).subquery()
|
|
82
|
+
stmt = stmt.join(sort_subq, models.ProjectSession.id == sort_subq.c.id)
|
|
83
|
+
return stmt, sort_subq
|
|
84
|
+
if self is ProjectSessionColumn.costTotal:
|
|
85
|
+
sort_subq = (
|
|
86
|
+
select(
|
|
87
|
+
models.Trace.project_session_rowid.label("id"),
|
|
88
|
+
func.sum(models.SpanCost.total_cost).label("key"),
|
|
89
|
+
)
|
|
90
|
+
.join_from(
|
|
91
|
+
models.Trace,
|
|
92
|
+
models.SpanCost,
|
|
93
|
+
models.Trace.id == models.SpanCost.trace_rowid,
|
|
94
|
+
)
|
|
95
|
+
.group_by(models.Trace.project_session_rowid)
|
|
96
|
+
).subquery()
|
|
97
|
+
stmt = stmt.join(sort_subq, models.ProjectSession.id == sort_subq.c.id)
|
|
98
|
+
return stmt, sort_subq
|
|
99
|
+
return stmt, None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@strawberry.enum
|
|
103
|
+
class ProjectSessionAnnoAttr(Enum):
|
|
104
|
+
score = "score"
|
|
105
|
+
label = "label"
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def column_name(self) -> str:
|
|
109
|
+
return f"{self.value}_anno_sort_column"
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def orm_expression(self) -> Any:
|
|
113
|
+
expr: InstrumentedAttribute[Any]
|
|
114
|
+
if self is ProjectSessionAnnoAttr.score:
|
|
115
|
+
expr = models.ProjectSessionAnnotation.score
|
|
116
|
+
elif self is ProjectSessionAnnoAttr.label:
|
|
117
|
+
expr = models.ProjectSessionAnnotation.label
|
|
118
|
+
else:
|
|
119
|
+
assert_never(self)
|
|
120
|
+
return expr.label(self.column_name)
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def data_type(self) -> CursorSortColumnDataType:
|
|
124
|
+
if self is ProjectSessionAnnoAttr.label:
|
|
125
|
+
return CursorSortColumnDataType.STRING
|
|
126
|
+
if self is ProjectSessionAnnoAttr.score:
|
|
127
|
+
return CursorSortColumnDataType.FLOAT
|
|
128
|
+
assert_never(self)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@strawberry.input
|
|
132
|
+
class ProjectSessionAnnoResultKey:
|
|
133
|
+
name: str
|
|
134
|
+
attr: ProjectSessionAnnoAttr
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(frozen=True)
|
|
138
|
+
class ProjectSessionSortConfig:
|
|
139
|
+
stmt: Select[Any]
|
|
140
|
+
orm_expression: Any
|
|
141
|
+
dir: SortDir
|
|
142
|
+
column_name: str
|
|
143
|
+
column_data_type: CursorSortColumnDataType
|
|
144
|
+
|
|
28
145
|
|
|
29
146
|
@strawberry.input(description="The sort key and direction for ProjectSession connections.")
|
|
30
147
|
class ProjectSessionSort:
|
|
31
|
-
col: ProjectSessionColumn
|
|
148
|
+
col: Optional[ProjectSessionColumn] = UNSET
|
|
149
|
+
anno_result_key: Optional[ProjectSessionAnnoResultKey] = UNSET
|
|
32
150
|
dir: SortDir
|
|
151
|
+
|
|
152
|
+
def update_orm_expr(self, stmt: Select[Any]) -> ProjectSessionSortConfig:
|
|
153
|
+
if (col := self.col) and not self.anno_result_key:
|
|
154
|
+
stmt, joined_table = col.join_tables(stmt)
|
|
155
|
+
expr = col.as_orm_expression(joined_table)
|
|
156
|
+
stmt = stmt.add_columns(expr)
|
|
157
|
+
if self.dir == SortDir.desc:
|
|
158
|
+
expr = desc(expr)
|
|
159
|
+
return ProjectSessionSortConfig(
|
|
160
|
+
stmt=stmt.order_by(nulls_last(expr)),
|
|
161
|
+
orm_expression=col.as_orm_expression(joined_table),
|
|
162
|
+
dir=self.dir,
|
|
163
|
+
column_name=col.column_name,
|
|
164
|
+
column_data_type=col.data_type,
|
|
165
|
+
)
|
|
166
|
+
if (anno_result_key := self.anno_result_key) and not col:
|
|
167
|
+
anno_name = anno_result_key.name
|
|
168
|
+
anno_attr = anno_result_key.attr
|
|
169
|
+
expr = anno_result_key.attr.orm_expression
|
|
170
|
+
stmt = stmt.add_columns(expr)
|
|
171
|
+
if self.dir == SortDir.desc:
|
|
172
|
+
expr = desc(expr)
|
|
173
|
+
stmt = stmt.join(
|
|
174
|
+
models.ProjectSessionAnnotation,
|
|
175
|
+
onclause=and_(
|
|
176
|
+
models.ProjectSessionAnnotation.project_session_id == models.ProjectSession.id,
|
|
177
|
+
models.ProjectSessionAnnotation.name == anno_name,
|
|
178
|
+
),
|
|
179
|
+
).order_by(nulls_last(expr))
|
|
180
|
+
return ProjectSessionSortConfig(
|
|
181
|
+
stmt=stmt,
|
|
182
|
+
orm_expression=anno_result_key.attr.orm_expression,
|
|
183
|
+
dir=self.dir,
|
|
184
|
+
column_name=anno_attr.column_name,
|
|
185
|
+
column_data_type=anno_attr.data_type,
|
|
186
|
+
)
|
|
187
|
+
raise ValueError(
|
|
188
|
+
"Exactly one of `col` or `annoResultKey` must be specified on `ProjectSessionSort`."
|
|
189
|
+
)
|
|
@@ -11,6 +11,7 @@ from typing_extensions import assert_never
|
|
|
11
11
|
|
|
12
12
|
import phoenix.trace.v1 as pb
|
|
13
13
|
from phoenix.db import models
|
|
14
|
+
from phoenix.db.helpers import truncate_name
|
|
14
15
|
from phoenix.server.api.types.pagination import CursorSortColumnDataType
|
|
15
16
|
from phoenix.server.api.types.SortDir import SortDir
|
|
16
17
|
from phoenix.trace.schemas import SpanID
|
|
@@ -32,7 +33,7 @@ class SpanColumn(Enum):
|
|
|
32
33
|
|
|
33
34
|
@property
|
|
34
35
|
def column_name(self) -> str:
|
|
35
|
-
return f"{self.name}_span_sort_column"
|
|
36
|
+
return truncate_name(f"{self.name}_span_sort_column")
|
|
36
37
|
|
|
37
38
|
def as_orm_expression(self, joined_table: Optional[Any] = None) -> Any:
|
|
38
39
|
expr: Any
|
|
@@ -23,7 +23,7 @@ from phoenix.db.types.annotation_configs import (
|
|
|
23
23
|
from phoenix.db.types.annotation_configs import (
|
|
24
24
|
FreeformAnnotationConfig as FreeformAnnotationConfigModel,
|
|
25
25
|
)
|
|
26
|
-
from phoenix.server.api.auth import IsNotReadOnly
|
|
26
|
+
from phoenix.server.api.auth import IsNotReadOnly, IsNotViewer
|
|
27
27
|
from phoenix.server.api.context import Context
|
|
28
28
|
from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound
|
|
29
29
|
from phoenix.server.api.queries import Query
|
|
@@ -197,7 +197,7 @@ def _to_pydantic_freeform_annotation_config(
|
|
|
197
197
|
|
|
198
198
|
@strawberry.type
|
|
199
199
|
class AnnotationConfigMutationMixin:
|
|
200
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore[misc]
|
|
200
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore[misc]
|
|
201
201
|
async def create_annotation_config(
|
|
202
202
|
self,
|
|
203
203
|
info: Info[Context, None],
|
|
@@ -236,7 +236,7 @@ class AnnotationConfigMutationMixin:
|
|
|
236
236
|
annotation_config=to_gql_annotation_config(annotation_config),
|
|
237
237
|
)
|
|
238
238
|
|
|
239
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore[misc]
|
|
239
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore[misc]
|
|
240
240
|
async def update_annotation_config(
|
|
241
241
|
self,
|
|
242
242
|
info: Info[Context, None],
|
|
@@ -285,7 +285,7 @@ class AnnotationConfigMutationMixin:
|
|
|
285
285
|
annotation_config=to_gql_annotation_config(annotation_config),
|
|
286
286
|
)
|
|
287
287
|
|
|
288
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore[misc]
|
|
288
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore[misc]
|
|
289
289
|
async def delete_annotation_configs(
|
|
290
290
|
self,
|
|
291
291
|
info: Info[Context, None],
|
|
@@ -317,7 +317,7 @@ class AnnotationConfigMutationMixin:
|
|
|
317
317
|
],
|
|
318
318
|
)
|
|
319
319
|
|
|
320
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore[misc]
|
|
320
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore[misc]
|
|
321
321
|
async def add_annotation_config_to_project(
|
|
322
322
|
self,
|
|
323
323
|
info: Info[Context, None],
|
|
@@ -377,7 +377,7 @@ class AnnotationConfigMutationMixin:
|
|
|
377
377
|
project=Project(project_rowid=project_id),
|
|
378
378
|
)
|
|
379
379
|
|
|
380
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore[misc]
|
|
380
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore[misc]
|
|
381
381
|
async def remove_annotation_config_from_project(
|
|
382
382
|
self,
|
|
383
383
|
info: Info[Context, None],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from datetime import datetime, timezone
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import Literal, Optional
|
|
3
3
|
|
|
4
4
|
import strawberry
|
|
5
5
|
from sqlalchemy import select
|
|
@@ -9,7 +9,7 @@ from strawberry.types import Info
|
|
|
9
9
|
|
|
10
10
|
from phoenix.db import models
|
|
11
11
|
from phoenix.db.models import UserRoleName
|
|
12
|
-
from phoenix.server.api.auth import IsAdmin, IsLocked, IsNotReadOnly
|
|
12
|
+
from phoenix.server.api.auth import IsAdmin, IsLocked, IsNotReadOnly, IsNotViewer
|
|
13
13
|
from phoenix.server.api.context import Context
|
|
14
14
|
from phoenix.server.api.exceptions import Unauthorized
|
|
15
15
|
from phoenix.server.api.queries import Query
|
|
@@ -61,7 +61,7 @@ class DeleteApiKeyMutationPayload:
|
|
|
61
61
|
|
|
62
62
|
@strawberry.type
|
|
63
63
|
class ApiKeyMutationMixin:
|
|
64
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin, IsLocked]) # type: ignore
|
|
64
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdmin, IsLocked]) # type: ignore
|
|
65
65
|
async def create_system_api_key(
|
|
66
66
|
self, info: Info[Context, None], input: CreateApiKeyInput
|
|
67
67
|
) -> CreateSystemApiKeyMutationPayload:
|
|
@@ -113,12 +113,20 @@ class ApiKeyMutationMixin:
|
|
|
113
113
|
except AttributeError:
|
|
114
114
|
raise ValueError("User not found")
|
|
115
115
|
issued_at = datetime.now(timezone.utc)
|
|
116
|
+
# Determine user role for API key
|
|
117
|
+
user_role: Literal["ADMIN", "MEMBER", "VIEWER"]
|
|
118
|
+
if user.is_admin:
|
|
119
|
+
user_role = "ADMIN"
|
|
120
|
+
elif user.is_viewer:
|
|
121
|
+
user_role = "VIEWER"
|
|
122
|
+
else:
|
|
123
|
+
user_role = "MEMBER"
|
|
116
124
|
claims = ApiKeyClaims(
|
|
117
125
|
subject=user.identity,
|
|
118
126
|
issued_at=issued_at,
|
|
119
127
|
expiration_time=input.expires_at or None,
|
|
120
128
|
attributes=ApiKeyAttributes(
|
|
121
|
-
user_role=
|
|
129
|
+
user_role=user_role,
|
|
122
130
|
name=input.name,
|
|
123
131
|
description=input.description,
|
|
124
132
|
),
|
|
@@ -137,7 +145,7 @@ class ApiKeyMutationMixin:
|
|
|
137
145
|
query=Query(),
|
|
138
146
|
)
|
|
139
147
|
|
|
140
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin]) # type: ignore
|
|
148
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdmin]) # type: ignore
|
|
141
149
|
async def delete_system_api_key(
|
|
142
150
|
self, info: Info[Context, None], input: DeleteApiKeyInput
|
|
143
151
|
) -> DeleteApiKeyMutationPayload:
|
|
@@ -30,7 +30,7 @@ from phoenix.db.helpers import (
|
|
|
30
30
|
get_dataset_example_revisions,
|
|
31
31
|
insert_experiment_with_examples_snapshot,
|
|
32
32
|
)
|
|
33
|
-
from phoenix.server.api.auth import IsLocked, IsNotReadOnly
|
|
33
|
+
from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
|
|
34
34
|
from phoenix.server.api.context import Context
|
|
35
35
|
from phoenix.server.api.exceptions import BadRequest, CustomGraphQLError, NotFound
|
|
36
36
|
from phoenix.server.api.helpers.dataset_helpers import get_dataset_example_output
|
|
@@ -131,7 +131,7 @@ class ChatCompletionOverDatasetMutationPayload:
|
|
|
131
131
|
|
|
132
132
|
@strawberry.type
|
|
133
133
|
class ChatCompletionMutationMixin:
|
|
134
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
134
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
135
135
|
@classmethod
|
|
136
136
|
async def chat_completion_over_dataset(
|
|
137
137
|
cls,
|
|
@@ -302,7 +302,7 @@ class ChatCompletionMutationMixin:
|
|
|
302
302
|
payload.examples.append(example_payload)
|
|
303
303
|
return payload
|
|
304
304
|
|
|
305
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
305
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
306
306
|
@classmethod
|
|
307
307
|
async def chat_completion(
|
|
308
308
|
cls, info: Info[Context, None], input: ChatCompletionInput
|
|
@@ -10,7 +10,7 @@ from strawberry.relay.types import GlobalID
|
|
|
10
10
|
from strawberry.types import Info
|
|
11
11
|
|
|
12
12
|
from phoenix.db import models
|
|
13
|
-
from phoenix.server.api.auth import IsLocked, IsNotReadOnly
|
|
13
|
+
from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
|
|
14
14
|
from phoenix.server.api.context import Context
|
|
15
15
|
from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound
|
|
16
16
|
from phoenix.server.api.queries import Query
|
|
@@ -78,7 +78,7 @@ class UnsetDatasetLabelsMutationPayload:
|
|
|
78
78
|
|
|
79
79
|
@strawberry.type
|
|
80
80
|
class DatasetLabelMutationMixin:
|
|
81
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
81
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
82
82
|
async def create_dataset_label(
|
|
83
83
|
self,
|
|
84
84
|
info: Info[Context, None],
|
|
@@ -100,7 +100,7 @@ class DatasetLabelMutationMixin:
|
|
|
100
100
|
dataset_label=to_gql_dataset_label(dataset_label_orm)
|
|
101
101
|
)
|
|
102
102
|
|
|
103
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
103
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
104
104
|
async def update_dataset_label(
|
|
105
105
|
self, info: Info[Context, None], input: UpdateDatasetLabelInput
|
|
106
106
|
) -> UpdateDatasetLabelMutationPayload:
|
|
@@ -133,7 +133,7 @@ class DatasetLabelMutationMixin:
|
|
|
133
133
|
dataset_label=to_gql_dataset_label(dataset_label_orm)
|
|
134
134
|
)
|
|
135
135
|
|
|
136
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
136
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
137
137
|
async def delete_dataset_labels(
|
|
138
138
|
self, info: Info[Context, None], input: DeleteDatasetLabelsInput
|
|
139
139
|
) -> DeleteDatasetLabelsMutationPayload:
|
|
@@ -166,7 +166,7 @@ class DatasetLabelMutationMixin:
|
|
|
166
166
|
]
|
|
167
167
|
)
|
|
168
168
|
|
|
169
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
169
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
170
170
|
async def set_dataset_labels(
|
|
171
171
|
self, info: Info[Context, None], input: SetDatasetLabelsInput
|
|
172
172
|
) -> SetDatasetLabelsMutationPayload:
|
|
@@ -248,7 +248,7 @@ class DatasetLabelMutationMixin:
|
|
|
248
248
|
query=Query(),
|
|
249
249
|
)
|
|
250
250
|
|
|
251
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
251
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
252
252
|
async def unset_dataset_labels(
|
|
253
253
|
self, info: Info[Context, None], input: UnsetDatasetLabelsInput
|
|
254
254
|
) -> UnsetDatasetLabelsMutationPayload:
|