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.

Files changed (81) hide show
  1. {arize_phoenix-8.32.0.dist-info → arize_phoenix-9.0.0.dist-info}/METADATA +3 -2
  2. {arize_phoenix-8.32.0.dist-info → arize_phoenix-9.0.0.dist-info}/RECORD +78 -58
  3. phoenix/db/constants.py +1 -0
  4. phoenix/db/facilitator.py +55 -0
  5. phoenix/db/insertion/document_annotation.py +31 -13
  6. phoenix/db/insertion/evaluation.py +15 -3
  7. phoenix/db/insertion/helpers.py +2 -1
  8. phoenix/db/insertion/span_annotation.py +26 -9
  9. phoenix/db/insertion/trace_annotation.py +25 -9
  10. phoenix/db/insertion/types.py +7 -0
  11. phoenix/db/migrations/versions/2f9d1a65945f_annotation_config_migration.py +322 -0
  12. phoenix/db/migrations/versions/8a3764fe7f1a_change_jsonb_to_json_for_prompts.py +76 -0
  13. phoenix/db/migrations/versions/bb8139330879_create_project_trace_retention_policies_table.py +77 -0
  14. phoenix/db/models.py +151 -10
  15. phoenix/db/types/annotation_configs.py +97 -0
  16. phoenix/db/types/db_models.py +41 -0
  17. phoenix/db/types/trace_retention.py +267 -0
  18. phoenix/experiments/functions.py +5 -1
  19. phoenix/server/api/auth.py +9 -0
  20. phoenix/server/api/context.py +5 -0
  21. phoenix/server/api/dataloaders/__init__.py +4 -0
  22. phoenix/server/api/dataloaders/annotation_summaries.py +203 -24
  23. phoenix/server/api/dataloaders/project_ids_by_trace_retention_policy_id.py +42 -0
  24. phoenix/server/api/dataloaders/trace_retention_policy_id_by_project_id.py +34 -0
  25. phoenix/server/api/helpers/annotations.py +9 -0
  26. phoenix/server/api/helpers/prompts/models.py +34 -67
  27. phoenix/server/api/input_types/CreateSpanAnnotationInput.py +9 -0
  28. phoenix/server/api/input_types/CreateTraceAnnotationInput.py +3 -0
  29. phoenix/server/api/input_types/PatchAnnotationInput.py +3 -0
  30. phoenix/server/api/input_types/SpanAnnotationFilter.py +67 -0
  31. phoenix/server/api/mutations/__init__.py +6 -0
  32. phoenix/server/api/mutations/annotation_config_mutations.py +413 -0
  33. phoenix/server/api/mutations/dataset_mutations.py +62 -39
  34. phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +245 -0
  35. phoenix/server/api/mutations/span_annotations_mutations.py +272 -70
  36. phoenix/server/api/mutations/trace_annotations_mutations.py +203 -74
  37. phoenix/server/api/queries.py +86 -0
  38. phoenix/server/api/routers/v1/__init__.py +4 -0
  39. phoenix/server/api/routers/v1/annotation_configs.py +449 -0
  40. phoenix/server/api/routers/v1/annotations.py +161 -0
  41. phoenix/server/api/routers/v1/evaluations.py +6 -0
  42. phoenix/server/api/routers/v1/projects.py +1 -50
  43. phoenix/server/api/routers/v1/spans.py +37 -8
  44. phoenix/server/api/routers/v1/traces.py +22 -13
  45. phoenix/server/api/routers/v1/utils.py +60 -0
  46. phoenix/server/api/types/Annotation.py +7 -0
  47. phoenix/server/api/types/AnnotationConfig.py +124 -0
  48. phoenix/server/api/types/AnnotationSource.py +9 -0
  49. phoenix/server/api/types/AnnotationSummary.py +28 -14
  50. phoenix/server/api/types/AnnotatorKind.py +1 -0
  51. phoenix/server/api/types/CronExpression.py +15 -0
  52. phoenix/server/api/types/Evaluation.py +4 -30
  53. phoenix/server/api/types/Project.py +50 -2
  54. phoenix/server/api/types/ProjectTraceRetentionPolicy.py +110 -0
  55. phoenix/server/api/types/Span.py +78 -0
  56. phoenix/server/api/types/SpanAnnotation.py +24 -0
  57. phoenix/server/api/types/Trace.py +2 -2
  58. phoenix/server/api/types/TraceAnnotation.py +23 -0
  59. phoenix/server/app.py +20 -0
  60. phoenix/server/retention.py +76 -0
  61. phoenix/server/static/.vite/manifest.json +36 -36
  62. phoenix/server/static/assets/components-B2MWTXnm.js +4326 -0
  63. phoenix/server/static/assets/{index-B0CbpsxD.js → index-Bfvpea_-.js} +10 -10
  64. phoenix/server/static/assets/pages-CZ2vKu8H.js +7268 -0
  65. phoenix/server/static/assets/vendor-BRDkBC5J.js +903 -0
  66. phoenix/server/static/assets/{vendor-arizeai-CxXYQNUl.js → vendor-arizeai-BvTqp_W8.js} +3 -3
  67. phoenix/server/static/assets/{vendor-codemirror-B0NIFPOL.js → vendor-codemirror-COt9UfW7.js} +1 -1
  68. phoenix/server/static/assets/{vendor-recharts-CrrDFWK1.js → vendor-recharts-BoHX9Hvs.js} +2 -2
  69. phoenix/server/static/assets/{vendor-shiki-C5bJ-RPf.js → vendor-shiki-Cw1dsDAz.js} +1 -1
  70. phoenix/session/client.py +6 -1
  71. phoenix/trace/dsl/filter.py +25 -5
  72. phoenix/trace/dsl/query.py +93 -13
  73. phoenix/utilities/__init__.py +18 -0
  74. phoenix/version.py +1 -1
  75. phoenix/server/static/assets/components-x-gKFJ8C.js +0 -3414
  76. phoenix/server/static/assets/pages-BU4VdyeH.js +0 -5867
  77. phoenix/server/static/assets/vendor-BfhM_F1u.js +0 -902
  78. {arize_phoenix-8.32.0.dist-info → arize_phoenix-9.0.0.dist-info}/WHEEL +0 -0
  79. {arize_phoenix-8.32.0.dist-info → arize_phoenix-9.0.0.dist-info}/entry_points.txt +0 -0
  80. {arize_phoenix-8.32.0.dist-info → arize_phoenix-9.0.0.dist-info}/licenses/IP_NOTICE +0 -0
  81. {arize_phoenix-8.32.0.dist-info → arize_phoenix-9.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,19 +1,23 @@
1
- from collections.abc import Sequence
1
+ from typing import Optional
2
2
 
3
3
  import strawberry
4
- from sqlalchemy import delete, insert, update
5
- from strawberry import UNSET
6
- from strawberry.types import Info
4
+ from sqlalchemy import delete, insert, select
5
+ from starlette.requests import Request
6
+ from strawberry import UNSET, Info
7
7
 
8
8
  from phoenix.db import models
9
9
  from phoenix.server.api.auth import IsLocked, IsNotReadOnly
10
10
  from phoenix.server.api.context import Context
11
+ from phoenix.server.api.exceptions import BadRequest, NotFound, Unauthorized
12
+ from phoenix.server.api.helpers.annotations import get_user_identifier
11
13
  from phoenix.server.api.input_types.CreateTraceAnnotationInput import CreateTraceAnnotationInput
12
14
  from phoenix.server.api.input_types.DeleteAnnotationsInput import DeleteAnnotationsInput
13
15
  from phoenix.server.api.input_types.PatchAnnotationInput import PatchAnnotationInput
14
16
  from phoenix.server.api.queries import Query
17
+ from phoenix.server.api.types.AnnotationSource import AnnotationSource
15
18
  from phoenix.server.api.types.node import from_global_id_with_expected_type
16
19
  from phoenix.server.api.types.TraceAnnotation import TraceAnnotation, to_gql_trace_annotation
20
+ from phoenix.server.bearer_auth import PhoenixUser
17
21
  from phoenix.server.dml_event import TraceAnnotationDeleteEvent, TraceAnnotationInsertEvent
18
22
 
19
23
 
@@ -29,33 +33,91 @@ class TraceAnnotationMutationMixin:
29
33
  async def create_trace_annotations(
30
34
  self, info: Info[Context, None], input: list[CreateTraceAnnotationInput]
31
35
  ) -> TraceAnnotationMutationPayload:
32
- inserted_annotations: Sequence[models.TraceAnnotation] = []
36
+ if not input:
37
+ raise BadRequest("No trace annotations provided.")
38
+
39
+ assert isinstance(request := info.context.request, Request)
40
+ user_id: Optional[int] = None
41
+ if "user" in request.scope and isinstance((user := info.context.user), PhoenixUser):
42
+ user_id = int(user.identity)
43
+
44
+ processed_annotations_map: dict[int, models.TraceAnnotation] = {}
45
+
46
+ trace_rowids = []
47
+ for idx, annotation_input in enumerate(input):
48
+ try:
49
+ trace_rowid = from_global_id_with_expected_type(annotation_input.trace_id, "Trace")
50
+ except ValueError:
51
+ raise BadRequest(
52
+ f"Invalid trace ID for annotation at index {idx}: "
53
+ f"{annotation_input.trace_id}"
54
+ )
55
+ trace_rowids.append(trace_rowid)
56
+
33
57
  async with info.context.db() as session:
34
- values_list = [
35
- dict(
36
- trace_rowid=from_global_id_with_expected_type(annotation.trace_id, "Trace"),
37
- name=annotation.name,
38
- label=annotation.label,
39
- score=annotation.score,
40
- explanation=annotation.explanation,
41
- annotator_kind=annotation.annotator_kind.value,
42
- metadata_=annotation.metadata,
58
+ for idx, (trace_rowid, annotation_input) in enumerate(zip(trace_rowids, input)):
59
+ resolved_identifier = ""
60
+ if isinstance(annotation_input.identifier, str):
61
+ resolved_identifier = annotation_input.identifier
62
+ elif annotation_input.source == AnnotationSource.APP and user_id is not None:
63
+ resolved_identifier = get_user_identifier(user_id)
64
+ values = {
65
+ "trace_rowid": trace_rowid,
66
+ "name": annotation_input.name,
67
+ "label": annotation_input.label,
68
+ "score": annotation_input.score,
69
+ "explanation": annotation_input.explanation,
70
+ "annotator_kind": annotation_input.annotator_kind.value,
71
+ "metadata_": annotation_input.metadata,
72
+ "identifier": resolved_identifier,
73
+ "source": annotation_input.source.value,
74
+ "user_id": user_id,
75
+ }
76
+
77
+ processed_annotation: Optional[models.TraceAnnotation] = None
78
+
79
+ # Check if an annotation with this trace_rowid, name, and identifier already exists
80
+ q = select(models.TraceAnnotation).where(
81
+ models.TraceAnnotation.trace_rowid == trace_rowid,
82
+ models.TraceAnnotation.name == annotation_input.name,
83
+ models.TraceAnnotation.identifier == resolved_identifier,
43
84
  )
44
- for annotation in input
45
- ]
46
- stmt = (
47
- insert(models.TraceAnnotation).values(values_list).returning(models.TraceAnnotation)
48
- )
49
- result = await session.scalars(stmt)
50
- inserted_annotations = result.all()
51
- if inserted_annotations:
52
- info.context.event_queue.put(
53
- TraceAnnotationInsertEvent(tuple(anno.id for anno in inserted_annotations))
54
- )
85
+ existing_annotation = await session.scalar(q)
86
+
87
+ if existing_annotation:
88
+ # Update existing annotation
89
+ existing_annotation.name = values["name"]
90
+ existing_annotation.label = values["label"]
91
+ existing_annotation.score = values["score"]
92
+ existing_annotation.explanation = values["explanation"]
93
+ existing_annotation.metadata_ = values["metadata_"]
94
+ existing_annotation.annotator_kind = values["annotator_kind"]
95
+ existing_annotation.source = values["source"]
96
+ existing_annotation.user_id = values["user_id"]
97
+ session.add(existing_annotation)
98
+ processed_annotation = existing_annotation
99
+
100
+ if processed_annotation is None:
101
+ stmt = insert(models.TraceAnnotation).values(**values)
102
+ stmt = stmt.returning(models.TraceAnnotation)
103
+ result = await session.scalars(stmt)
104
+ processed_annotation = result.one()
105
+
106
+ processed_annotations_map[idx] = processed_annotation
107
+
108
+ await session.commit()
109
+
110
+ inserted_annotation_ids = tuple(anno.id for anno in processed_annotations_map.values())
111
+ if inserted_annotation_ids:
112
+ info.context.event_queue.put(TraceAnnotationInsertEvent(inserted_annotation_ids))
113
+
114
+ returned_annotations = [
115
+ to_gql_trace_annotation(processed_annotations_map[i])
116
+ for i in sorted(processed_annotations_map.keys())
117
+ ]
118
+
55
119
  return TraceAnnotationMutationPayload(
56
- trace_annotations=[
57
- to_gql_trace_annotation(annotation) for annotation in inserted_annotations
58
- ],
120
+ trace_annotations=returned_annotations,
59
121
  query=Query(),
60
122
  )
61
123
 
@@ -63,65 +125,132 @@ class TraceAnnotationMutationMixin:
63
125
  async def patch_trace_annotations(
64
126
  self, info: Info[Context, None], input: list[PatchAnnotationInput]
65
127
  ) -> TraceAnnotationMutationPayload:
66
- patched_annotations = []
67
- async with info.context.db() as session:
68
- for annotation in input:
128
+ if not input:
129
+ raise BadRequest("No trace annotations provided.")
130
+
131
+ assert isinstance(request := info.context.request, Request)
132
+ user_id: Optional[int] = None
133
+ if "user" in request.scope and isinstance((user := info.context.user), PhoenixUser):
134
+ user_id = int(user.identity)
135
+
136
+ patch_by_id = {}
137
+ for patch in input:
138
+ try:
69
139
  trace_annotation_id = from_global_id_with_expected_type(
70
- annotation.annotation_id, "TraceAnnotation"
140
+ patch.annotation_id, "TraceAnnotation"
141
+ )
142
+ except ValueError:
143
+ raise BadRequest(f"Invalid trace annotation ID: {patch.annotation_id}")
144
+ if trace_annotation_id in patch_by_id:
145
+ raise BadRequest(f"Duplicate patch for trace annotation ID: {trace_annotation_id}")
146
+ patch_by_id[trace_annotation_id] = patch
147
+
148
+ async with info.context.db() as session:
149
+ trace_annotations_by_id = {}
150
+ for trace_annotation in await session.scalars(
151
+ select(models.TraceAnnotation).where(
152
+ models.TraceAnnotation.id.in_(patch_by_id.keys())
71
153
  )
72
- patch = {
73
- column.key: patch_value
74
- for column, patch_value, column_is_nullable in (
75
- (models.TraceAnnotation.name, annotation.name, False),
76
- (
77
- models.TraceAnnotation.annotator_kind,
78
- annotation.annotator_kind.value
79
- if annotation.annotator_kind is not None
80
- else None,
81
- False,
82
- ),
83
- (models.TraceAnnotation.label, annotation.label, True),
84
- (models.TraceAnnotation.score, annotation.score, True),
85
- (models.TraceAnnotation.explanation, annotation.explanation, True),
86
- (models.TraceAnnotation.metadata_, annotation.metadata, False),
154
+ ):
155
+ if trace_annotation.user_id != user_id:
156
+ raise Unauthorized(
157
+ "At least one trace annotation is not associated with the current user."
87
158
  )
88
- if patch_value is not UNSET and (patch_value is not None or column_is_nullable)
89
- }
90
- trace_annotation = await session.scalar(
91
- update(models.TraceAnnotation)
92
- .where(models.TraceAnnotation.id == trace_annotation_id)
93
- .values(**patch)
94
- .returning(models.TraceAnnotation)
159
+ trace_annotations_by_id[trace_annotation.id] = trace_annotation
160
+
161
+ missing_trace_annotation_ids = set(patch_by_id.keys()) - set(
162
+ trace_annotations_by_id.keys()
163
+ )
164
+ if missing_trace_annotation_ids:
165
+ raise NotFound(
166
+ f"Could not find trace annotations with IDs: {missing_trace_annotation_ids}"
95
167
  )
96
- if trace_annotation:
97
- patched_annotations.append(to_gql_trace_annotation(trace_annotation))
98
- info.context.event_queue.put(TraceAnnotationInsertEvent((trace_annotation.id,)))
99
- return TraceAnnotationMutationPayload(trace_annotations=patched_annotations, query=Query())
168
+
169
+ for trace_annotation_id, patch in patch_by_id.items():
170
+ trace_annotation = trace_annotations_by_id[trace_annotation_id]
171
+ if patch.name:
172
+ trace_annotation.name = patch.name
173
+ if patch.annotator_kind:
174
+ trace_annotation.annotator_kind = patch.annotator_kind.value
175
+ if patch.label is not UNSET:
176
+ trace_annotation.label = patch.label
177
+ if patch.score is not UNSET:
178
+ trace_annotation.score = patch.score
179
+ if patch.explanation is not UNSET:
180
+ trace_annotation.explanation = patch.explanation
181
+ if patch.metadata is not UNSET:
182
+ assert isinstance(patch.metadata, dict)
183
+ trace_annotation.metadata_ = patch.metadata
184
+ if patch.identifier is not UNSET:
185
+ trace_annotation.identifier = patch.identifier or ""
186
+ session.add(trace_annotation)
187
+ await session.commit()
188
+
189
+ patched_annotations = [
190
+ to_gql_trace_annotation(trace_annotation)
191
+ for trace_annotation in trace_annotations_by_id.values()
192
+ ]
193
+ info.context.event_queue.put(TraceAnnotationInsertEvent(tuple(patch_by_id.keys())))
194
+ return TraceAnnotationMutationPayload(
195
+ trace_annotations=patched_annotations,
196
+ query=Query(),
197
+ )
100
198
 
101
199
  @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
102
200
  async def delete_trace_annotations(
103
201
  self, info: Info[Context, None], input: DeleteAnnotationsInput
104
202
  ) -> TraceAnnotationMutationPayload:
105
- trace_annotation_ids = [
106
- from_global_id_with_expected_type(global_id, "TraceAnnotation")
107
- for global_id in input.annotation_ids
108
- ]
203
+ if not input.annotation_ids:
204
+ raise BadRequest("No trace annotation IDs provided.")
205
+
206
+ trace_annotation_ids: dict[int, None] = {} # use dict to preserve order
207
+ for annotation_gid in input.annotation_ids:
208
+ try:
209
+ annotation_id = from_global_id_with_expected_type(annotation_gid, "TraceAnnotation")
210
+ except ValueError:
211
+ raise BadRequest(f"Invalid trace annotation ID: {annotation_gid}")
212
+ if annotation_id in trace_annotation_ids:
213
+ raise BadRequest(f"Duplicate trace annotation ID: {annotation_id}")
214
+ trace_annotation_ids[annotation_id] = None
215
+
216
+ assert isinstance(request := info.context.request, Request)
217
+ user_id: Optional[int] = None
218
+ user_is_admin = False
219
+ if "user" in request.scope and isinstance((user := info.context.user), PhoenixUser):
220
+ user_id = int(user.identity)
221
+ user_is_admin = user.is_admin
222
+
109
223
  async with info.context.db() as session:
110
- stmt = (
224
+ result = await session.scalars(
111
225
  delete(models.TraceAnnotation)
112
- .where(models.TraceAnnotation.id.in_(trace_annotation_ids))
226
+ .where(models.TraceAnnotation.id.in_(trace_annotation_ids.keys()))
113
227
  .returning(models.TraceAnnotation)
114
228
  )
115
- result = await session.scalars(stmt)
116
- deleted_annotations = result.all()
117
-
118
- deleted_annotations_gql = [
119
- to_gql_trace_annotation(annotation) for annotation in deleted_annotations
120
- ]
121
- if deleted_annotations:
122
- info.context.event_queue.put(
123
- TraceAnnotationDeleteEvent(tuple(anno.id for anno in deleted_annotations))
229
+ deleted_annotations_by_id = {annotation.id: annotation for annotation in result.all()}
230
+
231
+ if not user_is_admin and any(
232
+ annotation.user_id != user_id for annotation in deleted_annotations_by_id.values()
233
+ ):
234
+ await session.rollback()
235
+ raise Unauthorized(
236
+ "At least one trace annotation is not associated with the current user "
237
+ "and the current user is not an admin."
238
+ )
239
+
240
+ missing_trace_annotation_ids = set(trace_annotation_ids.keys()) - set(
241
+ deleted_annotations_by_id.keys()
124
242
  )
243
+ if missing_trace_annotation_ids:
244
+ raise NotFound(
245
+ f"Could not find trace annotations with IDs: {missing_trace_annotation_ids}"
246
+ )
247
+
248
+ deleted_gql_annotations = [
249
+ to_gql_trace_annotation(deleted_annotations_by_id[id]) for id in trace_annotation_ids
250
+ ]
251
+ info.context.event_queue.put(
252
+ TraceAnnotationDeleteEvent(tuple(deleted_annotations_by_id.keys()))
253
+ )
125
254
  return TraceAnnotationMutationPayload(
126
- trace_annotations=deleted_annotations_gql, query=Query()
255
+ trace_annotations=deleted_gql_annotations, query=Query()
127
256
  )
@@ -19,6 +19,7 @@ from phoenix.config import (
19
19
  getenv,
20
20
  )
21
21
  from phoenix.db import enums, models
22
+ from phoenix.db.constants import DEFAULT_PROJECT_TRACE_RETENTION_POLICY_ID
22
23
  from phoenix.db.helpers import SupportedSQLDialect, exclude_experiment_projects
23
24
  from phoenix.db.models import DatasetExample as OrmExample
24
25
  from phoenix.db.models import DatasetExampleRevision as OrmRevision
@@ -42,6 +43,7 @@ from phoenix.server.api.input_types.ClusterInput import ClusterInput
42
43
  from phoenix.server.api.input_types.Coordinates import InputCoordinate2D, InputCoordinate3D
43
44
  from phoenix.server.api.input_types.DatasetSort import DatasetSort
44
45
  from phoenix.server.api.input_types.InvocationParameters import InvocationParameter
46
+ from phoenix.server.api.types.AnnotationConfig import AnnotationConfig, to_gql_annotation_config
45
47
  from phoenix.server.api.types.Cluster import Cluster, to_gql_clusters
46
48
  from phoenix.server.api.types.Dataset import Dataset, to_gql_dataset
47
49
  from phoenix.server.api.types.DatasetExample import DatasetExample
@@ -65,14 +67,17 @@ from phoenix.server.api.types.node import from_global_id, from_global_id_with_ex
65
67
  from phoenix.server.api.types.pagination import ConnectionArgs, CursorString, connection_from_list
66
68
  from phoenix.server.api.types.Project import Project
67
69
  from phoenix.server.api.types.ProjectSession import ProjectSession, to_gql_project_session
70
+ from phoenix.server.api.types.ProjectTraceRetentionPolicy import ProjectTraceRetentionPolicy
68
71
  from phoenix.server.api.types.Prompt import Prompt, to_gql_prompt_from_orm
69
72
  from phoenix.server.api.types.PromptLabel import PromptLabel, to_gql_prompt_label
70
73
  from phoenix.server.api.types.PromptVersion import PromptVersion, to_gql_prompt_version
71
74
  from phoenix.server.api.types.PromptVersionTag import PromptVersionTag, to_gql_prompt_version_tag
72
75
  from phoenix.server.api.types.SortDir import SortDir
73
76
  from phoenix.server.api.types.Span import Span
77
+ from phoenix.server.api.types.SpanAnnotation import SpanAnnotation, to_gql_span_annotation
74
78
  from phoenix.server.api.types.SystemApiKey import SystemApiKey
75
79
  from phoenix.server.api.types.Trace import Trace
80
+ from phoenix.server.api.types.TraceAnnotation import TraceAnnotation, to_gql_trace_annotation
76
81
  from phoenix.server.api.types.User import User, to_gql_user
77
82
  from phoenix.server.api.types.UserApiKey import UserApiKey, to_gql_api_key
78
83
  from phoenix.server.api.types.UserRole import UserRole
@@ -588,6 +593,26 @@ class Query:
588
593
  if not (prompt_version_tag := await session.get(models.PromptVersionTag, node_id)):
589
594
  raise NotFound(f"Unknown prompt version tag: {id}")
590
595
  return to_gql_prompt_version_tag(prompt_version_tag)
596
+ elif type_name == ProjectTraceRetentionPolicy.__name__:
597
+ async with info.context.db() as session:
598
+ db_policy = await session.scalar(
599
+ select(models.ProjectTraceRetentionPolicy).filter_by(id=node_id)
600
+ )
601
+ if not db_policy:
602
+ raise NotFound(f"Unknown project trace retention policy: {id}")
603
+ return ProjectTraceRetentionPolicy(id=db_policy.id, db_policy=db_policy)
604
+ elif type_name == SpanAnnotation.__name__:
605
+ async with info.context.db() as session:
606
+ span_annotation = await session.get(models.SpanAnnotation, node_id)
607
+ if not span_annotation:
608
+ raise NotFound(f"Unknown span annotation: {id}")
609
+ return to_gql_span_annotation(span_annotation)
610
+ elif type_name == TraceAnnotation.__name__:
611
+ async with info.context.db() as session:
612
+ trace_annotation = await session.get(models.TraceAnnotation, node_id)
613
+ if not trace_annotation:
614
+ raise NotFound(f"Unknown trace annotation: {id}")
615
+ return to_gql_trace_annotation(trace_annotation)
591
616
  raise NotFound(f"Unknown node type: {type_name}")
592
617
 
593
618
  @strawberry.field
@@ -657,6 +682,28 @@ class Query:
657
682
  args=args,
658
683
  )
659
684
 
685
+ @strawberry.field
686
+ async def annotation_configs(
687
+ self,
688
+ info: Info[Context, None],
689
+ first: Optional[int] = 50,
690
+ last: Optional[int] = None,
691
+ after: Optional[str] = None,
692
+ before: Optional[str] = None,
693
+ ) -> Connection[AnnotationConfig]:
694
+ args = ConnectionArgs(
695
+ first=first,
696
+ after=after if isinstance(after, CursorString) else None,
697
+ last=last,
698
+ before=before if isinstance(before, CursorString) else None,
699
+ )
700
+ async with info.context.db() as session:
701
+ configs = await session.stream_scalars(
702
+ select(models.AnnotationConfig).order_by(models.AnnotationConfig.name)
703
+ )
704
+ data = [to_gql_annotation_config(config) async for config in configs]
705
+ return connection_from_list(data=data, args=args)
706
+
660
707
  @strawberry.field
661
708
  def clusters(
662
709
  self,
@@ -785,6 +832,45 @@ class Query:
785
832
  clustered_events=clustered_events,
786
833
  )
787
834
 
835
+ @strawberry.field
836
+ async def default_project_trace_retention_policy(
837
+ self,
838
+ info: Info[Context, None],
839
+ ) -> ProjectTraceRetentionPolicy:
840
+ stmt = select(models.ProjectTraceRetentionPolicy).filter_by(
841
+ id=DEFAULT_PROJECT_TRACE_RETENTION_POLICY_ID
842
+ )
843
+ async with info.context.db() as session:
844
+ db_policy = await session.scalar(stmt)
845
+ assert db_policy
846
+ return ProjectTraceRetentionPolicy(id=db_policy.id, db_policy=db_policy)
847
+
848
+ @strawberry.field
849
+ async def project_trace_retention_policies(
850
+ self,
851
+ info: Info[Context, None],
852
+ first: Optional[int] = 100,
853
+ last: Optional[int] = UNSET,
854
+ after: Optional[CursorString] = UNSET,
855
+ before: Optional[CursorString] = UNSET,
856
+ ) -> Connection[ProjectTraceRetentionPolicy]:
857
+ args = ConnectionArgs(
858
+ first=first,
859
+ after=after if isinstance(after, CursorString) else None,
860
+ last=last,
861
+ before=before if isinstance(before, CursorString) else None,
862
+ )
863
+ stmt = select(models.ProjectTraceRetentionPolicy).order_by(
864
+ models.ProjectTraceRetentionPolicy.id
865
+ )
866
+ async with info.context.db() as session:
867
+ result = await session.stream_scalars(stmt)
868
+ data = [
869
+ ProjectTraceRetentionPolicy(id=db_policy.id, db_policy=db_policy)
870
+ async for db_policy in result
871
+ ]
872
+ return connection_from_list(data=data, args=args)
873
+
788
874
  @strawberry.field(
789
875
  description="The allocated storage capacity of the database in bytes. "
790
876
  "Return None if this information is unavailable.",
@@ -4,6 +4,8 @@ from starlette.status import HTTP_403_FORBIDDEN
4
4
 
5
5
  from phoenix.server.bearer_auth import is_authenticated
6
6
 
7
+ from .annotation_configs import router as annotation_configs_router
8
+ from .annotations import router as annotations_router
7
9
  from .datasets import router as datasets_router
8
10
  from .evaluations import router as evaluations_router
9
11
  from .experiment_evaluations import router as experiment_evaluations_router
@@ -56,6 +58,8 @@ def create_v1_router(authentication_enabled: bool) -> APIRouter:
56
58
  ]
57
59
  ),
58
60
  )
61
+ router.include_router(annotation_configs_router)
62
+ router.include_router(annotations_router)
59
63
  router.include_router(datasets_router)
60
64
  router.include_router(experiments_router)
61
65
  router.include_router(experiment_runs_router)