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
@@ -0,0 +1,142 @@
1
+ from typing import Optional
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException, Query
4
+ from pydantic import Field
5
+ from sqlalchemy import select
6
+ from starlette.requests import Request
7
+ from strawberry.relay import GlobalID
8
+
9
+ from phoenix.db import models
10
+ from phoenix.db.helpers import SupportedSQLDialect
11
+ from phoenix.db.insertion.helpers import as_kv, insert_on_conflict
12
+ from phoenix.server.api.routers.v1.annotations import SpanDocumentAnnotationData
13
+ from phoenix.server.api.types.DocumentAnnotation import DocumentAnnotation
14
+ from phoenix.server.authorization import is_not_locked
15
+ from phoenix.server.bearer_auth import PhoenixUser
16
+ from phoenix.server.dml_event import DocumentAnnotationInsertEvent
17
+
18
+ from .models import V1RoutesBaseModel
19
+ from .utils import RequestBody, ResponseBody, add_errors_to_responses
20
+
21
+ # Since the document annotations are spans related, we place it under spans
22
+ router = APIRouter(tags=["spans"])
23
+
24
+
25
+ class AnnotateSpanDocumentsRequestBody(RequestBody[list[SpanDocumentAnnotationData]]):
26
+ pass
27
+
28
+
29
+ class InsertedSpanDocumentAnnotation(V1RoutesBaseModel):
30
+ id: str = Field(description="The ID of the inserted span document annotation")
31
+
32
+
33
+ class AnnotateSpanDocumentsResponseBody(ResponseBody[list[InsertedSpanDocumentAnnotation]]):
34
+ pass
35
+
36
+
37
+ @router.post(
38
+ "/document_annotations",
39
+ dependencies=[Depends(is_not_locked)],
40
+ operation_id="annotateSpanDocuments",
41
+ responses=add_errors_to_responses(
42
+ [
43
+ {
44
+ "status_code": 404,
45
+ "description": "Span not found",
46
+ },
47
+ {
48
+ "status_code": 422,
49
+ "description": "Invalid request - non-empty identifier not supported",
50
+ },
51
+ ]
52
+ ),
53
+ response_description="Span document annotation inserted successfully",
54
+ include_in_schema=True,
55
+ )
56
+ async def annotate_span_documents(
57
+ request: Request,
58
+ request_body: AnnotateSpanDocumentsRequestBody,
59
+ sync: bool = Query(
60
+ default=False, description="If set to true, the annotations are inserted synchronously."
61
+ ),
62
+ ) -> AnnotateSpanDocumentsResponseBody:
63
+ if not request_body.data:
64
+ return AnnotateSpanDocumentsResponseBody(data=[])
65
+
66
+ # Validate that identifiers are empty or only whitespace
67
+ for annotation in request_body.data:
68
+ if annotation.identifier.strip():
69
+ raise HTTPException(
70
+ detail=f"Non-empty identifier '{annotation.identifier}' is not supported",
71
+ status_code=422, # Unprocessable Entity
72
+ )
73
+
74
+ user_id: Optional[int] = None
75
+ if request.app.state.authentication_enabled and isinstance(request.user, PhoenixUser):
76
+ user_id = int(request.user.identity)
77
+
78
+ span_document_annotations = request_body.data
79
+
80
+ precursors = [
81
+ annotation.as_precursor(user_id=user_id) for annotation in span_document_annotations
82
+ ]
83
+ if not sync:
84
+ await request.state.enqueue_annotations(*precursors)
85
+ return AnnotateSpanDocumentsResponseBody(data=[])
86
+
87
+ span_ids = {p.span_id for p in precursors}
88
+ # Account for the fact that the spans could arrive after the annotation
89
+ async with request.app.state.db() as session:
90
+ existing_spans = {
91
+ span_id: (id_, num_docs)
92
+ async for span_id, id_, num_docs in await session.stream(
93
+ select(models.Span.span_id, models.Span.id, models.Span.num_documents).filter(
94
+ models.Span.span_id.in_(span_ids)
95
+ )
96
+ )
97
+ }
98
+
99
+ missing_span_ids = span_ids - set(existing_spans.keys())
100
+ # We prefer to fail the entire operation if there are missing spans in sync mode
101
+ if missing_span_ids:
102
+ raise HTTPException(
103
+ detail=f"Spans with IDs {', '.join(missing_span_ids)} do not exist.",
104
+ status_code=404,
105
+ )
106
+
107
+ # Validate that document positions are within bounds
108
+ for annotation in span_document_annotations:
109
+ _, num_docs = existing_spans[annotation.span_id]
110
+ if annotation.document_position not in range(num_docs):
111
+ raise HTTPException(
112
+ detail=f"Document position {annotation.document_position} is out of bounds for "
113
+ f"span {annotation.span_id} (max: {num_docs - 1})",
114
+ status_code=422, # Unprocessable Entity
115
+ )
116
+
117
+ inserted_document_annotation_ids = []
118
+ dialect = SupportedSQLDialect(session.bind.dialect.name)
119
+ for anno in precursors:
120
+ span_rowid, _ = existing_spans[anno.span_id]
121
+ values = dict(as_kv(anno.as_insertable(span_rowid).row))
122
+ span_document_annotation_id = await session.scalar(
123
+ insert_on_conflict(
124
+ values,
125
+ dialect=dialect,
126
+ table=models.DocumentAnnotation,
127
+ unique_by=("name", "span_rowid", "identifier", "document_position"),
128
+ constraint_name="uq_document_annotations_name_span_rowid_document_pos_identifier",
129
+ ).returning(models.DocumentAnnotation.id)
130
+ )
131
+ inserted_document_annotation_ids.append(span_document_annotation_id)
132
+
133
+ # We queue an event to let the application know that annotations have changed
134
+ request.state.event_queue.put(
135
+ DocumentAnnotationInsertEvent(tuple(inserted_document_annotation_ids))
136
+ )
137
+ return AnnotateSpanDocumentsResponseBody(
138
+ data=[
139
+ InsertedSpanDocumentAnnotation(id=str(GlobalID(DocumentAnnotation.__name__, str(id_))))
140
+ for id_ in inserted_document_annotation_ids
141
+ ]
142
+ )
@@ -1,11 +1,12 @@
1
1
  import gzip
2
2
  from collections.abc import Callable
3
+ from datetime import datetime, timezone
3
4
  from itertools import chain
4
5
  from typing import Any, Iterator, Optional, Union, cast
5
6
 
6
7
  import pandas as pd
7
8
  import pyarrow as pa
8
- from fastapi import APIRouter, Header, HTTPException, Query
9
+ from fastapi import APIRouter, Depends, Header, HTTPException, Query
9
10
  from google.protobuf.message import DecodeError
10
11
  from pandas import DataFrame
11
12
  from sqlalchemy import select
@@ -14,12 +15,6 @@ from starlette.background import BackgroundTask
14
15
  from starlette.datastructures import State
15
16
  from starlette.requests import Request
16
17
  from starlette.responses import Response, StreamingResponse
17
- from starlette.status import (
18
- HTTP_204_NO_CONTENT,
19
- HTTP_404_NOT_FOUND,
20
- HTTP_415_UNSUPPORTED_MEDIA_TYPE,
21
- HTTP_422_UNPROCESSABLE_ENTITY,
22
- )
23
18
  from typing_extensions import TypeAlias
24
19
 
25
20
  import phoenix.trace.v1 as pb
@@ -28,6 +23,7 @@ from phoenix.db import models
28
23
  from phoenix.db.insertion.types import Precursors
29
24
  from phoenix.exceptions import PhoenixEvaluationNameIsMissing
30
25
  from phoenix.server.api.routers.utils import table_to_bytes
26
+ from phoenix.server.authorization import is_not_locked
31
27
  from phoenix.server.types import DbSessionFactory
32
28
  from phoenix.trace.span_evaluations import (
33
29
  DocumentEvaluations,
@@ -45,19 +41,19 @@ router = APIRouter(tags=["traces"], include_in_schema=True)
45
41
 
46
42
  @router.post(
47
43
  "/evaluations",
44
+ dependencies=[Depends(is_not_locked)],
48
45
  operation_id="addEvaluations",
49
46
  summary="Add span, trace, or document evaluations",
50
- status_code=HTTP_204_NO_CONTENT,
47
+ status_code=204,
51
48
  responses=add_errors_to_responses(
52
49
  [
53
50
  {
54
- "status_code": HTTP_415_UNSUPPORTED_MEDIA_TYPE,
51
+ "status_code": 415,
55
52
  "description": (
56
- "Unsupported content type, "
57
- "only gzipped protobuf and pandas-arrow are supported"
53
+ "Unsupported content type, only gzipped protobuf and pandas-arrow are supported"
58
54
  ),
59
55
  },
60
- HTTP_422_UNPROCESSABLE_ENTITY,
56
+ 422,
61
57
  ]
62
58
  ),
63
59
  openapi_extra={
@@ -78,29 +74,23 @@ async def post_evaluations(
78
74
  if content_type == "application/x-pandas-arrow":
79
75
  return await _process_pyarrow(request)
80
76
  if content_type != "application/x-protobuf":
81
- raise HTTPException(
82
- detail="Unsupported content type", status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE
83
- )
77
+ raise HTTPException(detail="Unsupported content type", status_code=415)
84
78
  body = await request.body()
85
79
  if content_encoding == "gzip":
86
80
  body = gzip.decompress(body)
87
81
  elif content_encoding:
88
- raise HTTPException(
89
- detail="Unsupported content encoding", status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE
90
- )
82
+ raise HTTPException(detail="Unsupported content encoding", status_code=415)
91
83
  evaluation = pb.Evaluation()
92
84
  try:
93
85
  evaluation.ParseFromString(body)
94
86
  except DecodeError:
95
- raise HTTPException(
96
- detail="Request body is invalid", status_code=HTTP_422_UNPROCESSABLE_ENTITY
97
- )
87
+ raise HTTPException(detail="Request body is invalid", status_code=422)
98
88
  if not evaluation.name.strip():
99
89
  raise HTTPException(
100
90
  detail="Evaluation name must not be blank/empty",
101
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
91
+ status_code=422,
102
92
  )
103
- await request.state.queue_evaluation_for_bulk_insert(evaluation)
93
+ await request.state.enqueue_evaluation(evaluation)
104
94
  return Response()
105
95
 
106
96
 
@@ -108,7 +98,7 @@ async def post_evaluations(
108
98
  "/evaluations",
109
99
  operation_id="getEvaluations",
110
100
  summary="Get span, trace, or document evaluations from a project",
111
- responses=add_errors_to_responses([HTTP_404_NOT_FOUND]),
101
+ responses=add_errors_to_responses([404]),
112
102
  )
113
103
  async def get_evaluations(
114
104
  request: Request,
@@ -147,7 +137,7 @@ async def get_evaluations(
147
137
  and span_evals_dataframe.empty
148
138
  and document_evals_dataframe.empty
149
139
  ):
150
- return Response(status_code=HTTP_404_NOT_FOUND)
140
+ return Response(status_code=404)
151
141
 
152
142
  evals = chain(
153
143
  map(
@@ -177,7 +167,7 @@ async def _process_pyarrow(request: Request) -> Response:
177
167
  except pa.ArrowInvalid:
178
168
  raise HTTPException(
179
169
  detail="Request body is not valid pyarrow",
180
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
170
+ status_code=422,
181
171
  )
182
172
  try:
183
173
  evaluations = Evaluations.from_pyarrow_reader(reader)
@@ -185,11 +175,11 @@ async def _process_pyarrow(request: Request) -> Response:
185
175
  if isinstance(e, PhoenixEvaluationNameIsMissing):
186
176
  raise HTTPException(
187
177
  detail="Evaluation name must not be blank/empty",
188
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
178
+ status_code=422,
189
179
  )
190
180
  raise HTTPException(
191
181
  detail="Invalid data in request body",
192
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
182
+ status_code=422,
193
183
  )
194
184
  return Response(background=BackgroundTask(_add_evaluations, request.state, evaluations))
195
185
 
@@ -219,7 +209,7 @@ async def _add_evaluations(state: State, evaluations: Evaluations) -> None:
219
209
  explanation=explanation,
220
210
  metadata_={},
221
211
  )
222
- await state.enqueue(document_annotation)
212
+ await state.enqueue_annotations(document_annotation)
223
213
  elif len(names) == 1 and names[0] in ("context.span_id", "span_id"):
224
214
  for index, row in dataframe.iterrows():
225
215
  score, label, explanation = _get_annotation_result(row)
@@ -233,7 +223,7 @@ async def _add_evaluations(state: State, evaluations: Evaluations) -> None:
233
223
  explanation=explanation,
234
224
  metadata_={},
235
225
  )
236
- await state.enqueue(span_annotation)
226
+ await state.enqueue_annotations(span_annotation)
237
227
  elif len(names) == 1 and names[0] in ("context.trace_id", "trace_id"):
238
228
  for index, row in dataframe.iterrows():
239
229
  score, label, explanation = _get_annotation_result(row)
@@ -247,7 +237,7 @@ async def _add_evaluations(state: State, evaluations: Evaluations) -> None:
247
237
  explanation=explanation,
248
238
  metadata_={},
249
239
  )
250
- await state.enqueue(trace_annotation)
240
+ await state.enqueue_annotations(trace_annotation)
251
241
 
252
242
 
253
243
  def _get_annotation_result(
@@ -268,6 +258,7 @@ def _document_annotation_factory(
268
258
  Callable[..., Precursors.DocumentAnnotation],
269
259
  ]:
270
260
  return lambda index: lambda **kwargs: Precursors.DocumentAnnotation(
261
+ datetime.now(timezone.utc),
271
262
  span_id=str(index[span_id_idx]),
272
263
  document_position=int(index[document_position_idx]),
273
264
  obj=models.DocumentAnnotation(
@@ -279,6 +270,7 @@ def _document_annotation_factory(
279
270
 
280
271
  def _span_annotation_factory(span_id: str) -> Callable[..., Precursors.SpanAnnotation]:
281
272
  return lambda **kwargs: Precursors.SpanAnnotation(
273
+ datetime.now(timezone.utc),
282
274
  span_id=str(span_id),
283
275
  obj=models.SpanAnnotation(**kwargs),
284
276
  )
@@ -286,6 +278,7 @@ def _span_annotation_factory(span_id: str) -> Callable[..., Precursors.SpanAnnot
286
278
 
287
279
  def _trace_annotation_factory(trace_id: str) -> Callable[..., Precursors.TraceAnnotation]:
288
280
  return lambda **kwargs: Precursors.TraceAnnotation(
281
+ datetime.now(timezone.utc),
289
282
  trace_id=str(trace_id),
290
283
  obj=models.TraceAnnotation(**kwargs),
291
284
  )
@@ -1,11 +1,12 @@
1
1
  from datetime import datetime
2
2
  from typing import Any, Literal, Optional
3
3
 
4
+ from dateutil.parser import isoparse
4
5
  from fastapi import APIRouter, HTTPException
5
- from pydantic import Field
6
+ from pydantic import Field, model_validator
6
7
  from starlette.requests import Request
7
- from starlette.status import HTTP_404_NOT_FOUND
8
8
  from strawberry.relay import GlobalID
9
+ from typing_extensions import Self
9
10
 
10
11
  from phoenix.db import models
11
12
  from phoenix.db.helpers import SupportedSQLDialect
@@ -35,15 +36,25 @@ class UpsertExperimentEvaluationRequestBody(V1RoutesBaseModel):
35
36
  )
36
37
  start_time: datetime = Field(description="The start time of the evaluation in ISO format")
37
38
  end_time: datetime = Field(description="The end time of the evaluation in ISO format")
38
- result: ExperimentEvaluationResult = Field(description="The result of the evaluation")
39
+ result: Optional[ExperimentEvaluationResult] = Field(
40
+ None, description="The result of the evaluation. Either result or error must be provided."
41
+ )
39
42
  error: Optional[str] = Field(
40
- None, description="Optional error message if the evaluation encountered an error"
43
+ None,
44
+ description="Error message if the evaluation encountered an error. "
45
+ "Either result or error must be provided.",
41
46
  )
42
47
  metadata: Optional[dict[str, Any]] = Field(
43
48
  default=None, description="Metadata for the evaluation"
44
49
  )
45
50
  trace_id: Optional[str] = Field(default=None, description="Optional trace ID for tracking")
46
51
 
52
+ @model_validator(mode="after")
53
+ def validate_result_or_error(self) -> Self:
54
+ if self.result is None and self.error is None:
55
+ raise ValueError("Either 'result' or 'error' must be provided")
56
+ return self
57
+
47
58
 
48
59
  class UpsertExperimentEvaluationResponseBodyData(V1RoutesBaseModel):
49
60
  id: str = Field(description="The ID of the upserted experiment evaluation")
@@ -60,7 +71,7 @@ class UpsertExperimentEvaluationResponseBody(
60
71
  operation_id="upsertExperimentEvaluation",
61
72
  summary="Create or update evaluation for an experiment run",
62
73
  responses=add_errors_to_responses(
63
- [{"status_code": HTTP_404_NOT_FOUND, "description": "Experiment run not found"}]
74
+ [{"status_code": 404, "description": "Experiment run not found"}]
64
75
  ),
65
76
  )
66
77
  async def upsert_experiment_evaluation(
@@ -73,7 +84,7 @@ async def upsert_experiment_evaluation(
73
84
  except ValueError:
74
85
  raise HTTPException(
75
86
  detail=f"ExperimentRun with ID {experiment_run_gid} does not exist",
76
- status_code=HTTP_404_NOT_FOUND,
87
+ status_code=404,
77
88
  )
78
89
  name = request_body.name
79
90
  annotator_kind = request_body.annotator_kind
@@ -95,8 +106,8 @@ async def upsert_experiment_evaluation(
95
106
  explanation=explanation,
96
107
  error=error,
97
108
  metadata_=metadata, # `metadata_` must match database
98
- start_time=datetime.fromisoformat(start_time),
99
- end_time=datetime.fromisoformat(end_time),
109
+ start_time=isoparse(start_time),
110
+ end_time=isoparse(end_time),
100
111
  trace_id=payload.get("trace_id"),
101
112
  )
102
113
  dialect = SupportedSQLDialect(session.bind.dialect.name)