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
@@ -1,32 +1,41 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from datetime import datetime
5
- from typing import Literal, Optional
4
+ from datetime import datetime, timezone
5
+ from typing import Any, Literal, Optional
6
6
 
7
7
  from fastapi import APIRouter, HTTPException, Path, Query
8
+ from pydantic import Field
8
9
  from sqlalchemy import exists, select
9
10
  from starlette.requests import Request
10
- from starlette.status import HTTP_200_OK, HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY
11
11
  from strawberry.relay import GlobalID
12
12
 
13
13
  from phoenix.db import models
14
+ from phoenix.db.insertion.types import Precursors
15
+ from phoenix.server.api.routers.v1.models import V1RoutesBaseModel
16
+ from phoenix.server.api.types.ProjectSessionAnnotation import (
17
+ ProjectSessionAnnotation as SessionAnnotationNodeType,
18
+ )
14
19
  from phoenix.server.api.types.SpanAnnotation import SpanAnnotation as SpanAnnotationNodeType
20
+ from phoenix.server.api.types.TraceAnnotation import TraceAnnotation as TraceAnnotationNodeType
15
21
  from phoenix.server.api.types.User import User as UserNodeType
16
22
 
17
- from .spans import SpanAnnotationData, SpanAnnotationResult
18
23
  from .utils import PaginatedResponseBody, _get_project_by_identifier, add_errors_to_responses
19
24
 
20
25
  logger = logging.getLogger(__name__)
21
26
 
22
27
  SPAN_ANNOTATION_NODE_NAME = SpanAnnotationNodeType.__name__
28
+ TRACE_ANNOTATION_NODE_NAME = TraceAnnotationNodeType.__name__
29
+ SESSION_ANNOTATION_NODE_NAME = SessionAnnotationNodeType.__name__
30
+ MAX_TRACE_IDS = 1_000
23
31
  USER_NODE_NAME = UserNodeType.__name__
24
32
  MAX_SPAN_IDS = 1_000
33
+ MAX_SESSION_IDS = 1_000
25
34
 
26
35
  router = APIRouter(tags=["annotations"])
27
36
 
28
37
 
29
- class SpanAnnotation(SpanAnnotationData):
38
+ class Annotation(V1RoutesBaseModel):
30
39
  id: str
31
40
  created_at: datetime
32
41
  updated_at: datetime
@@ -34,19 +43,165 @@ class SpanAnnotation(SpanAnnotationData):
34
43
  user_id: Optional[str]
35
44
 
36
45
 
46
+ class AnnotationResult(V1RoutesBaseModel):
47
+ label: Optional[str] = Field(default=None, description="The label assigned by the annotation")
48
+ score: Optional[float] = Field(default=None, description="The score assigned by the annotation")
49
+ explanation: Optional[str] = Field(
50
+ default=None, description="Explanation of the annotation result"
51
+ )
52
+
53
+
54
+ class AnnotationData(V1RoutesBaseModel):
55
+ name: str = Field(description="The name of the annotation")
56
+ annotator_kind: Literal["LLM", "CODE", "HUMAN"] = Field(
57
+ description="The kind of annotator used for the annotation"
58
+ )
59
+ result: Optional[AnnotationResult] = Field(
60
+ default=None, description="The result of the annotation"
61
+ )
62
+ metadata: Optional[dict[str, Any]] = Field(
63
+ default=None, description="Metadata for the annotation"
64
+ )
65
+ identifier: str = Field(
66
+ default="",
67
+ description=(
68
+ "The identifier of the annotation. "
69
+ "If provided, the annotation will be updated if it already exists."
70
+ ),
71
+ )
72
+
73
+
74
+ class SpanAnnotationData(AnnotationData):
75
+ span_id: str = Field(description="OpenTelemetry Span ID (hex format w/o 0x prefix)")
76
+
77
+ def as_precursor(self, *, user_id: Optional[int] = None) -> Precursors.SpanAnnotation:
78
+ return Precursors.SpanAnnotation(
79
+ datetime.now(timezone.utc),
80
+ self.span_id,
81
+ models.SpanAnnotation(
82
+ name=self.name,
83
+ annotator_kind=self.annotator_kind,
84
+ score=self.result.score if self.result else None,
85
+ label=self.result.label if self.result else None,
86
+ explanation=self.result.explanation if self.result else None,
87
+ metadata_=self.metadata or {},
88
+ identifier=self.identifier,
89
+ source="API",
90
+ user_id=user_id,
91
+ ),
92
+ )
93
+
94
+
95
+ class SpanAnnotation(SpanAnnotationData, Annotation):
96
+ pass
97
+
98
+
37
99
  class SpanAnnotationsResponseBody(PaginatedResponseBody[SpanAnnotation]):
38
100
  pass
39
101
 
40
102
 
103
+ class SpanDocumentAnnotationData(AnnotationData):
104
+ span_id: str = Field(description="OpenTelemetry Span ID (hex format w/o 0x prefix)")
105
+ document_position: int = Field(
106
+ description="A 0 based index of the document. E.x. the first document during retrieval is 0"
107
+ )
108
+
109
+ # Precursor here means a value to add to a queue for processing async
110
+ def as_precursor(self, *, user_id: Optional[int] = None) -> Precursors.DocumentAnnotation:
111
+ return Precursors.DocumentAnnotation(
112
+ datetime.now(timezone.utc),
113
+ self.span_id,
114
+ self.document_position,
115
+ models.DocumentAnnotation(
116
+ name=self.name,
117
+ annotator_kind=self.annotator_kind,
118
+ document_position=self.document_position,
119
+ score=self.result.score if self.result else None,
120
+ label=self.result.label if self.result else None,
121
+ explanation=self.result.explanation if self.result else None,
122
+ metadata_=self.metadata or {},
123
+ identifier=self.identifier,
124
+ source="API",
125
+ user_id=user_id,
126
+ ),
127
+ )
128
+
129
+
130
+ class SpanDocumentAnnotation(SpanDocumentAnnotationData, Annotation):
131
+ pass
132
+
133
+
134
+ class SpanDocumentAnnotationsResponseBody(PaginatedResponseBody[SpanDocumentAnnotation]):
135
+ pass
136
+
137
+
138
+ class TraceAnnotationData(AnnotationData):
139
+ trace_id: str = Field(description="OpenTelemetry Trace ID (hex format w/o 0x prefix)")
140
+
141
+ def as_precursor(self, *, user_id: Optional[int] = None) -> Precursors.TraceAnnotation:
142
+ return Precursors.TraceAnnotation(
143
+ datetime.now(timezone.utc),
144
+ self.trace_id,
145
+ models.TraceAnnotation(
146
+ name=self.name,
147
+ annotator_kind=self.annotator_kind,
148
+ score=self.result.score if self.result else None,
149
+ label=self.result.label if self.result else None,
150
+ explanation=self.result.explanation if self.result else None,
151
+ metadata_=self.metadata or {},
152
+ identifier=self.identifier,
153
+ source="API",
154
+ user_id=user_id,
155
+ ),
156
+ )
157
+
158
+
159
+ class TraceAnnotation(TraceAnnotationData, Annotation):
160
+ pass
161
+
162
+
163
+ class TraceAnnotationsResponseBody(PaginatedResponseBody[TraceAnnotation]):
164
+ pass
165
+
166
+
167
+ class SessionAnnotationData(AnnotationData):
168
+ session_id: str = Field(description="Session ID")
169
+
170
+ def as_precursor(self, *, user_id: Optional[int] = None) -> Precursors.SessionAnnotation:
171
+ return Precursors.SessionAnnotation(
172
+ datetime.now(timezone.utc),
173
+ self.session_id,
174
+ models.ProjectSessionAnnotation(
175
+ name=self.name,
176
+ annotator_kind=self.annotator_kind,
177
+ score=self.result.score if self.result else None,
178
+ label=self.result.label if self.result else None,
179
+ explanation=self.result.explanation if self.result else None,
180
+ metadata_=self.metadata or {},
181
+ identifier=self.identifier,
182
+ source="API",
183
+ user_id=user_id,
184
+ ),
185
+ )
186
+
187
+
188
+ class SessionAnnotation(SessionAnnotationData, Annotation):
189
+ pass
190
+
191
+
192
+ class SessionAnnotationsResponseBody(PaginatedResponseBody[SessionAnnotation]):
193
+ pass
194
+
195
+
41
196
  @router.get(
42
197
  "/projects/{project_identifier}/span_annotations",
43
198
  operation_id="listSpanAnnotationsBySpanIds",
44
199
  summary="Get span annotations for a list of span_ids.",
45
- status_code=HTTP_200_OK,
200
+ status_code=200,
46
201
  responses=add_errors_to_responses(
47
202
  [
48
- {"status_code": HTTP_404_NOT_FOUND, "description": "Project or spans not found"},
49
- {"status_code": HTTP_422_UNPROCESSABLE_ENTITY, "description": "Invalid parameters"},
203
+ {"status_code": 404, "description": "Project or spans not found"},
204
+ {"status_code": 422, "description": "Invalid parameters"},
50
205
  ]
51
206
  ),
52
207
  )
@@ -84,7 +239,7 @@ async def list_span_annotations(
84
239
  span_ids = list({*span_ids})
85
240
  if len(span_ids) > MAX_SPAN_IDS:
86
241
  raise HTTPException(
87
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
242
+ status_code=422,
88
243
  detail=f"Too many span_ids supplied: {len(span_ids)} (max {MAX_SPAN_IDS})",
89
244
  )
90
245
 
@@ -92,7 +247,7 @@ async def list_span_annotations(
92
247
  project = await _get_project_by_identifier(session, project_identifier)
93
248
  if not project:
94
249
  raise HTTPException(
95
- status_code=HTTP_404_NOT_FOUND,
250
+ status_code=404,
96
251
  detail=f"Project with identifier {project_identifier} not found",
97
252
  )
98
253
 
@@ -124,7 +279,7 @@ async def list_span_annotations(
124
279
  cursor_id = int(GlobalID.from_id(cursor).node_id)
125
280
  except ValueError:
126
281
  raise HTTPException(
127
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
282
+ status_code=422,
128
283
  detail="Invalid cursor value",
129
284
  )
130
285
  stmt = stmt.where(models.SpanAnnotation.id <= cursor_id)
@@ -154,7 +309,7 @@ async def list_span_annotations(
154
309
  if not spans_exist:
155
310
  raise HTTPException(
156
311
  detail="None of the supplied span_ids exist in this project",
157
- status_code=HTTP_404_NOT_FOUND,
312
+ status_code=404,
158
313
  )
159
314
 
160
315
  return SpanAnnotationsResponseBody(data=[], next_cursor=None)
@@ -164,7 +319,7 @@ async def list_span_annotations(
164
319
  id=str(GlobalID(SPAN_ANNOTATION_NODE_NAME, str(anno.id))),
165
320
  span_id=span_id,
166
321
  name=anno.name,
167
- result=SpanAnnotationResult(
322
+ result=AnnotationResult(
168
323
  label=anno.label,
169
324
  score=anno.score,
170
325
  explanation=anno.explanation,
@@ -181,3 +336,290 @@ async def list_span_annotations(
181
336
  ]
182
337
 
183
338
  return SpanAnnotationsResponseBody(data=data, next_cursor=next_cursor)
339
+
340
+
341
+ @router.get(
342
+ "/projects/{project_identifier}/trace_annotations",
343
+ operation_id="listTraceAnnotationsByTraceIds",
344
+ summary="Get trace annotations for a list of trace_ids.",
345
+ status_code=200,
346
+ responses=add_errors_to_responses(
347
+ [
348
+ {"status_code": 404, "description": "Project or traces not found"},
349
+ {"status_code": 422, "description": "Invalid parameters"},
350
+ ]
351
+ ),
352
+ )
353
+ async def list_trace_annotations(
354
+ request: Request,
355
+ project_identifier: str = Path(
356
+ description=(
357
+ "The project identifier: either project ID or project name. If using a project name as "
358
+ "the identifier, it cannot contain slash (/), question mark (?), or pound sign (#) "
359
+ "characters."
360
+ )
361
+ ),
362
+ trace_ids: list[str] = Query(
363
+ ..., min_length=1, description="One or more trace id to fetch annotations for"
364
+ ),
365
+ include_annotation_names: Optional[list[str]] = Query(
366
+ default=None,
367
+ description=(
368
+ "Optional list of annotation names to include. If provided, only annotations with "
369
+ "these names will be returned. 'note' annotations are excluded by default unless "
370
+ "explicitly included in this list."
371
+ ),
372
+ ),
373
+ exclude_annotation_names: Optional[list[str]] = Query(
374
+ default=None, description="Optional list of annotation names to exclude from results."
375
+ ),
376
+ cursor: Optional[str] = Query(default=None, description="A cursor for pagination"),
377
+ limit: int = Query(
378
+ default=10,
379
+ gt=0,
380
+ le=10000,
381
+ description="The maximum number of annotations to return in a single request",
382
+ ),
383
+ ) -> TraceAnnotationsResponseBody:
384
+ trace_ids = list({*trace_ids})
385
+ if len(trace_ids) > MAX_TRACE_IDS:
386
+ raise HTTPException(
387
+ status_code=422,
388
+ detail=f"Too many trace_ids supplied: {len(trace_ids)} (max {MAX_TRACE_IDS})",
389
+ )
390
+
391
+ async with request.app.state.db() as session:
392
+ project = await _get_project_by_identifier(session, project_identifier)
393
+ if not project:
394
+ raise HTTPException(
395
+ status_code=404,
396
+ detail=f"Project with identifier {project_identifier} not found",
397
+ )
398
+
399
+ # Build the base query
400
+ where_conditions = [
401
+ models.Project.id == project.id,
402
+ models.Trace.trace_id.in_(trace_ids),
403
+ ]
404
+
405
+ # Add annotation name filtering
406
+ if include_annotation_names:
407
+ where_conditions.append(models.TraceAnnotation.name.in_(include_annotation_names))
408
+
409
+ if exclude_annotation_names:
410
+ where_conditions.append(models.TraceAnnotation.name.not_in(exclude_annotation_names))
411
+
412
+ stmt = (
413
+ select(models.Trace.trace_id, models.TraceAnnotation)
414
+ .join(models.Project, models.Trace.project_rowid == models.Project.id)
415
+ .join(models.TraceAnnotation, models.TraceAnnotation.trace_rowid == models.Trace.id)
416
+ .where(*where_conditions)
417
+ .order_by(models.TraceAnnotation.id.desc())
418
+ .limit(limit + 1)
419
+ )
420
+
421
+ if cursor:
422
+ try:
423
+ cursor_id = int(GlobalID.from_id(cursor).node_id)
424
+ except ValueError:
425
+ raise HTTPException(
426
+ status_code=422,
427
+ detail="Invalid cursor value",
428
+ )
429
+ stmt = stmt.where(models.TraceAnnotation.id <= cursor_id)
430
+
431
+ rows: list[tuple[str, models.TraceAnnotation]] = [
432
+ r async for r in (await session.stream(stmt))
433
+ ]
434
+
435
+ next_cursor: Optional[str] = None
436
+ if len(rows) == limit + 1:
437
+ *rows, extra = rows
438
+ next_cursor = str(GlobalID(TRACE_ANNOTATION_NODE_NAME, str(extra[1].id)))
439
+
440
+ if not rows:
441
+ traces_exist = await session.scalar(
442
+ select(
443
+ exists().where(
444
+ models.Trace.trace_id.in_(trace_ids),
445
+ models.Trace.project_rowid == project.id,
446
+ )
447
+ )
448
+ )
449
+ if not traces_exist:
450
+ raise HTTPException(
451
+ detail="None of the supplied trace_ids exist in this project",
452
+ status_code=404,
453
+ )
454
+
455
+ return TraceAnnotationsResponseBody(data=[], next_cursor=None)
456
+
457
+ data = [
458
+ TraceAnnotation(
459
+ id=str(GlobalID(TRACE_ANNOTATION_NODE_NAME, str(anno.id))),
460
+ trace_id=trace_id,
461
+ name=anno.name,
462
+ result=AnnotationResult(
463
+ label=anno.label,
464
+ score=anno.score,
465
+ explanation=anno.explanation,
466
+ ),
467
+ metadata=anno.metadata_,
468
+ annotator_kind=anno.annotator_kind,
469
+ created_at=anno.created_at,
470
+ updated_at=anno.updated_at,
471
+ identifier=anno.identifier,
472
+ source=anno.source,
473
+ user_id=str(GlobalID("User", str(anno.user_id))) if anno.user_id else None,
474
+ )
475
+ for trace_id, anno in rows
476
+ ]
477
+
478
+ return TraceAnnotationsResponseBody(data=data, next_cursor=next_cursor)
479
+
480
+
481
+ @router.get(
482
+ "/projects/{project_identifier}/session_annotations",
483
+ operation_id="listSessionAnnotationsBySessionIds",
484
+ summary="Get session annotations for a list of session_ids.",
485
+ status_code=200,
486
+ responses=add_errors_to_responses(
487
+ [
488
+ {"status_code": 404, "description": "Project or sessions not found"},
489
+ {"status_code": 422, "description": "Invalid parameters"},
490
+ ]
491
+ ),
492
+ )
493
+ async def list_session_annotations(
494
+ request: Request,
495
+ project_identifier: str = Path(
496
+ description=(
497
+ "The project identifier: either project ID or project name. If using a project name as "
498
+ "the identifier, it cannot contain slash (/), question mark (?), or pound sign (#) "
499
+ "characters."
500
+ )
501
+ ),
502
+ session_ids: list[str] = Query(
503
+ ..., min_length=1, description="One or more session id to fetch annotations for"
504
+ ),
505
+ include_annotation_names: Optional[list[str]] = Query(
506
+ default=None,
507
+ description=(
508
+ "Optional list of annotation names to include. If provided, only annotations with "
509
+ "these names will be returned. 'note' annotations are excluded by default unless "
510
+ "explicitly included in this list."
511
+ ),
512
+ ),
513
+ exclude_annotation_names: Optional[list[str]] = Query(
514
+ default=None, description="Optional list of annotation names to exclude from results."
515
+ ),
516
+ cursor: Optional[str] = Query(default=None, description="A cursor for pagination"),
517
+ limit: int = Query(
518
+ default=10,
519
+ gt=0,
520
+ le=10000,
521
+ description="The maximum number of annotations to return in a single request",
522
+ ),
523
+ ) -> SessionAnnotationsResponseBody:
524
+ session_ids = list({*session_ids})
525
+ if len(session_ids) > MAX_SESSION_IDS:
526
+ raise HTTPException(
527
+ status_code=422,
528
+ detail=f"Too many session_ids supplied: {len(session_ids)} (max {MAX_SESSION_IDS})",
529
+ )
530
+
531
+ async with request.app.state.db() as session:
532
+ project = await _get_project_by_identifier(session, project_identifier)
533
+ if not project:
534
+ raise HTTPException(
535
+ status_code=404,
536
+ detail=f"Project with identifier {project_identifier} not found",
537
+ )
538
+
539
+ # Build the base query
540
+ where_conditions = [
541
+ models.Project.id == project.id,
542
+ models.ProjectSession.session_id.in_(session_ids),
543
+ ]
544
+
545
+ # Add annotation name filtering
546
+ if include_annotation_names:
547
+ where_conditions.append(
548
+ models.ProjectSessionAnnotation.name.in_(include_annotation_names)
549
+ )
550
+
551
+ if exclude_annotation_names:
552
+ where_conditions.append(
553
+ models.ProjectSessionAnnotation.name.not_in(exclude_annotation_names)
554
+ )
555
+
556
+ stmt = (
557
+ select(models.ProjectSession.session_id, models.ProjectSessionAnnotation)
558
+ .join(models.Project, models.ProjectSession.project_id == models.Project.id)
559
+ .join(
560
+ models.ProjectSessionAnnotation,
561
+ models.ProjectSessionAnnotation.project_session_id == models.ProjectSession.id,
562
+ )
563
+ .where(*where_conditions)
564
+ .order_by(models.ProjectSessionAnnotation.id.desc())
565
+ .limit(limit + 1)
566
+ )
567
+
568
+ if cursor:
569
+ try:
570
+ cursor_id = int(GlobalID.from_id(cursor).node_id)
571
+ except ValueError:
572
+ raise HTTPException(
573
+ status_code=422,
574
+ detail="Invalid cursor value",
575
+ )
576
+ stmt = stmt.where(models.ProjectSessionAnnotation.id <= cursor_id)
577
+
578
+ rows: list[tuple[str, models.ProjectSessionAnnotation]] = [
579
+ r async for r in (await session.stream(stmt))
580
+ ]
581
+
582
+ next_cursor: Optional[str] = None
583
+ if len(rows) == limit + 1:
584
+ *rows, extra = rows
585
+ next_cursor = str(GlobalID(SESSION_ANNOTATION_NODE_NAME, str(extra[1].id)))
586
+
587
+ if not rows:
588
+ sessions_exist = await session.scalar(
589
+ select(
590
+ exists().where(
591
+ models.ProjectSession.session_id.in_(session_ids),
592
+ models.ProjectSession.project_id == project.id,
593
+ )
594
+ )
595
+ )
596
+ if not sessions_exist:
597
+ raise HTTPException(
598
+ detail="None of the supplied session_ids exist in this project",
599
+ status_code=404,
600
+ )
601
+
602
+ return SessionAnnotationsResponseBody(data=[], next_cursor=None)
603
+
604
+ data = [
605
+ SessionAnnotation(
606
+ id=str(GlobalID(SESSION_ANNOTATION_NODE_NAME, str(anno.id))),
607
+ session_id=session_id,
608
+ name=anno.name,
609
+ result=AnnotationResult(
610
+ label=anno.label,
611
+ score=anno.score,
612
+ explanation=anno.explanation,
613
+ ),
614
+ metadata=anno.metadata_,
615
+ annotator_kind=anno.annotator_kind,
616
+ created_at=anno.created_at,
617
+ updated_at=anno.updated_at,
618
+ identifier=anno.identifier,
619
+ source=anno.source,
620
+ user_id=str(GlobalID(USER_NODE_NAME, str(anno.user_id))) if anno.user_id else None,
621
+ )
622
+ for session_id, anno in rows
623
+ ]
624
+
625
+ return SessionAnnotationsResponseBody(data=data, next_cursor=next_cursor)