arize-phoenix 10.0.4__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 (276) hide show
  1. {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/METADATA +124 -72
  2. arize_phoenix-12.28.1.dist-info/RECORD +499 -0
  3. {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/WHEEL +1 -1
  4. {arize_phoenix-10.0.4.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 +5 -4
  12. phoenix/auth.py +39 -2
  13. phoenix/config.py +1763 -91
  14. phoenix/datetime_utils.py +120 -2
  15. phoenix/db/README.md +595 -25
  16. phoenix/db/bulk_inserter.py +145 -103
  17. phoenix/db/engines.py +140 -33
  18. phoenix/db/enums.py +3 -12
  19. phoenix/db/facilitator.py +302 -35
  20. phoenix/db/helpers.py +1000 -65
  21. phoenix/db/iam_auth.py +64 -0
  22. phoenix/db/insertion/dataset.py +135 -2
  23. phoenix/db/insertion/document_annotation.py +9 -6
  24. phoenix/db/insertion/evaluation.py +2 -3
  25. phoenix/db/insertion/helpers.py +17 -2
  26. phoenix/db/insertion/session_annotation.py +176 -0
  27. phoenix/db/insertion/span.py +15 -11
  28. phoenix/db/insertion/span_annotation.py +3 -4
  29. phoenix/db/insertion/trace_annotation.py +3 -4
  30. phoenix/db/insertion/types.py +50 -20
  31. phoenix/db/migrations/versions/01a8342c9cdf_add_user_id_on_datasets.py +40 -0
  32. phoenix/db/migrations/versions/0df286449799_add_session_annotations_table.py +105 -0
  33. phoenix/db/migrations/versions/272b66ff50f8_drop_single_indices.py +119 -0
  34. phoenix/db/migrations/versions/58228d933c91_dataset_labels.py +67 -0
  35. phoenix/db/migrations/versions/699f655af132_experiment_tags.py +57 -0
  36. phoenix/db/migrations/versions/735d3d93c33e_add_composite_indices.py +41 -0
  37. phoenix/db/migrations/versions/a20694b15f82_cost.py +196 -0
  38. phoenix/db/migrations/versions/ab513d89518b_add_user_id_on_dataset_versions.py +40 -0
  39. phoenix/db/migrations/versions/d0690a79ea51_users_on_experiments.py +40 -0
  40. phoenix/db/migrations/versions/deb2c81c0bb2_dataset_splits.py +139 -0
  41. phoenix/db/migrations/versions/e76cbd66ffc3_add_experiments_dataset_examples.py +87 -0
  42. phoenix/db/models.py +669 -56
  43. phoenix/db/pg_config.py +10 -0
  44. phoenix/db/types/model_provider.py +4 -0
  45. phoenix/db/types/token_price_customization.py +29 -0
  46. phoenix/db/types/trace_retention.py +23 -15
  47. phoenix/experiments/evaluators/utils.py +3 -3
  48. phoenix/experiments/functions.py +160 -52
  49. phoenix/experiments/tracing.py +2 -2
  50. phoenix/experiments/types.py +1 -1
  51. phoenix/inferences/inferences.py +1 -2
  52. phoenix/server/api/auth.py +38 -7
  53. phoenix/server/api/auth_messages.py +46 -0
  54. phoenix/server/api/context.py +100 -4
  55. phoenix/server/api/dataloaders/__init__.py +79 -5
  56. phoenix/server/api/dataloaders/annotation_configs_by_project.py +31 -0
  57. phoenix/server/api/dataloaders/annotation_summaries.py +60 -8
  58. phoenix/server/api/dataloaders/average_experiment_repeated_run_group_latency.py +50 -0
  59. phoenix/server/api/dataloaders/average_experiment_run_latency.py +17 -24
  60. phoenix/server/api/dataloaders/cache/two_tier_cache.py +1 -2
  61. phoenix/server/api/dataloaders/dataset_dataset_splits.py +52 -0
  62. phoenix/server/api/dataloaders/dataset_example_revisions.py +0 -1
  63. phoenix/server/api/dataloaders/dataset_example_splits.py +40 -0
  64. phoenix/server/api/dataloaders/dataset_examples_and_versions_by_experiment_run.py +47 -0
  65. phoenix/server/api/dataloaders/dataset_labels.py +36 -0
  66. phoenix/server/api/dataloaders/document_evaluation_summaries.py +2 -2
  67. phoenix/server/api/dataloaders/document_evaluations.py +6 -9
  68. phoenix/server/api/dataloaders/experiment_annotation_summaries.py +88 -34
  69. phoenix/server/api/dataloaders/experiment_dataset_splits.py +43 -0
  70. phoenix/server/api/dataloaders/experiment_error_rates.py +21 -28
  71. phoenix/server/api/dataloaders/experiment_repeated_run_group_annotation_summaries.py +77 -0
  72. phoenix/server/api/dataloaders/experiment_repeated_run_groups.py +57 -0
  73. phoenix/server/api/dataloaders/experiment_runs_by_experiment_and_example.py +44 -0
  74. phoenix/server/api/dataloaders/last_used_times_by_generative_model_id.py +35 -0
  75. phoenix/server/api/dataloaders/latency_ms_quantile.py +40 -8
  76. phoenix/server/api/dataloaders/record_counts.py +37 -10
  77. phoenix/server/api/dataloaders/session_annotations_by_session.py +29 -0
  78. phoenix/server/api/dataloaders/span_cost_by_span.py +24 -0
  79. phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_generative_model.py +56 -0
  80. phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_project_session.py +57 -0
  81. phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_span.py +43 -0
  82. phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_trace.py +56 -0
  83. phoenix/server/api/dataloaders/span_cost_details_by_span_cost.py +27 -0
  84. phoenix/server/api/dataloaders/span_cost_summary_by_experiment.py +57 -0
  85. phoenix/server/api/dataloaders/span_cost_summary_by_experiment_repeated_run_group.py +64 -0
  86. phoenix/server/api/dataloaders/span_cost_summary_by_experiment_run.py +58 -0
  87. phoenix/server/api/dataloaders/span_cost_summary_by_generative_model.py +55 -0
  88. phoenix/server/api/dataloaders/span_cost_summary_by_project.py +152 -0
  89. phoenix/server/api/dataloaders/span_cost_summary_by_project_session.py +56 -0
  90. phoenix/server/api/dataloaders/span_cost_summary_by_trace.py +55 -0
  91. phoenix/server/api/dataloaders/span_costs.py +29 -0
  92. phoenix/server/api/dataloaders/table_fields.py +2 -2
  93. phoenix/server/api/dataloaders/token_prices_by_model.py +30 -0
  94. phoenix/server/api/dataloaders/trace_annotations_by_trace.py +27 -0
  95. phoenix/server/api/dataloaders/types.py +29 -0
  96. phoenix/server/api/exceptions.py +11 -1
  97. phoenix/server/api/helpers/dataset_helpers.py +5 -1
  98. phoenix/server/api/helpers/playground_clients.py +1243 -292
  99. phoenix/server/api/helpers/playground_registry.py +2 -2
  100. phoenix/server/api/helpers/playground_spans.py +8 -4
  101. phoenix/server/api/helpers/playground_users.py +26 -0
  102. phoenix/server/api/helpers/prompts/conversions/aws.py +83 -0
  103. phoenix/server/api/helpers/prompts/conversions/google.py +103 -0
  104. phoenix/server/api/helpers/prompts/models.py +205 -22
  105. phoenix/server/api/input_types/{SpanAnnotationFilter.py → AnnotationFilter.py} +22 -14
  106. phoenix/server/api/input_types/ChatCompletionInput.py +6 -2
  107. phoenix/server/api/input_types/CreateProjectInput.py +27 -0
  108. phoenix/server/api/input_types/CreateProjectSessionAnnotationInput.py +37 -0
  109. phoenix/server/api/input_types/DatasetFilter.py +17 -0
  110. phoenix/server/api/input_types/ExperimentRunSort.py +237 -0
  111. phoenix/server/api/input_types/GenerativeCredentialInput.py +9 -0
  112. phoenix/server/api/input_types/GenerativeModelInput.py +5 -0
  113. phoenix/server/api/input_types/ProjectSessionSort.py +161 -1
  114. phoenix/server/api/input_types/PromptFilter.py +14 -0
  115. phoenix/server/api/input_types/PromptVersionInput.py +52 -1
  116. phoenix/server/api/input_types/SpanSort.py +44 -7
  117. phoenix/server/api/input_types/TimeBinConfig.py +23 -0
  118. phoenix/server/api/input_types/UpdateAnnotationInput.py +34 -0
  119. phoenix/server/api/input_types/UserRoleInput.py +1 -0
  120. phoenix/server/api/mutations/__init__.py +10 -0
  121. phoenix/server/api/mutations/annotation_config_mutations.py +8 -8
  122. phoenix/server/api/mutations/api_key_mutations.py +19 -23
  123. phoenix/server/api/mutations/chat_mutations.py +154 -47
  124. phoenix/server/api/mutations/dataset_label_mutations.py +243 -0
  125. phoenix/server/api/mutations/dataset_mutations.py +21 -16
  126. phoenix/server/api/mutations/dataset_split_mutations.py +351 -0
  127. phoenix/server/api/mutations/experiment_mutations.py +2 -2
  128. phoenix/server/api/mutations/export_events_mutations.py +3 -3
  129. phoenix/server/api/mutations/model_mutations.py +210 -0
  130. phoenix/server/api/mutations/project_mutations.py +49 -10
  131. phoenix/server/api/mutations/project_session_annotations_mutations.py +158 -0
  132. phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +8 -4
  133. phoenix/server/api/mutations/prompt_label_mutations.py +74 -65
  134. phoenix/server/api/mutations/prompt_mutations.py +65 -129
  135. phoenix/server/api/mutations/prompt_version_tag_mutations.py +11 -8
  136. phoenix/server/api/mutations/span_annotations_mutations.py +15 -10
  137. phoenix/server/api/mutations/trace_annotations_mutations.py +14 -10
  138. phoenix/server/api/mutations/trace_mutations.py +47 -3
  139. phoenix/server/api/mutations/user_mutations.py +66 -41
  140. phoenix/server/api/queries.py +768 -293
  141. phoenix/server/api/routers/__init__.py +2 -2
  142. phoenix/server/api/routers/auth.py +154 -88
  143. phoenix/server/api/routers/ldap.py +229 -0
  144. phoenix/server/api/routers/oauth2.py +369 -106
  145. phoenix/server/api/routers/v1/__init__.py +24 -4
  146. phoenix/server/api/routers/v1/annotation_configs.py +23 -31
  147. phoenix/server/api/routers/v1/annotations.py +481 -17
  148. phoenix/server/api/routers/v1/datasets.py +395 -81
  149. phoenix/server/api/routers/v1/documents.py +142 -0
  150. phoenix/server/api/routers/v1/evaluations.py +24 -31
  151. phoenix/server/api/routers/v1/experiment_evaluations.py +19 -8
  152. phoenix/server/api/routers/v1/experiment_runs.py +337 -59
  153. phoenix/server/api/routers/v1/experiments.py +479 -48
  154. phoenix/server/api/routers/v1/models.py +7 -0
  155. phoenix/server/api/routers/v1/projects.py +18 -49
  156. phoenix/server/api/routers/v1/prompts.py +54 -40
  157. phoenix/server/api/routers/v1/sessions.py +108 -0
  158. phoenix/server/api/routers/v1/spans.py +1091 -81
  159. phoenix/server/api/routers/v1/traces.py +132 -78
  160. phoenix/server/api/routers/v1/users.py +389 -0
  161. phoenix/server/api/routers/v1/utils.py +3 -7
  162. phoenix/server/api/subscriptions.py +305 -88
  163. phoenix/server/api/types/Annotation.py +90 -23
  164. phoenix/server/api/types/ApiKey.py +13 -17
  165. phoenix/server/api/types/AuthMethod.py +1 -0
  166. phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +1 -0
  167. phoenix/server/api/types/CostBreakdown.py +12 -0
  168. phoenix/server/api/types/Dataset.py +226 -72
  169. phoenix/server/api/types/DatasetExample.py +88 -18
  170. phoenix/server/api/types/DatasetExperimentAnnotationSummary.py +10 -0
  171. phoenix/server/api/types/DatasetLabel.py +57 -0
  172. phoenix/server/api/types/DatasetSplit.py +98 -0
  173. phoenix/server/api/types/DatasetVersion.py +49 -4
  174. phoenix/server/api/types/DocumentAnnotation.py +212 -0
  175. phoenix/server/api/types/Experiment.py +264 -59
  176. phoenix/server/api/types/ExperimentComparison.py +5 -10
  177. phoenix/server/api/types/ExperimentRepeatedRunGroup.py +155 -0
  178. phoenix/server/api/types/ExperimentRepeatedRunGroupAnnotationSummary.py +9 -0
  179. phoenix/server/api/types/ExperimentRun.py +169 -65
  180. phoenix/server/api/types/ExperimentRunAnnotation.py +158 -39
  181. phoenix/server/api/types/GenerativeModel.py +245 -3
  182. phoenix/server/api/types/GenerativeProvider.py +70 -11
  183. phoenix/server/api/types/{Model.py → InferenceModel.py} +1 -1
  184. phoenix/server/api/types/ModelInterface.py +16 -0
  185. phoenix/server/api/types/PlaygroundModel.py +20 -0
  186. phoenix/server/api/types/Project.py +1278 -216
  187. phoenix/server/api/types/ProjectSession.py +188 -28
  188. phoenix/server/api/types/ProjectSessionAnnotation.py +187 -0
  189. phoenix/server/api/types/ProjectTraceRetentionPolicy.py +1 -1
  190. phoenix/server/api/types/Prompt.py +119 -39
  191. phoenix/server/api/types/PromptLabel.py +42 -25
  192. phoenix/server/api/types/PromptVersion.py +11 -8
  193. phoenix/server/api/types/PromptVersionTag.py +65 -25
  194. phoenix/server/api/types/ServerStatus.py +6 -0
  195. phoenix/server/api/types/Span.py +167 -123
  196. phoenix/server/api/types/SpanAnnotation.py +189 -42
  197. phoenix/server/api/types/SpanCostDetailSummaryEntry.py +10 -0
  198. phoenix/server/api/types/SpanCostSummary.py +10 -0
  199. phoenix/server/api/types/SystemApiKey.py +65 -1
  200. phoenix/server/api/types/TokenPrice.py +16 -0
  201. phoenix/server/api/types/TokenUsage.py +3 -3
  202. phoenix/server/api/types/Trace.py +223 -51
  203. phoenix/server/api/types/TraceAnnotation.py +149 -50
  204. phoenix/server/api/types/User.py +137 -32
  205. phoenix/server/api/types/UserApiKey.py +73 -26
  206. phoenix/server/api/types/node.py +10 -0
  207. phoenix/server/api/types/pagination.py +11 -2
  208. phoenix/server/app.py +290 -45
  209. phoenix/server/authorization.py +38 -3
  210. phoenix/server/bearer_auth.py +34 -24
  211. phoenix/server/cost_tracking/cost_details_calculator.py +196 -0
  212. phoenix/server/cost_tracking/cost_model_lookup.py +179 -0
  213. phoenix/server/cost_tracking/helpers.py +68 -0
  214. phoenix/server/cost_tracking/model_cost_manifest.json +3657 -830
  215. phoenix/server/cost_tracking/regex_specificity.py +397 -0
  216. phoenix/server/cost_tracking/token_cost_calculator.py +57 -0
  217. phoenix/server/daemons/__init__.py +0 -0
  218. phoenix/server/daemons/db_disk_usage_monitor.py +214 -0
  219. phoenix/server/daemons/generative_model_store.py +103 -0
  220. phoenix/server/daemons/span_cost_calculator.py +99 -0
  221. phoenix/server/dml_event.py +17 -0
  222. phoenix/server/dml_event_handler.py +5 -0
  223. phoenix/server/email/sender.py +56 -3
  224. phoenix/server/email/templates/db_disk_usage_notification.html +19 -0
  225. phoenix/server/email/types.py +11 -0
  226. phoenix/server/experiments/__init__.py +0 -0
  227. phoenix/server/experiments/utils.py +14 -0
  228. phoenix/server/grpc_server.py +11 -11
  229. phoenix/server/jwt_store.py +17 -15
  230. phoenix/server/ldap.py +1449 -0
  231. phoenix/server/main.py +26 -10
  232. phoenix/server/oauth2.py +330 -12
  233. phoenix/server/prometheus.py +66 -6
  234. phoenix/server/rate_limiters.py +4 -9
  235. phoenix/server/retention.py +33 -20
  236. phoenix/server/session_filters.py +49 -0
  237. phoenix/server/static/.vite/manifest.json +55 -51
  238. phoenix/server/static/assets/components-BreFUQQa.js +6702 -0
  239. phoenix/server/static/assets/{index-E0M82BdE.js → index-CTQoemZv.js} +140 -56
  240. phoenix/server/static/assets/pages-DBE5iYM3.js +9524 -0
  241. phoenix/server/static/assets/vendor-BGzfc4EU.css +1 -0
  242. phoenix/server/static/assets/vendor-DCE4v-Ot.js +920 -0
  243. phoenix/server/static/assets/vendor-codemirror-D5f205eT.js +25 -0
  244. phoenix/server/static/assets/vendor-recharts-V9cwpXsm.js +37 -0
  245. phoenix/server/static/assets/vendor-shiki-Do--csgv.js +5 -0
  246. phoenix/server/static/assets/vendor-three-CmB8bl_y.js +3840 -0
  247. phoenix/server/templates/index.html +40 -6
  248. phoenix/server/thread_server.py +1 -2
  249. phoenix/server/types.py +14 -4
  250. phoenix/server/utils.py +74 -0
  251. phoenix/session/client.py +56 -3
  252. phoenix/session/data_extractor.py +5 -0
  253. phoenix/session/evaluation.py +14 -5
  254. phoenix/session/session.py +45 -9
  255. phoenix/settings.py +5 -0
  256. phoenix/trace/attributes.py +80 -13
  257. phoenix/trace/dsl/helpers.py +90 -1
  258. phoenix/trace/dsl/query.py +8 -6
  259. phoenix/trace/projects.py +5 -0
  260. phoenix/utilities/template_formatters.py +1 -1
  261. phoenix/version.py +1 -1
  262. arize_phoenix-10.0.4.dist-info/RECORD +0 -405
  263. phoenix/server/api/types/Evaluation.py +0 -39
  264. phoenix/server/cost_tracking/cost_lookup.py +0 -255
  265. phoenix/server/static/assets/components-DULKeDfL.js +0 -4365
  266. phoenix/server/static/assets/pages-Cl0A-0U2.js +0 -7430
  267. phoenix/server/static/assets/vendor-WIZid84E.css +0 -1
  268. phoenix/server/static/assets/vendor-arizeai-Dy-0mSNw.js +0 -649
  269. phoenix/server/static/assets/vendor-codemirror-DBtifKNr.js +0 -33
  270. phoenix/server/static/assets/vendor-oB4u9zuV.js +0 -905
  271. phoenix/server/static/assets/vendor-recharts-D-T4KPz2.js +0 -59
  272. phoenix/server/static/assets/vendor-shiki-BMn4O_9F.js +0 -5
  273. phoenix/server/static/assets/vendor-three-C5WAXd5r.js +0 -2998
  274. phoenix/utilities/deprecation.py +0 -31
  275. {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/entry_points.txt +0 -0
  276. {arize_phoenix-10.0.4.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
  )
@@ -62,6 +217,17 @@ async def list_span_annotations(
62
217
  span_ids: list[str] = Query(
63
218
  ..., min_length=1, description="One or more span id to fetch annotations for"
64
219
  ),
220
+ include_annotation_names: Optional[list[str]] = Query(
221
+ default=None,
222
+ description=(
223
+ "Optional list of annotation names to include. If provided, only annotations with "
224
+ "these names will be returned. 'note' annotations are excluded by default unless "
225
+ "explicitly included in this list."
226
+ ),
227
+ ),
228
+ exclude_annotation_names: Optional[list[str]] = Query(
229
+ default=None, description="Optional list of annotation names to exclude from results."
230
+ ),
65
231
  cursor: Optional[str] = Query(default=None, description="A cursor for pagination"),
66
232
  limit: int = Query(
67
233
  default=10,
@@ -73,7 +239,7 @@ async def list_span_annotations(
73
239
  span_ids = list({*span_ids})
74
240
  if len(span_ids) > MAX_SPAN_IDS:
75
241
  raise HTTPException(
76
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
242
+ status_code=422,
77
243
  detail=f"Too many span_ids supplied: {len(span_ids)} (max {MAX_SPAN_IDS})",
78
244
  )
79
245
 
@@ -81,18 +247,29 @@ async def list_span_annotations(
81
247
  project = await _get_project_by_identifier(session, project_identifier)
82
248
  if not project:
83
249
  raise HTTPException(
84
- status_code=HTTP_404_NOT_FOUND,
250
+ status_code=404,
85
251
  detail=f"Project with identifier {project_identifier} not found",
86
252
  )
253
+
254
+ # Build the base query
255
+ where_conditions = [
256
+ models.Project.id == project.id,
257
+ models.Span.span_id.in_(span_ids),
258
+ ]
259
+
260
+ # Add annotation name filtering
261
+ if include_annotation_names:
262
+ where_conditions.append(models.SpanAnnotation.name.in_(include_annotation_names))
263
+
264
+ if exclude_annotation_names:
265
+ where_conditions.append(models.SpanAnnotation.name.not_in(exclude_annotation_names))
266
+
87
267
  stmt = (
88
268
  select(models.Span.span_id, models.SpanAnnotation)
89
269
  .join(models.Trace, models.Span.trace_rowid == models.Trace.id)
90
270
  .join(models.Project, models.Trace.project_rowid == models.Project.id)
91
271
  .join(models.SpanAnnotation, models.SpanAnnotation.span_rowid == models.Span.id)
92
- .where(
93
- models.Project.id == project.id,
94
- models.Span.span_id.in_(span_ids),
95
- )
272
+ .where(*where_conditions)
96
273
  .order_by(models.SpanAnnotation.id.desc())
97
274
  .limit(limit + 1)
98
275
  )
@@ -102,7 +279,7 @@ async def list_span_annotations(
102
279
  cursor_id = int(GlobalID.from_id(cursor).node_id)
103
280
  except ValueError:
104
281
  raise HTTPException(
105
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
282
+ status_code=422,
106
283
  detail="Invalid cursor value",
107
284
  )
108
285
  stmt = stmt.where(models.SpanAnnotation.id <= cursor_id)
@@ -132,7 +309,7 @@ async def list_span_annotations(
132
309
  if not spans_exist:
133
310
  raise HTTPException(
134
311
  detail="None of the supplied span_ids exist in this project",
135
- status_code=HTTP_404_NOT_FOUND,
312
+ status_code=404,
136
313
  )
137
314
 
138
315
  return SpanAnnotationsResponseBody(data=[], next_cursor=None)
@@ -142,7 +319,7 @@ async def list_span_annotations(
142
319
  id=str(GlobalID(SPAN_ANNOTATION_NODE_NAME, str(anno.id))),
143
320
  span_id=span_id,
144
321
  name=anno.name,
145
- result=SpanAnnotationResult(
322
+ result=AnnotationResult(
146
323
  label=anno.label,
147
324
  score=anno.score,
148
325
  explanation=anno.explanation,
@@ -159,3 +336,290 @@ async def list_span_annotations(
159
336
  ]
160
337
 
161
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)