arize-phoenix 8.32.0__py3-none-any.whl → 9.0.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-8.32.0.dist-info → arize_phoenix-9.0.0.dist-info}/METADATA +3 -2
- {arize_phoenix-8.32.0.dist-info → arize_phoenix-9.0.0.dist-info}/RECORD +78 -58
- phoenix/db/constants.py +1 -0
- phoenix/db/facilitator.py +55 -0
- phoenix/db/insertion/document_annotation.py +31 -13
- phoenix/db/insertion/evaluation.py +15 -3
- phoenix/db/insertion/helpers.py +2 -1
- phoenix/db/insertion/span_annotation.py +26 -9
- phoenix/db/insertion/trace_annotation.py +25 -9
- phoenix/db/insertion/types.py +7 -0
- phoenix/db/migrations/versions/2f9d1a65945f_annotation_config_migration.py +322 -0
- phoenix/db/migrations/versions/8a3764fe7f1a_change_jsonb_to_json_for_prompts.py +76 -0
- phoenix/db/migrations/versions/bb8139330879_create_project_trace_retention_policies_table.py +77 -0
- phoenix/db/models.py +151 -10
- phoenix/db/types/annotation_configs.py +97 -0
- phoenix/db/types/db_models.py +41 -0
- phoenix/db/types/trace_retention.py +267 -0
- phoenix/experiments/functions.py +5 -1
- phoenix/server/api/auth.py +9 -0
- phoenix/server/api/context.py +5 -0
- phoenix/server/api/dataloaders/__init__.py +4 -0
- phoenix/server/api/dataloaders/annotation_summaries.py +203 -24
- phoenix/server/api/dataloaders/project_ids_by_trace_retention_policy_id.py +42 -0
- phoenix/server/api/dataloaders/trace_retention_policy_id_by_project_id.py +34 -0
- phoenix/server/api/helpers/annotations.py +9 -0
- phoenix/server/api/helpers/prompts/models.py +34 -67
- phoenix/server/api/input_types/CreateSpanAnnotationInput.py +9 -0
- phoenix/server/api/input_types/CreateTraceAnnotationInput.py +3 -0
- phoenix/server/api/input_types/PatchAnnotationInput.py +3 -0
- phoenix/server/api/input_types/SpanAnnotationFilter.py +67 -0
- phoenix/server/api/mutations/__init__.py +6 -0
- phoenix/server/api/mutations/annotation_config_mutations.py +413 -0
- phoenix/server/api/mutations/dataset_mutations.py +62 -39
- phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +245 -0
- phoenix/server/api/mutations/span_annotations_mutations.py +272 -70
- phoenix/server/api/mutations/trace_annotations_mutations.py +203 -74
- phoenix/server/api/queries.py +86 -0
- phoenix/server/api/routers/v1/__init__.py +4 -0
- phoenix/server/api/routers/v1/annotation_configs.py +449 -0
- phoenix/server/api/routers/v1/annotations.py +161 -0
- phoenix/server/api/routers/v1/evaluations.py +6 -0
- phoenix/server/api/routers/v1/projects.py +1 -50
- phoenix/server/api/routers/v1/spans.py +37 -8
- phoenix/server/api/routers/v1/traces.py +22 -13
- phoenix/server/api/routers/v1/utils.py +60 -0
- phoenix/server/api/types/Annotation.py +7 -0
- phoenix/server/api/types/AnnotationConfig.py +124 -0
- phoenix/server/api/types/AnnotationSource.py +9 -0
- phoenix/server/api/types/AnnotationSummary.py +28 -14
- phoenix/server/api/types/AnnotatorKind.py +1 -0
- phoenix/server/api/types/CronExpression.py +15 -0
- phoenix/server/api/types/Evaluation.py +4 -30
- phoenix/server/api/types/Project.py +50 -2
- phoenix/server/api/types/ProjectTraceRetentionPolicy.py +110 -0
- phoenix/server/api/types/Span.py +78 -0
- phoenix/server/api/types/SpanAnnotation.py +24 -0
- phoenix/server/api/types/Trace.py +2 -2
- phoenix/server/api/types/TraceAnnotation.py +23 -0
- phoenix/server/app.py +20 -0
- phoenix/server/retention.py +76 -0
- phoenix/server/static/.vite/manifest.json +36 -36
- phoenix/server/static/assets/components-B2MWTXnm.js +4326 -0
- phoenix/server/static/assets/{index-B0CbpsxD.js → index-Bfvpea_-.js} +10 -10
- phoenix/server/static/assets/pages-CZ2vKu8H.js +7268 -0
- phoenix/server/static/assets/vendor-BRDkBC5J.js +903 -0
- phoenix/server/static/assets/{vendor-arizeai-CxXYQNUl.js → vendor-arizeai-BvTqp_W8.js} +3 -3
- phoenix/server/static/assets/{vendor-codemirror-B0NIFPOL.js → vendor-codemirror-COt9UfW7.js} +1 -1
- phoenix/server/static/assets/{vendor-recharts-CrrDFWK1.js → vendor-recharts-BoHX9Hvs.js} +2 -2
- phoenix/server/static/assets/{vendor-shiki-C5bJ-RPf.js → vendor-shiki-Cw1dsDAz.js} +1 -1
- phoenix/session/client.py +6 -1
- phoenix/trace/dsl/filter.py +25 -5
- phoenix/trace/dsl/query.py +93 -13
- phoenix/utilities/__init__.py +18 -0
- phoenix/version.py +1 -1
- phoenix/server/static/assets/components-x-gKFJ8C.js +0 -3414
- phoenix/server/static/assets/pages-BU4VdyeH.js +0 -5867
- phoenix/server/static/assets/vendor-BfhM_F1u.js +0 -902
- {arize_phoenix-8.32.0.dist-info → arize_phoenix-9.0.0.dist-info}/WHEEL +0 -0
- {arize_phoenix-8.32.0.dist-info → arize_phoenix-9.0.0.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-8.32.0.dist-info → arize_phoenix-9.0.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-8.32.0.dist-info → arize_phoenix-9.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import strawberry
|
|
4
|
+
from sqlalchemy import delete, select, tuple_
|
|
5
|
+
from sqlalchemy.exc import IntegrityError as PostgreSQLIntegrityError
|
|
6
|
+
from sqlean.dbapi2 import IntegrityError as SQLiteIntegrityError # type: ignore[import-untyped]
|
|
7
|
+
from strawberry.relay.types import GlobalID
|
|
8
|
+
from strawberry.types import Info
|
|
9
|
+
|
|
10
|
+
from phoenix.db import models
|
|
11
|
+
from phoenix.db.types.annotation_configs import (
|
|
12
|
+
AnnotationConfigType,
|
|
13
|
+
AnnotationType,
|
|
14
|
+
CategoricalAnnotationValue,
|
|
15
|
+
OptimizationDirection,
|
|
16
|
+
)
|
|
17
|
+
from phoenix.db.types.annotation_configs import (
|
|
18
|
+
CategoricalAnnotationConfig as CategoricalAnnotationConfigModel,
|
|
19
|
+
)
|
|
20
|
+
from phoenix.db.types.annotation_configs import (
|
|
21
|
+
ContinuousAnnotationConfig as ContinuousAnnotationConfigModel,
|
|
22
|
+
)
|
|
23
|
+
from phoenix.db.types.annotation_configs import (
|
|
24
|
+
FreeformAnnotationConfig as FreeformAnnotationConfigModel,
|
|
25
|
+
)
|
|
26
|
+
from phoenix.server.api.auth import IsNotReadOnly
|
|
27
|
+
from phoenix.server.api.context import Context
|
|
28
|
+
from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound
|
|
29
|
+
from phoenix.server.api.queries import Query
|
|
30
|
+
from phoenix.server.api.types.AnnotationConfig import (
|
|
31
|
+
AnnotationConfig,
|
|
32
|
+
CategoricalAnnotationConfig,
|
|
33
|
+
ContinuousAnnotationConfig,
|
|
34
|
+
FreeformAnnotationConfig,
|
|
35
|
+
to_gql_annotation_config,
|
|
36
|
+
)
|
|
37
|
+
from phoenix.server.api.types.node import from_global_id_with_expected_type
|
|
38
|
+
from phoenix.server.api.types.Project import Project
|
|
39
|
+
|
|
40
|
+
ANNOTATION_TYPE_NAMES = (
|
|
41
|
+
CategoricalAnnotationConfig.__name__,
|
|
42
|
+
ContinuousAnnotationConfig.__name__,
|
|
43
|
+
FreeformAnnotationConfig.__name__,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@strawberry.input
|
|
48
|
+
class CategoricalAnnotationConfigValueInput:
|
|
49
|
+
label: str
|
|
50
|
+
score: Optional[float] = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@strawberry.input
|
|
54
|
+
class CategoricalAnnotationConfigInput:
|
|
55
|
+
name: str
|
|
56
|
+
description: Optional[str] = None
|
|
57
|
+
optimization_direction: OptimizationDirection
|
|
58
|
+
values: list[CategoricalAnnotationConfigValueInput]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@strawberry.input
|
|
62
|
+
class ContinuousAnnotationConfigInput:
|
|
63
|
+
name: str
|
|
64
|
+
description: Optional[str] = None
|
|
65
|
+
optimization_direction: OptimizationDirection
|
|
66
|
+
lower_bound: Optional[float] = None
|
|
67
|
+
upper_bound: Optional[float] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@strawberry.input
|
|
71
|
+
class FreeformAnnotationConfigInput:
|
|
72
|
+
name: str
|
|
73
|
+
description: Optional[str] = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@strawberry.input(one_of=True)
|
|
77
|
+
class AnnotationConfigInput:
|
|
78
|
+
categorical: Optional[CategoricalAnnotationConfigInput] = strawberry.UNSET
|
|
79
|
+
continuous: Optional[ContinuousAnnotationConfigInput] = strawberry.UNSET
|
|
80
|
+
freeform: Optional[FreeformAnnotationConfigInput] = strawberry.UNSET
|
|
81
|
+
|
|
82
|
+
def __post_init__(self) -> None:
|
|
83
|
+
if (
|
|
84
|
+
sum(
|
|
85
|
+
[
|
|
86
|
+
self.categorical is not strawberry.UNSET,
|
|
87
|
+
self.continuous is not strawberry.UNSET,
|
|
88
|
+
self.freeform is not strawberry.UNSET,
|
|
89
|
+
]
|
|
90
|
+
)
|
|
91
|
+
!= 1
|
|
92
|
+
):
|
|
93
|
+
raise BadRequest("Exactly one of categorical, continuous, or freeform must be set")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@strawberry.input
|
|
97
|
+
class CreateAnnotationConfigInput:
|
|
98
|
+
annotation_config: AnnotationConfigInput
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@strawberry.type
|
|
102
|
+
class CreateAnnotationConfigPayload:
|
|
103
|
+
query: Query
|
|
104
|
+
annotation_config: AnnotationConfig
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@strawberry.input
|
|
108
|
+
class UpdateAnnotationConfigInput:
|
|
109
|
+
id: GlobalID
|
|
110
|
+
annotation_config: AnnotationConfigInput
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@strawberry.type
|
|
114
|
+
class UpdateAnnotationConfigPayload:
|
|
115
|
+
query: Query
|
|
116
|
+
annotation_config: AnnotationConfig
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@strawberry.input
|
|
120
|
+
class DeleteAnnotationConfigsInput:
|
|
121
|
+
ids: list[GlobalID]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@strawberry.type
|
|
125
|
+
class DeleteAnnotationConfigsPayload:
|
|
126
|
+
query: Query
|
|
127
|
+
annotation_configs: list[AnnotationConfig]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@strawberry.input
|
|
131
|
+
class AddAnnotationConfigToProjectInput:
|
|
132
|
+
project_id: GlobalID
|
|
133
|
+
annotation_config_id: GlobalID
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@strawberry.type
|
|
137
|
+
class AddAnnotationConfigToProjectPayload:
|
|
138
|
+
query: Query
|
|
139
|
+
project: Project
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@strawberry.input
|
|
143
|
+
class RemoveAnnotationConfigFromProjectInput:
|
|
144
|
+
project_id: GlobalID
|
|
145
|
+
annotation_config_id: GlobalID
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@strawberry.type
|
|
149
|
+
class RemoveAnnotationConfigFromProjectPayload:
|
|
150
|
+
query: Query
|
|
151
|
+
project: Project
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _to_pydantic_categorical_annotation_config(
|
|
155
|
+
input: CategoricalAnnotationConfigInput,
|
|
156
|
+
) -> CategoricalAnnotationConfigModel:
|
|
157
|
+
try:
|
|
158
|
+
return CategoricalAnnotationConfigModel(
|
|
159
|
+
type=AnnotationType.CATEGORICAL.value,
|
|
160
|
+
description=input.description,
|
|
161
|
+
optimization_direction=input.optimization_direction,
|
|
162
|
+
values=[
|
|
163
|
+
CategoricalAnnotationValue(label=value.label, score=value.score)
|
|
164
|
+
for value in input.values
|
|
165
|
+
],
|
|
166
|
+
)
|
|
167
|
+
except ValueError as error:
|
|
168
|
+
raise BadRequest(str(error))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _to_pydantic_continuous_annotation_config(
|
|
172
|
+
input: ContinuousAnnotationConfigInput,
|
|
173
|
+
) -> ContinuousAnnotationConfigModel:
|
|
174
|
+
try:
|
|
175
|
+
return ContinuousAnnotationConfigModel(
|
|
176
|
+
type=AnnotationType.CONTINUOUS.value,
|
|
177
|
+
description=input.description,
|
|
178
|
+
optimization_direction=input.optimization_direction,
|
|
179
|
+
lower_bound=input.lower_bound,
|
|
180
|
+
upper_bound=input.upper_bound,
|
|
181
|
+
)
|
|
182
|
+
except ValueError as error:
|
|
183
|
+
raise BadRequest(str(error))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _to_pydantic_freeform_annotation_config(
|
|
187
|
+
input: FreeformAnnotationConfigInput,
|
|
188
|
+
) -> FreeformAnnotationConfigModel:
|
|
189
|
+
try:
|
|
190
|
+
return FreeformAnnotationConfigModel(
|
|
191
|
+
type=AnnotationType.FREEFORM.value,
|
|
192
|
+
description=input.description,
|
|
193
|
+
)
|
|
194
|
+
except ValueError as error:
|
|
195
|
+
raise BadRequest(str(error))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@strawberry.type
|
|
199
|
+
class AnnotationConfigMutationMixin:
|
|
200
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore[misc]
|
|
201
|
+
async def create_annotation_config(
|
|
202
|
+
self,
|
|
203
|
+
info: Info[Context, None],
|
|
204
|
+
input: CreateAnnotationConfigInput,
|
|
205
|
+
) -> CreateAnnotationConfigPayload:
|
|
206
|
+
input_annotation_config = input.annotation_config
|
|
207
|
+
config: AnnotationConfigType
|
|
208
|
+
name: str
|
|
209
|
+
if categorical_input := input_annotation_config.categorical:
|
|
210
|
+
name = categorical_input.name
|
|
211
|
+
config = _to_pydantic_categorical_annotation_config(categorical_input)
|
|
212
|
+
elif continuous_input := input_annotation_config.continuous:
|
|
213
|
+
name = input_annotation_config.continuous.name
|
|
214
|
+
config = _to_pydantic_continuous_annotation_config(continuous_input)
|
|
215
|
+
elif freeform_input := input_annotation_config.freeform:
|
|
216
|
+
name = freeform_input.name
|
|
217
|
+
config = _to_pydantic_freeform_annotation_config(freeform_input)
|
|
218
|
+
else:
|
|
219
|
+
raise BadRequest("No annotation config provided")
|
|
220
|
+
|
|
221
|
+
if name == "note":
|
|
222
|
+
raise BadRequest("The name 'note' is reserved for span notes")
|
|
223
|
+
|
|
224
|
+
async with info.context.db() as session:
|
|
225
|
+
annotation_config = models.AnnotationConfig(
|
|
226
|
+
name=name,
|
|
227
|
+
config=config,
|
|
228
|
+
)
|
|
229
|
+
session.add(annotation_config)
|
|
230
|
+
try:
|
|
231
|
+
await session.commit()
|
|
232
|
+
except (PostgreSQLIntegrityError, SQLiteIntegrityError):
|
|
233
|
+
raise Conflict(f"Annotation configuration with name '{name}' already exists")
|
|
234
|
+
return CreateAnnotationConfigPayload(
|
|
235
|
+
query=Query(),
|
|
236
|
+
annotation_config=to_gql_annotation_config(annotation_config),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore[misc]
|
|
240
|
+
async def update_annotation_config(
|
|
241
|
+
self,
|
|
242
|
+
info: Info[Context, None],
|
|
243
|
+
input: UpdateAnnotationConfigInput,
|
|
244
|
+
) -> UpdateAnnotationConfigPayload:
|
|
245
|
+
try:
|
|
246
|
+
config_id = int(input.id.node_id)
|
|
247
|
+
except ValueError:
|
|
248
|
+
raise BadRequest("Invalid annotation config ID")
|
|
249
|
+
|
|
250
|
+
if input.id.type_name not in ANNOTATION_TYPE_NAMES:
|
|
251
|
+
raise BadRequest("Invalid annotation config ID")
|
|
252
|
+
|
|
253
|
+
input_annotation_config = input.annotation_config
|
|
254
|
+
config: AnnotationConfigType
|
|
255
|
+
name: str
|
|
256
|
+
if categorical_input := input_annotation_config.categorical:
|
|
257
|
+
name = categorical_input.name
|
|
258
|
+
config = _to_pydantic_categorical_annotation_config(categorical_input)
|
|
259
|
+
elif continuous_input := input_annotation_config.continuous:
|
|
260
|
+
name = input_annotation_config.continuous.name
|
|
261
|
+
config = _to_pydantic_continuous_annotation_config(continuous_input)
|
|
262
|
+
elif freeform_input := input_annotation_config.freeform:
|
|
263
|
+
name = freeform_input.name
|
|
264
|
+
config = _to_pydantic_freeform_annotation_config(freeform_input)
|
|
265
|
+
else:
|
|
266
|
+
raise BadRequest("No annotation config provided")
|
|
267
|
+
|
|
268
|
+
if name == "note":
|
|
269
|
+
raise BadRequest("The name 'note' is reserved for span notes")
|
|
270
|
+
|
|
271
|
+
async with info.context.db() as session:
|
|
272
|
+
annotation_config = await session.get(models.AnnotationConfig, config_id)
|
|
273
|
+
if not annotation_config:
|
|
274
|
+
raise NotFound("Annotation config not found")
|
|
275
|
+
|
|
276
|
+
annotation_config.name = name
|
|
277
|
+
annotation_config.config = config
|
|
278
|
+
try:
|
|
279
|
+
await session.commit()
|
|
280
|
+
except (PostgreSQLIntegrityError, SQLiteIntegrityError):
|
|
281
|
+
raise Conflict(f"Annotation configuration with name '{name}' already exists")
|
|
282
|
+
|
|
283
|
+
return UpdateAnnotationConfigPayload(
|
|
284
|
+
query=Query(),
|
|
285
|
+
annotation_config=to_gql_annotation_config(annotation_config),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore[misc]
|
|
289
|
+
async def delete_annotation_configs(
|
|
290
|
+
self,
|
|
291
|
+
info: Info[Context, None],
|
|
292
|
+
input: DeleteAnnotationConfigsInput,
|
|
293
|
+
) -> DeleteAnnotationConfigsPayload:
|
|
294
|
+
config_ids = set()
|
|
295
|
+
for config_gid in input.ids:
|
|
296
|
+
if (type_name := config_gid.type_name) not in ANNOTATION_TYPE_NAMES:
|
|
297
|
+
raise BadRequest(f"Unexpected type name in Relay ID: {type_name}")
|
|
298
|
+
config_ids.add(int(config_gid.node_id))
|
|
299
|
+
|
|
300
|
+
async with info.context.db() as session:
|
|
301
|
+
result = await session.scalars(
|
|
302
|
+
delete(models.AnnotationConfig)
|
|
303
|
+
.where(models.AnnotationConfig.id.in_(config_ids))
|
|
304
|
+
.returning(models.AnnotationConfig)
|
|
305
|
+
)
|
|
306
|
+
deleted_annotation_configs = result.all()
|
|
307
|
+
if len(deleted_annotation_configs) < len(config_ids):
|
|
308
|
+
await session.rollback()
|
|
309
|
+
raise NotFound(
|
|
310
|
+
"Could not find one or more annotation configs to delete, deletion aborted."
|
|
311
|
+
)
|
|
312
|
+
return DeleteAnnotationConfigsPayload(
|
|
313
|
+
query=Query(),
|
|
314
|
+
annotation_configs=[
|
|
315
|
+
to_gql_annotation_config(annotation_config)
|
|
316
|
+
for annotation_config in deleted_annotation_configs
|
|
317
|
+
],
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore[misc]
|
|
321
|
+
async def add_annotation_config_to_project(
|
|
322
|
+
self,
|
|
323
|
+
info: Info[Context, None],
|
|
324
|
+
input: list[AddAnnotationConfigToProjectInput],
|
|
325
|
+
) -> AddAnnotationConfigToProjectPayload:
|
|
326
|
+
if not input:
|
|
327
|
+
raise BadRequest("No project annotation config associations provided")
|
|
328
|
+
project_annotation_config_ids: set[tuple[int, int]] = set()
|
|
329
|
+
for item in input:
|
|
330
|
+
project_id = from_global_id_with_expected_type(
|
|
331
|
+
global_id=item.project_id, expected_type_name="Project"
|
|
332
|
+
)
|
|
333
|
+
if (item.annotation_config_id.type_name) not in ANNOTATION_TYPE_NAMES:
|
|
334
|
+
raise BadRequest(
|
|
335
|
+
f"Invalidation ID for annotation config: {str(item.annotation_config_id)}"
|
|
336
|
+
)
|
|
337
|
+
annotation_config_id = int(item.annotation_config_id.node_id)
|
|
338
|
+
project_annotation_config_ids.add((project_id, annotation_config_id))
|
|
339
|
+
project_ids = [project_id for project_id, _ in project_annotation_config_ids]
|
|
340
|
+
annotation_config_ids = [
|
|
341
|
+
annotation_config_id for _, annotation_config_id in project_annotation_config_ids
|
|
342
|
+
]
|
|
343
|
+
|
|
344
|
+
async with info.context.db() as session:
|
|
345
|
+
result = await session.scalars(
|
|
346
|
+
select(models.Project.id).where(models.Project.id.in_(project_ids))
|
|
347
|
+
)
|
|
348
|
+
resolved_project_ids = result.all()
|
|
349
|
+
if set(project_ids) - set(resolved_project_ids):
|
|
350
|
+
raise NotFound("One or more projects were not found")
|
|
351
|
+
|
|
352
|
+
result = await session.scalars(
|
|
353
|
+
select(models.AnnotationConfig.id).where(
|
|
354
|
+
models.AnnotationConfig.id.in_(annotation_config_ids)
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
resolved_annotation_config_ids = result.all()
|
|
358
|
+
if set(annotation_config_ids) - set(resolved_annotation_config_ids):
|
|
359
|
+
raise NotFound("One or more annotation configs were not found")
|
|
360
|
+
|
|
361
|
+
for project_id, annotation_config_id in project_annotation_config_ids:
|
|
362
|
+
project_annotation_config = models.ProjectAnnotationConfig(
|
|
363
|
+
project_id=project_id,
|
|
364
|
+
annotation_config_id=annotation_config_id,
|
|
365
|
+
)
|
|
366
|
+
session.add(project_annotation_config)
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
await session.commit()
|
|
370
|
+
except (PostgreSQLIntegrityError, SQLiteIntegrityError):
|
|
371
|
+
await session.rollback()
|
|
372
|
+
raise Conflict(
|
|
373
|
+
"One or more annotation configs have already been added to the project"
|
|
374
|
+
)
|
|
375
|
+
return AddAnnotationConfigToProjectPayload(
|
|
376
|
+
query=Query(),
|
|
377
|
+
project=Project(project_rowid=project_id),
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore[misc]
|
|
381
|
+
async def remove_annotation_config_from_project(
|
|
382
|
+
self,
|
|
383
|
+
info: Info[Context, None],
|
|
384
|
+
input: list[RemoveAnnotationConfigFromProjectInput],
|
|
385
|
+
) -> RemoveAnnotationConfigFromProjectPayload:
|
|
386
|
+
project_annotation_config_associations = set()
|
|
387
|
+
for item in input:
|
|
388
|
+
project_id = from_global_id_with_expected_type(
|
|
389
|
+
global_id=item.project_id, expected_type_name="Project"
|
|
390
|
+
)
|
|
391
|
+
if (type_name := item.annotation_config_id.type_name) not in ANNOTATION_TYPE_NAMES:
|
|
392
|
+
raise BadRequest(f"Unexpected type name in Relay ID: {type_name}")
|
|
393
|
+
annotation_config_id = int(item.annotation_config_id.node_id)
|
|
394
|
+
project_annotation_config_associations.add((project_id, annotation_config_id))
|
|
395
|
+
async with info.context.db() as session:
|
|
396
|
+
result = await session.scalars(
|
|
397
|
+
delete(models.ProjectAnnotationConfig)
|
|
398
|
+
.where(
|
|
399
|
+
tuple_(
|
|
400
|
+
models.ProjectAnnotationConfig.project_id,
|
|
401
|
+
models.ProjectAnnotationConfig.annotation_config_id,
|
|
402
|
+
).in_(project_annotation_config_associations)
|
|
403
|
+
)
|
|
404
|
+
.returning(models.ProjectAnnotationConfig)
|
|
405
|
+
)
|
|
406
|
+
annotation_configs = result.all()
|
|
407
|
+
if len(annotation_configs) < len(project_annotation_config_associations):
|
|
408
|
+
await session.rollback()
|
|
409
|
+
raise NotFound("Could not find one or more input project annotation configs")
|
|
410
|
+
return RemoveAnnotationConfigFromProjectPayload(
|
|
411
|
+
query=Query(),
|
|
412
|
+
project=Project(project_rowid=project_id),
|
|
413
|
+
)
|
|
@@ -11,7 +11,9 @@ from openinference.semconv.trace import (
|
|
|
11
11
|
ToolCallAttributes,
|
|
12
12
|
)
|
|
13
13
|
from sqlalchemy import and_, delete, distinct, func, insert, select, update
|
|
14
|
+
from sqlalchemy.orm import contains_eager
|
|
14
15
|
from strawberry import UNSET
|
|
16
|
+
from strawberry.relay.types import GlobalID
|
|
15
17
|
from strawberry.types import Info
|
|
16
18
|
|
|
17
19
|
from phoenix.db import models
|
|
@@ -130,43 +132,40 @@ class DatasetMutationMixin:
|
|
|
130
132
|
raise ValueError(
|
|
131
133
|
f"Unknown dataset: {dataset_id}"
|
|
132
134
|
) # todo: implement error types https://github.com/Arize-ai/phoenix/issues/3221
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
description=dataset_version_description,
|
|
138
|
-
metadata_=dataset_version_metadata,
|
|
139
|
-
)
|
|
140
|
-
.returning(models.DatasetVersion.id)
|
|
135
|
+
dataset_version = models.DatasetVersion(
|
|
136
|
+
dataset_id=dataset_rowid,
|
|
137
|
+
description=dataset_version_description,
|
|
138
|
+
metadata_=dataset_version_metadata or {},
|
|
141
139
|
)
|
|
140
|
+
session.add(dataset_version)
|
|
141
|
+
await session.flush()
|
|
142
142
|
spans = (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
143
|
+
(
|
|
144
|
+
await session.scalars(
|
|
145
|
+
select(models.Span)
|
|
146
|
+
.outerjoin(
|
|
147
|
+
models.SpanAnnotation,
|
|
148
|
+
models.Span.id == models.SpanAnnotation.span_rowid,
|
|
149
|
+
)
|
|
150
|
+
.outerjoin(models.User, models.SpanAnnotation.user_id == models.User.id)
|
|
151
|
+
.order_by(
|
|
152
|
+
models.Span.id,
|
|
153
|
+
models.SpanAnnotation.name,
|
|
154
|
+
models.User.username,
|
|
155
|
+
)
|
|
156
|
+
.where(models.Span.id.in_(span_rowids))
|
|
157
|
+
.options(
|
|
158
|
+
contains_eager(models.Span.span_annotations).contains_eager(
|
|
159
|
+
models.SpanAnnotation.user
|
|
160
|
+
)
|
|
161
|
+
)
|
|
154
162
|
)
|
|
155
163
|
)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
for
|
|
160
|
-
|
|
161
|
-
if span_id not in span_annotations_by_span:
|
|
162
|
-
span_annotations_by_span[span_id] = dict()
|
|
163
|
-
span_annotations_by_span[span_id][annotation.name] = {
|
|
164
|
-
"label": annotation.label,
|
|
165
|
-
"score": annotation.score,
|
|
166
|
-
"explanation": annotation.explanation,
|
|
167
|
-
"metadata": annotation.metadata_,
|
|
168
|
-
"annotator_kind": annotation.annotator_kind,
|
|
169
|
-
}
|
|
164
|
+
.unique()
|
|
165
|
+
.all()
|
|
166
|
+
)
|
|
167
|
+
if span_rowids - {span.id for span in spans}:
|
|
168
|
+
raise NotFound("Some spans could not be found")
|
|
170
169
|
|
|
171
170
|
DatasetExample = models.DatasetExample
|
|
172
171
|
dataset_example_rowids = (
|
|
@@ -201,7 +200,7 @@ class DatasetMutationMixin:
|
|
|
201
200
|
[
|
|
202
201
|
{
|
|
203
202
|
DatasetExampleRevision.dataset_example_id.key: dataset_example_rowid,
|
|
204
|
-
DatasetExampleRevision.dataset_version_id.key:
|
|
203
|
+
DatasetExampleRevision.dataset_version_id.key: dataset_version.id,
|
|
205
204
|
DatasetExampleRevision.input.key: get_dataset_example_input(span),
|
|
206
205
|
DatasetExampleRevision.output.key: get_dataset_example_output(span),
|
|
207
206
|
DatasetExampleRevision.metadata_.key: {
|
|
@@ -212,11 +211,7 @@ class DatasetMutationMixin:
|
|
|
212
211
|
if k in nonprivate_span_attributes
|
|
213
212
|
},
|
|
214
213
|
"span_kind": span.span_kind,
|
|
215
|
-
|
|
216
|
-
{"annotations": annotations}
|
|
217
|
-
if (annotations := span_annotations_by_span[span.id])
|
|
218
|
-
else {}
|
|
219
|
-
),
|
|
214
|
+
"annotations": _gather_span_annotations_by_name(span.span_annotations),
|
|
220
215
|
},
|
|
221
216
|
DatasetExampleRevision.revision_kind.key: "CREATE",
|
|
222
217
|
}
|
|
@@ -602,6 +597,34 @@ def _to_orm_revision(
|
|
|
602
597
|
}
|
|
603
598
|
|
|
604
599
|
|
|
600
|
+
def _gather_span_annotations_by_name(
|
|
601
|
+
span_annotations: list[models.SpanAnnotation],
|
|
602
|
+
) -> dict[str, list[dict[str, Any]]]:
|
|
603
|
+
span_annotations_by_name: dict[str, list[dict[str, Any]]] = {}
|
|
604
|
+
for span_annotation in span_annotations:
|
|
605
|
+
if span_annotation.name not in span_annotations_by_name:
|
|
606
|
+
span_annotations_by_name[span_annotation.name] = []
|
|
607
|
+
span_annotations_by_name[span_annotation.name].append(
|
|
608
|
+
_to_span_annotation_dict(span_annotation)
|
|
609
|
+
)
|
|
610
|
+
return span_annotations_by_name
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _to_span_annotation_dict(span_annotation: models.SpanAnnotation) -> dict[str, Any]:
|
|
614
|
+
return {
|
|
615
|
+
"label": span_annotation.label,
|
|
616
|
+
"score": span_annotation.score,
|
|
617
|
+
"explanation": span_annotation.explanation,
|
|
618
|
+
"metadata": span_annotation.metadata_,
|
|
619
|
+
"annotator_kind": span_annotation.annotator_kind,
|
|
620
|
+
"user_id": str(GlobalID(models.User.__name__, str(user_id)))
|
|
621
|
+
if (user_id := span_annotation.user_id) is not None
|
|
622
|
+
else None,
|
|
623
|
+
"username": user.username if (user := span_annotation.user) is not None else None,
|
|
624
|
+
"email": user.email if user is not None else None,
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
|
|
605
628
|
INPUT_MIME_TYPE = SpanAttributes.INPUT_MIME_TYPE
|
|
606
629
|
INPUT_VALUE = SpanAttributes.INPUT_VALUE
|
|
607
630
|
OUTPUT_MIME_TYPE = SpanAttributes.OUTPUT_MIME_TYPE
|