arize-phoenix 11.23.1__py3-none-any.whl → 12.28.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.
Files changed (221) hide show
  1. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/METADATA +61 -36
  2. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/RECORD +212 -162
  3. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/WHEEL +1 -1
  4. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/IP_NOTICE +1 -1
  5. phoenix/__generated__/__init__.py +0 -0
  6. phoenix/__generated__/classification_evaluator_configs/__init__.py +20 -0
  7. phoenix/__generated__/classification_evaluator_configs/_document_relevance_classification_evaluator_config.py +17 -0
  8. phoenix/__generated__/classification_evaluator_configs/_hallucination_classification_evaluator_config.py +17 -0
  9. phoenix/__generated__/classification_evaluator_configs/_models.py +18 -0
  10. phoenix/__generated__/classification_evaluator_configs/_tool_selection_classification_evaluator_config.py +17 -0
  11. phoenix/__init__.py +2 -1
  12. phoenix/auth.py +27 -2
  13. phoenix/config.py +1594 -81
  14. phoenix/db/README.md +546 -28
  15. phoenix/db/bulk_inserter.py +119 -116
  16. phoenix/db/engines.py +140 -33
  17. phoenix/db/facilitator.py +22 -1
  18. phoenix/db/helpers.py +818 -65
  19. phoenix/db/iam_auth.py +64 -0
  20. phoenix/db/insertion/dataset.py +133 -1
  21. phoenix/db/insertion/document_annotation.py +9 -6
  22. phoenix/db/insertion/evaluation.py +2 -3
  23. phoenix/db/insertion/helpers.py +2 -2
  24. phoenix/db/insertion/session_annotation.py +176 -0
  25. phoenix/db/insertion/span_annotation.py +3 -4
  26. phoenix/db/insertion/trace_annotation.py +3 -4
  27. phoenix/db/insertion/types.py +41 -18
  28. phoenix/db/migrations/versions/01a8342c9cdf_add_user_id_on_datasets.py +40 -0
  29. phoenix/db/migrations/versions/0df286449799_add_session_annotations_table.py +105 -0
  30. phoenix/db/migrations/versions/272b66ff50f8_drop_single_indices.py +119 -0
  31. phoenix/db/migrations/versions/58228d933c91_dataset_labels.py +67 -0
  32. phoenix/db/migrations/versions/699f655af132_experiment_tags.py +57 -0
  33. phoenix/db/migrations/versions/735d3d93c33e_add_composite_indices.py +41 -0
  34. phoenix/db/migrations/versions/ab513d89518b_add_user_id_on_dataset_versions.py +40 -0
  35. phoenix/db/migrations/versions/d0690a79ea51_users_on_experiments.py +40 -0
  36. phoenix/db/migrations/versions/deb2c81c0bb2_dataset_splits.py +139 -0
  37. phoenix/db/migrations/versions/e76cbd66ffc3_add_experiments_dataset_examples.py +87 -0
  38. phoenix/db/models.py +364 -56
  39. phoenix/db/pg_config.py +10 -0
  40. phoenix/db/types/trace_retention.py +7 -6
  41. phoenix/experiments/functions.py +69 -19
  42. phoenix/inferences/inferences.py +1 -2
  43. phoenix/server/api/auth.py +9 -0
  44. phoenix/server/api/auth_messages.py +46 -0
  45. phoenix/server/api/context.py +60 -0
  46. phoenix/server/api/dataloaders/__init__.py +36 -0
  47. phoenix/server/api/dataloaders/annotation_summaries.py +60 -8
  48. phoenix/server/api/dataloaders/average_experiment_repeated_run_group_latency.py +50 -0
  49. phoenix/server/api/dataloaders/average_experiment_run_latency.py +17 -24
  50. phoenix/server/api/dataloaders/cache/two_tier_cache.py +1 -2
  51. phoenix/server/api/dataloaders/dataset_dataset_splits.py +52 -0
  52. phoenix/server/api/dataloaders/dataset_example_revisions.py +0 -1
  53. phoenix/server/api/dataloaders/dataset_example_splits.py +40 -0
  54. phoenix/server/api/dataloaders/dataset_examples_and_versions_by_experiment_run.py +47 -0
  55. phoenix/server/api/dataloaders/dataset_labels.py +36 -0
  56. phoenix/server/api/dataloaders/document_evaluation_summaries.py +2 -2
  57. phoenix/server/api/dataloaders/document_evaluations.py +6 -9
  58. phoenix/server/api/dataloaders/experiment_annotation_summaries.py +88 -34
  59. phoenix/server/api/dataloaders/experiment_dataset_splits.py +43 -0
  60. phoenix/server/api/dataloaders/experiment_error_rates.py +21 -28
  61. phoenix/server/api/dataloaders/experiment_repeated_run_group_annotation_summaries.py +77 -0
  62. phoenix/server/api/dataloaders/experiment_repeated_run_groups.py +57 -0
  63. phoenix/server/api/dataloaders/experiment_runs_by_experiment_and_example.py +44 -0
  64. phoenix/server/api/dataloaders/latency_ms_quantile.py +40 -8
  65. phoenix/server/api/dataloaders/record_counts.py +37 -10
  66. phoenix/server/api/dataloaders/session_annotations_by_session.py +29 -0
  67. phoenix/server/api/dataloaders/span_cost_summary_by_experiment_repeated_run_group.py +64 -0
  68. phoenix/server/api/dataloaders/span_cost_summary_by_project.py +28 -14
  69. phoenix/server/api/dataloaders/span_costs.py +3 -9
  70. phoenix/server/api/dataloaders/table_fields.py +2 -2
  71. phoenix/server/api/dataloaders/token_prices_by_model.py +30 -0
  72. phoenix/server/api/dataloaders/trace_annotations_by_trace.py +27 -0
  73. phoenix/server/api/exceptions.py +5 -1
  74. phoenix/server/api/helpers/playground_clients.py +263 -83
  75. phoenix/server/api/helpers/playground_spans.py +2 -1
  76. phoenix/server/api/helpers/playground_users.py +26 -0
  77. phoenix/server/api/helpers/prompts/conversions/google.py +103 -0
  78. phoenix/server/api/helpers/prompts/models.py +61 -19
  79. phoenix/server/api/input_types/{SpanAnnotationFilter.py → AnnotationFilter.py} +22 -14
  80. phoenix/server/api/input_types/ChatCompletionInput.py +3 -0
  81. phoenix/server/api/input_types/CreateProjectSessionAnnotationInput.py +37 -0
  82. phoenix/server/api/input_types/DatasetFilter.py +5 -2
  83. phoenix/server/api/input_types/ExperimentRunSort.py +237 -0
  84. phoenix/server/api/input_types/GenerativeModelInput.py +3 -0
  85. phoenix/server/api/input_types/ProjectSessionSort.py +158 -1
  86. phoenix/server/api/input_types/PromptVersionInput.py +47 -1
  87. phoenix/server/api/input_types/SpanSort.py +3 -2
  88. phoenix/server/api/input_types/UpdateAnnotationInput.py +34 -0
  89. phoenix/server/api/input_types/UserRoleInput.py +1 -0
  90. phoenix/server/api/mutations/__init__.py +8 -0
  91. phoenix/server/api/mutations/annotation_config_mutations.py +8 -8
  92. phoenix/server/api/mutations/api_key_mutations.py +15 -20
  93. phoenix/server/api/mutations/chat_mutations.py +106 -37
  94. phoenix/server/api/mutations/dataset_label_mutations.py +243 -0
  95. phoenix/server/api/mutations/dataset_mutations.py +21 -16
  96. phoenix/server/api/mutations/dataset_split_mutations.py +351 -0
  97. phoenix/server/api/mutations/experiment_mutations.py +2 -2
  98. phoenix/server/api/mutations/export_events_mutations.py +3 -3
  99. phoenix/server/api/mutations/model_mutations.py +11 -9
  100. phoenix/server/api/mutations/project_mutations.py +4 -4
  101. phoenix/server/api/mutations/project_session_annotations_mutations.py +158 -0
  102. phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +8 -4
  103. phoenix/server/api/mutations/prompt_label_mutations.py +74 -65
  104. phoenix/server/api/mutations/prompt_mutations.py +65 -129
  105. phoenix/server/api/mutations/prompt_version_tag_mutations.py +11 -8
  106. phoenix/server/api/mutations/span_annotations_mutations.py +15 -10
  107. phoenix/server/api/mutations/trace_annotations_mutations.py +13 -8
  108. phoenix/server/api/mutations/trace_mutations.py +3 -3
  109. phoenix/server/api/mutations/user_mutations.py +55 -26
  110. phoenix/server/api/queries.py +501 -617
  111. phoenix/server/api/routers/__init__.py +2 -2
  112. phoenix/server/api/routers/auth.py +141 -87
  113. phoenix/server/api/routers/ldap.py +229 -0
  114. phoenix/server/api/routers/oauth2.py +349 -101
  115. phoenix/server/api/routers/v1/__init__.py +22 -4
  116. phoenix/server/api/routers/v1/annotation_configs.py +19 -30
  117. phoenix/server/api/routers/v1/annotations.py +455 -13
  118. phoenix/server/api/routers/v1/datasets.py +355 -68
  119. phoenix/server/api/routers/v1/documents.py +142 -0
  120. phoenix/server/api/routers/v1/evaluations.py +20 -28
  121. phoenix/server/api/routers/v1/experiment_evaluations.py +16 -6
  122. phoenix/server/api/routers/v1/experiment_runs.py +335 -59
  123. phoenix/server/api/routers/v1/experiments.py +475 -47
  124. phoenix/server/api/routers/v1/projects.py +16 -50
  125. phoenix/server/api/routers/v1/prompts.py +50 -39
  126. phoenix/server/api/routers/v1/sessions.py +108 -0
  127. phoenix/server/api/routers/v1/spans.py +156 -96
  128. phoenix/server/api/routers/v1/traces.py +51 -77
  129. phoenix/server/api/routers/v1/users.py +64 -24
  130. phoenix/server/api/routers/v1/utils.py +3 -7
  131. phoenix/server/api/subscriptions.py +257 -93
  132. phoenix/server/api/types/Annotation.py +90 -23
  133. phoenix/server/api/types/ApiKey.py +13 -17
  134. phoenix/server/api/types/AuthMethod.py +1 -0
  135. phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +1 -0
  136. phoenix/server/api/types/Dataset.py +199 -72
  137. phoenix/server/api/types/DatasetExample.py +88 -18
  138. phoenix/server/api/types/DatasetExperimentAnnotationSummary.py +10 -0
  139. phoenix/server/api/types/DatasetLabel.py +57 -0
  140. phoenix/server/api/types/DatasetSplit.py +98 -0
  141. phoenix/server/api/types/DatasetVersion.py +49 -4
  142. phoenix/server/api/types/DocumentAnnotation.py +212 -0
  143. phoenix/server/api/types/Experiment.py +215 -68
  144. phoenix/server/api/types/ExperimentComparison.py +3 -9
  145. phoenix/server/api/types/ExperimentRepeatedRunGroup.py +155 -0
  146. phoenix/server/api/types/ExperimentRepeatedRunGroupAnnotationSummary.py +9 -0
  147. phoenix/server/api/types/ExperimentRun.py +120 -70
  148. phoenix/server/api/types/ExperimentRunAnnotation.py +158 -39
  149. phoenix/server/api/types/GenerativeModel.py +95 -42
  150. phoenix/server/api/types/GenerativeProvider.py +1 -1
  151. phoenix/server/api/types/ModelInterface.py +7 -2
  152. phoenix/server/api/types/PlaygroundModel.py +12 -2
  153. phoenix/server/api/types/Project.py +218 -185
  154. phoenix/server/api/types/ProjectSession.py +146 -29
  155. phoenix/server/api/types/ProjectSessionAnnotation.py +187 -0
  156. phoenix/server/api/types/ProjectTraceRetentionPolicy.py +1 -1
  157. phoenix/server/api/types/Prompt.py +119 -39
  158. phoenix/server/api/types/PromptLabel.py +42 -25
  159. phoenix/server/api/types/PromptVersion.py +11 -8
  160. phoenix/server/api/types/PromptVersionTag.py +65 -25
  161. phoenix/server/api/types/Span.py +130 -123
  162. phoenix/server/api/types/SpanAnnotation.py +189 -42
  163. phoenix/server/api/types/SystemApiKey.py +65 -1
  164. phoenix/server/api/types/Trace.py +184 -53
  165. phoenix/server/api/types/TraceAnnotation.py +149 -50
  166. phoenix/server/api/types/User.py +128 -33
  167. phoenix/server/api/types/UserApiKey.py +73 -26
  168. phoenix/server/api/types/node.py +10 -0
  169. phoenix/server/api/types/pagination.py +11 -2
  170. phoenix/server/app.py +154 -36
  171. phoenix/server/authorization.py +5 -4
  172. phoenix/server/bearer_auth.py +13 -5
  173. phoenix/server/cost_tracking/cost_model_lookup.py +42 -14
  174. phoenix/server/cost_tracking/model_cost_manifest.json +1085 -194
  175. phoenix/server/daemons/generative_model_store.py +61 -9
  176. phoenix/server/daemons/span_cost_calculator.py +10 -8
  177. phoenix/server/dml_event.py +13 -0
  178. phoenix/server/email/sender.py +29 -2
  179. phoenix/server/grpc_server.py +9 -9
  180. phoenix/server/jwt_store.py +8 -6
  181. phoenix/server/ldap.py +1449 -0
  182. phoenix/server/main.py +9 -3
  183. phoenix/server/oauth2.py +330 -12
  184. phoenix/server/prometheus.py +43 -6
  185. phoenix/server/rate_limiters.py +4 -9
  186. phoenix/server/retention.py +33 -20
  187. phoenix/server/session_filters.py +49 -0
  188. phoenix/server/static/.vite/manifest.json +51 -53
  189. phoenix/server/static/assets/components-BreFUQQa.js +6702 -0
  190. phoenix/server/static/assets/{index-BPCwGQr8.js → index-CTQoemZv.js} +42 -35
  191. phoenix/server/static/assets/pages-DBE5iYM3.js +9524 -0
  192. phoenix/server/static/assets/vendor-BGzfc4EU.css +1 -0
  193. phoenix/server/static/assets/vendor-DCE4v-Ot.js +920 -0
  194. phoenix/server/static/assets/vendor-codemirror-D5f205eT.js +25 -0
  195. phoenix/server/static/assets/{vendor-recharts-Bw30oz1A.js → vendor-recharts-V9cwpXsm.js} +7 -7
  196. phoenix/server/static/assets/{vendor-shiki-DZajAPeq.js → vendor-shiki-Do--csgv.js} +1 -1
  197. phoenix/server/static/assets/vendor-three-CmB8bl_y.js +3840 -0
  198. phoenix/server/templates/index.html +7 -1
  199. phoenix/server/thread_server.py +1 -2
  200. phoenix/server/utils.py +74 -0
  201. phoenix/session/client.py +55 -1
  202. phoenix/session/data_extractor.py +5 -0
  203. phoenix/session/evaluation.py +8 -4
  204. phoenix/session/session.py +44 -8
  205. phoenix/settings.py +2 -0
  206. phoenix/trace/attributes.py +80 -13
  207. phoenix/trace/dsl/query.py +2 -0
  208. phoenix/trace/projects.py +5 -0
  209. phoenix/utilities/template_formatters.py +1 -1
  210. phoenix/version.py +1 -1
  211. phoenix/server/api/types/Evaluation.py +0 -39
  212. phoenix/server/static/assets/components-D0DWAf0l.js +0 -5650
  213. phoenix/server/static/assets/pages-Creyamao.js +0 -8612
  214. phoenix/server/static/assets/vendor-CU36oj8y.js +0 -905
  215. phoenix/server/static/assets/vendor-CqDb5u4o.css +0 -1
  216. phoenix/server/static/assets/vendor-arizeai-Ctgw0e1G.js +0 -168
  217. phoenix/server/static/assets/vendor-codemirror-Cojjzqb9.js +0 -25
  218. phoenix/server/static/assets/vendor-three-BLWp5bic.js +0 -2998
  219. phoenix/utilities/deprecation.py +0 -31
  220. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/entry_points.txt +0 -0
  221. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.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
@@ -2,6 +2,7 @@ from typing import Optional
2
2
 
3
3
  import strawberry
4
4
  from strawberry import UNSET
5
+ from strawberry.scalars import JSON
5
6
 
6
7
  from phoenix.server.api.types.GenerativeProvider import GenerativeProviderKey
7
8
 
@@ -19,3 +20,5 @@ class GenerativeModelInput:
19
20
  """ The API version to use for the model. """
20
21
  region: Optional[str] = UNSET
21
22
  """ The region to use for the model. """
23
+ custom_headers: Optional[JSON] = UNSET
24
+ """ Custom headers to use for the model. """
@@ -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
+ )
@@ -1,10 +1,11 @@
1
1
  import json
2
- from typing import Optional, cast
2
+ from typing import Any, Optional, Union, cast
3
3
 
4
4
  import strawberry
5
5
  from strawberry import UNSET
6
6
  from strawberry.scalars import JSON
7
7
 
8
+ from phoenix.db import models
8
9
  from phoenix.db.types.model_provider import ModelProvider
9
10
  from phoenix.server.api.helpers.prompts.models import (
10
11
  ContentPart,
@@ -12,11 +13,15 @@ from phoenix.server.api.helpers.prompts.models import (
12
13
  PromptMessage,
13
14
  PromptMessageRole,
14
15
  PromptTemplateFormat,
16
+ PromptTemplateType,
15
17
  RoleConversion,
16
18
  TextContentPart,
17
19
  ToolCallContentPart,
18
20
  ToolCallFunction,
19
21
  ToolResultContentPart,
22
+ normalize_response_format,
23
+ normalize_tools,
24
+ validate_invocation_parameters,
20
25
  )
21
26
 
22
27
 
@@ -88,6 +93,47 @@ class ChatPromptVersionInput:
88
93
  k: v for k, v in self.invocation_parameters.items() if v is not None
89
94
  }
90
95
 
96
+ def to_orm_prompt_version(
97
+ self,
98
+ user_id: Optional[int],
99
+ ) -> models.PromptVersion:
100
+ tool_definitions = [tool.definition for tool in self.tools]
101
+ tool_choice = cast(
102
+ Optional[Union[str, dict[str, Any]]],
103
+ cast(dict[str, Any], self.invocation_parameters).pop("tool_choice", None),
104
+ )
105
+ model_provider = ModelProvider(self.model_provider)
106
+ tools = (
107
+ normalize_tools(tool_definitions, model_provider, tool_choice)
108
+ if tool_definitions
109
+ else None
110
+ )
111
+ template = to_pydantic_prompt_chat_template_v1(self.template)
112
+ response_format = (
113
+ normalize_response_format(
114
+ self.response_format.definition,
115
+ model_provider,
116
+ )
117
+ if self.response_format
118
+ else None
119
+ )
120
+ invocation_parameters = validate_invocation_parameters(
121
+ self.invocation_parameters,
122
+ model_provider,
123
+ )
124
+ return models.PromptVersion(
125
+ description=self.description,
126
+ user_id=user_id,
127
+ template_type=PromptTemplateType.CHAT,
128
+ template_format=self.template_format,
129
+ template=template,
130
+ invocation_parameters=invocation_parameters,
131
+ tools=tools,
132
+ response_format=response_format,
133
+ model_provider=ModelProvider(self.model_provider),
134
+ model_name=self.model_name,
135
+ )
136
+
91
137
 
92
138
  def to_pydantic_prompt_chat_template_v1(
93
139
  prompt_chat_template_input: PromptChatTemplateInput,
@@ -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
@@ -199,7 +200,7 @@ class SpanSort:
199
200
  models.SpanAnnotation.span_rowid == models.Span.id,
200
201
  models.SpanAnnotation.name == eval_name,
201
202
  ),
202
- ).order_by(expr)
203
+ ).order_by(nulls_last(expr))
203
204
  return SpanSortConfig(
204
205
  stmt=stmt,
205
206
  orm_expression=eval_result_key.attr.orm_expression,
@@ -0,0 +1,34 @@
1
+ from typing import Optional
2
+
3
+ import strawberry
4
+ from strawberry.relay import GlobalID
5
+ from strawberry.scalars import JSON
6
+
7
+ from phoenix.server.api.exceptions import BadRequest
8
+ from phoenix.server.api.types.AnnotationSource import AnnotationSource
9
+ from phoenix.server.api.types.AnnotatorKind import AnnotatorKind
10
+
11
+
12
+ @strawberry.input
13
+ class UpdateAnnotationInput:
14
+ id: GlobalID
15
+ name: str
16
+ annotator_kind: AnnotatorKind = AnnotatorKind.HUMAN
17
+ label: Optional[str] = None
18
+ score: Optional[float] = None
19
+ explanation: Optional[str] = None
20
+ metadata: JSON = strawberry.field(default_factory=dict)
21
+ source: AnnotationSource = AnnotationSource.APP
22
+
23
+ def __post_init__(self) -> None:
24
+ self.name = self.name.strip()
25
+ if isinstance(self.label, str):
26
+ self.label = self.label.strip()
27
+ if not self.label:
28
+ self.label = None
29
+ if isinstance(self.explanation, str):
30
+ self.explanation = self.explanation.strip()
31
+ if not self.explanation:
32
+ self.explanation = None
33
+ if self.score is None and not self.label and not self.explanation:
34
+ raise BadRequest("At least one of score, label, or explanation must be not null/empty.")
@@ -7,3 +7,4 @@ import strawberry
7
7
  class UserRoleInput(Enum):
8
8
  ADMIN = "ADMIN"
9
9
  MEMBER = "MEMBER"
10
+ VIEWER = "VIEWER"
@@ -5,11 +5,16 @@ from phoenix.server.api.mutations.api_key_mutations import ApiKeyMutationMixin
5
5
  from phoenix.server.api.mutations.chat_mutations import (
6
6
  ChatCompletionMutationMixin,
7
7
  )
8
+ from phoenix.server.api.mutations.dataset_label_mutations import DatasetLabelMutationMixin
8
9
  from phoenix.server.api.mutations.dataset_mutations import DatasetMutationMixin
10
+ from phoenix.server.api.mutations.dataset_split_mutations import DatasetSplitMutationMixin
9
11
  from phoenix.server.api.mutations.experiment_mutations import ExperimentMutationMixin
10
12
  from phoenix.server.api.mutations.export_events_mutations import ExportEventsMutationMixin
11
13
  from phoenix.server.api.mutations.model_mutations import ModelMutationMixin
12
14
  from phoenix.server.api.mutations.project_mutations import ProjectMutationMixin
15
+ from phoenix.server.api.mutations.project_session_annotations_mutations import (
16
+ ProjectSessionAnnotationMutationMixin,
17
+ )
13
18
  from phoenix.server.api.mutations.project_trace_retention_policy_mutations import (
14
19
  ProjectTraceRetentionPolicyMutationMixin,
15
20
  )
@@ -27,7 +32,9 @@ class Mutation(
27
32
  AnnotationConfigMutationMixin,
28
33
  ApiKeyMutationMixin,
29
34
  ChatCompletionMutationMixin,
35
+ DatasetLabelMutationMixin,
30
36
  DatasetMutationMixin,
37
+ DatasetSplitMutationMixin,
31
38
  ExperimentMutationMixin,
32
39
  ExportEventsMutationMixin,
33
40
  ModelMutationMixin,
@@ -37,6 +44,7 @@ class Mutation(
37
44
  PromptVersionTagMutationMixin,
38
45
  PromptLabelMutationMixin,
39
46
  SpanAnnotationMutationMixin,
47
+ ProjectSessionAnnotationMutationMixin,
40
48
  TraceAnnotationMutationMixin,
41
49
  TraceMutationMixin,
42
50
  UserMutationMixin,
@@ -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],
@@ -374,10 +374,10 @@ class AnnotationConfigMutationMixin:
374
374
  )
375
375
  return AddAnnotationConfigToProjectPayload(
376
376
  query=Query(),
377
- project=Project(project_rowid=project_id),
377
+ project=Project(id=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],
@@ -409,5 +409,5 @@ class AnnotationConfigMutationMixin:
409
409
  raise NotFound("Could not find one or more input project annotation configs")
410
410
  return RemoveAnnotationConfigFromProjectPayload(
411
411
  query=Query(),
412
- project=Project(project_rowid=project_id),
412
+ project=Project(id=project_id),
413
413
  )