arize-phoenix 3.16.1__py3-none-any.whl → 7.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of arize-phoenix might be problematic. Click here for more details.

Files changed (338) hide show
  1. arize_phoenix-7.7.0.dist-info/METADATA +261 -0
  2. arize_phoenix-7.7.0.dist-info/RECORD +345 -0
  3. {arize_phoenix-3.16.1.dist-info → arize_phoenix-7.7.0.dist-info}/WHEEL +1 -1
  4. arize_phoenix-7.7.0.dist-info/entry_points.txt +3 -0
  5. phoenix/__init__.py +86 -14
  6. phoenix/auth.py +309 -0
  7. phoenix/config.py +675 -45
  8. phoenix/core/model.py +32 -30
  9. phoenix/core/model_schema.py +102 -109
  10. phoenix/core/model_schema_adapter.py +48 -45
  11. phoenix/datetime_utils.py +24 -3
  12. phoenix/db/README.md +54 -0
  13. phoenix/db/__init__.py +4 -0
  14. phoenix/db/alembic.ini +85 -0
  15. phoenix/db/bulk_inserter.py +294 -0
  16. phoenix/db/engines.py +208 -0
  17. phoenix/db/enums.py +20 -0
  18. phoenix/db/facilitator.py +113 -0
  19. phoenix/db/helpers.py +159 -0
  20. phoenix/db/insertion/constants.py +2 -0
  21. phoenix/db/insertion/dataset.py +227 -0
  22. phoenix/db/insertion/document_annotation.py +171 -0
  23. phoenix/db/insertion/evaluation.py +191 -0
  24. phoenix/db/insertion/helpers.py +98 -0
  25. phoenix/db/insertion/span.py +193 -0
  26. phoenix/db/insertion/span_annotation.py +158 -0
  27. phoenix/db/insertion/trace_annotation.py +158 -0
  28. phoenix/db/insertion/types.py +256 -0
  29. phoenix/db/migrate.py +86 -0
  30. phoenix/db/migrations/data_migration_scripts/populate_project_sessions.py +199 -0
  31. phoenix/db/migrations/env.py +114 -0
  32. phoenix/db/migrations/script.py.mako +26 -0
  33. phoenix/db/migrations/versions/10460e46d750_datasets.py +317 -0
  34. phoenix/db/migrations/versions/3be8647b87d8_add_token_columns_to_spans_table.py +126 -0
  35. phoenix/db/migrations/versions/4ded9e43755f_create_project_sessions_table.py +66 -0
  36. phoenix/db/migrations/versions/cd164e83824f_users_and_tokens.py +157 -0
  37. phoenix/db/migrations/versions/cf03bd6bae1d_init.py +280 -0
  38. phoenix/db/models.py +807 -0
  39. phoenix/exceptions.py +5 -1
  40. phoenix/experiments/__init__.py +6 -0
  41. phoenix/experiments/evaluators/__init__.py +29 -0
  42. phoenix/experiments/evaluators/base.py +158 -0
  43. phoenix/experiments/evaluators/code_evaluators.py +184 -0
  44. phoenix/experiments/evaluators/llm_evaluators.py +473 -0
  45. phoenix/experiments/evaluators/utils.py +236 -0
  46. phoenix/experiments/functions.py +772 -0
  47. phoenix/experiments/tracing.py +86 -0
  48. phoenix/experiments/types.py +726 -0
  49. phoenix/experiments/utils.py +25 -0
  50. phoenix/inferences/__init__.py +0 -0
  51. phoenix/{datasets → inferences}/errors.py +6 -5
  52. phoenix/{datasets → inferences}/fixtures.py +49 -42
  53. phoenix/{datasets/dataset.py → inferences/inferences.py} +121 -105
  54. phoenix/{datasets → inferences}/schema.py +11 -11
  55. phoenix/{datasets → inferences}/validation.py +13 -14
  56. phoenix/logging/__init__.py +3 -0
  57. phoenix/logging/_config.py +90 -0
  58. phoenix/logging/_filter.py +6 -0
  59. phoenix/logging/_formatter.py +69 -0
  60. phoenix/metrics/__init__.py +5 -4
  61. phoenix/metrics/binning.py +4 -3
  62. phoenix/metrics/metrics.py +2 -1
  63. phoenix/metrics/mixins.py +7 -6
  64. phoenix/metrics/retrieval_metrics.py +2 -1
  65. phoenix/metrics/timeseries.py +5 -4
  66. phoenix/metrics/wrappers.py +9 -3
  67. phoenix/pointcloud/clustering.py +5 -5
  68. phoenix/pointcloud/pointcloud.py +7 -5
  69. phoenix/pointcloud/projectors.py +5 -6
  70. phoenix/pointcloud/umap_parameters.py +53 -52
  71. phoenix/server/api/README.md +28 -0
  72. phoenix/server/api/auth.py +44 -0
  73. phoenix/server/api/context.py +152 -9
  74. phoenix/server/api/dataloaders/__init__.py +91 -0
  75. phoenix/server/api/dataloaders/annotation_summaries.py +139 -0
  76. phoenix/server/api/dataloaders/average_experiment_run_latency.py +54 -0
  77. phoenix/server/api/dataloaders/cache/__init__.py +3 -0
  78. phoenix/server/api/dataloaders/cache/two_tier_cache.py +68 -0
  79. phoenix/server/api/dataloaders/dataset_example_revisions.py +131 -0
  80. phoenix/server/api/dataloaders/dataset_example_spans.py +38 -0
  81. phoenix/server/api/dataloaders/document_evaluation_summaries.py +144 -0
  82. phoenix/server/api/dataloaders/document_evaluations.py +31 -0
  83. phoenix/server/api/dataloaders/document_retrieval_metrics.py +89 -0
  84. phoenix/server/api/dataloaders/experiment_annotation_summaries.py +79 -0
  85. phoenix/server/api/dataloaders/experiment_error_rates.py +58 -0
  86. phoenix/server/api/dataloaders/experiment_run_annotations.py +36 -0
  87. phoenix/server/api/dataloaders/experiment_run_counts.py +49 -0
  88. phoenix/server/api/dataloaders/experiment_sequence_number.py +44 -0
  89. phoenix/server/api/dataloaders/latency_ms_quantile.py +188 -0
  90. phoenix/server/api/dataloaders/min_start_or_max_end_times.py +85 -0
  91. phoenix/server/api/dataloaders/project_by_name.py +31 -0
  92. phoenix/server/api/dataloaders/record_counts.py +116 -0
  93. phoenix/server/api/dataloaders/session_io.py +79 -0
  94. phoenix/server/api/dataloaders/session_num_traces.py +30 -0
  95. phoenix/server/api/dataloaders/session_num_traces_with_error.py +32 -0
  96. phoenix/server/api/dataloaders/session_token_usages.py +41 -0
  97. phoenix/server/api/dataloaders/session_trace_latency_ms_quantile.py +55 -0
  98. phoenix/server/api/dataloaders/span_annotations.py +26 -0
  99. phoenix/server/api/dataloaders/span_dataset_examples.py +31 -0
  100. phoenix/server/api/dataloaders/span_descendants.py +57 -0
  101. phoenix/server/api/dataloaders/span_projects.py +33 -0
  102. phoenix/server/api/dataloaders/token_counts.py +124 -0
  103. phoenix/server/api/dataloaders/trace_by_trace_ids.py +25 -0
  104. phoenix/server/api/dataloaders/trace_root_spans.py +32 -0
  105. phoenix/server/api/dataloaders/user_roles.py +30 -0
  106. phoenix/server/api/dataloaders/users.py +33 -0
  107. phoenix/server/api/exceptions.py +48 -0
  108. phoenix/server/api/helpers/__init__.py +12 -0
  109. phoenix/server/api/helpers/dataset_helpers.py +217 -0
  110. phoenix/server/api/helpers/experiment_run_filters.py +763 -0
  111. phoenix/server/api/helpers/playground_clients.py +948 -0
  112. phoenix/server/api/helpers/playground_registry.py +70 -0
  113. phoenix/server/api/helpers/playground_spans.py +455 -0
  114. phoenix/server/api/input_types/AddExamplesToDatasetInput.py +16 -0
  115. phoenix/server/api/input_types/AddSpansToDatasetInput.py +14 -0
  116. phoenix/server/api/input_types/ChatCompletionInput.py +38 -0
  117. phoenix/server/api/input_types/ChatCompletionMessageInput.py +24 -0
  118. phoenix/server/api/input_types/ClearProjectInput.py +15 -0
  119. phoenix/server/api/input_types/ClusterInput.py +2 -2
  120. phoenix/server/api/input_types/CreateDatasetInput.py +12 -0
  121. phoenix/server/api/input_types/CreateSpanAnnotationInput.py +18 -0
  122. phoenix/server/api/input_types/CreateTraceAnnotationInput.py +18 -0
  123. phoenix/server/api/input_types/DataQualityMetricInput.py +5 -2
  124. phoenix/server/api/input_types/DatasetExampleInput.py +14 -0
  125. phoenix/server/api/input_types/DatasetSort.py +17 -0
  126. phoenix/server/api/input_types/DatasetVersionSort.py +16 -0
  127. phoenix/server/api/input_types/DeleteAnnotationsInput.py +7 -0
  128. phoenix/server/api/input_types/DeleteDatasetExamplesInput.py +13 -0
  129. phoenix/server/api/input_types/DeleteDatasetInput.py +7 -0
  130. phoenix/server/api/input_types/DeleteExperimentsInput.py +7 -0
  131. phoenix/server/api/input_types/DimensionFilter.py +4 -4
  132. phoenix/server/api/input_types/GenerativeModelInput.py +17 -0
  133. phoenix/server/api/input_types/Granularity.py +1 -1
  134. phoenix/server/api/input_types/InvocationParameters.py +162 -0
  135. phoenix/server/api/input_types/PatchAnnotationInput.py +19 -0
  136. phoenix/server/api/input_types/PatchDatasetExamplesInput.py +35 -0
  137. phoenix/server/api/input_types/PatchDatasetInput.py +14 -0
  138. phoenix/server/api/input_types/PerformanceMetricInput.py +5 -2
  139. phoenix/server/api/input_types/ProjectSessionSort.py +29 -0
  140. phoenix/server/api/input_types/SpanAnnotationSort.py +17 -0
  141. phoenix/server/api/input_types/SpanSort.py +134 -69
  142. phoenix/server/api/input_types/TemplateOptions.py +10 -0
  143. phoenix/server/api/input_types/TraceAnnotationSort.py +17 -0
  144. phoenix/server/api/input_types/UserRoleInput.py +9 -0
  145. phoenix/server/api/mutations/__init__.py +28 -0
  146. phoenix/server/api/mutations/api_key_mutations.py +167 -0
  147. phoenix/server/api/mutations/chat_mutations.py +593 -0
  148. phoenix/server/api/mutations/dataset_mutations.py +591 -0
  149. phoenix/server/api/mutations/experiment_mutations.py +75 -0
  150. phoenix/server/api/{types/ExportEventsMutation.py → mutations/export_events_mutations.py} +21 -18
  151. phoenix/server/api/mutations/project_mutations.py +57 -0
  152. phoenix/server/api/mutations/span_annotations_mutations.py +128 -0
  153. phoenix/server/api/mutations/trace_annotations_mutations.py +127 -0
  154. phoenix/server/api/mutations/user_mutations.py +329 -0
  155. phoenix/server/api/openapi/__init__.py +0 -0
  156. phoenix/server/api/openapi/main.py +17 -0
  157. phoenix/server/api/openapi/schema.py +16 -0
  158. phoenix/server/api/queries.py +738 -0
  159. phoenix/server/api/routers/__init__.py +11 -0
  160. phoenix/server/api/routers/auth.py +284 -0
  161. phoenix/server/api/routers/embeddings.py +26 -0
  162. phoenix/server/api/routers/oauth2.py +488 -0
  163. phoenix/server/api/routers/v1/__init__.py +64 -0
  164. phoenix/server/api/routers/v1/datasets.py +1017 -0
  165. phoenix/server/api/routers/v1/evaluations.py +362 -0
  166. phoenix/server/api/routers/v1/experiment_evaluations.py +115 -0
  167. phoenix/server/api/routers/v1/experiment_runs.py +167 -0
  168. phoenix/server/api/routers/v1/experiments.py +308 -0
  169. phoenix/server/api/routers/v1/pydantic_compat.py +78 -0
  170. phoenix/server/api/routers/v1/spans.py +267 -0
  171. phoenix/server/api/routers/v1/traces.py +208 -0
  172. phoenix/server/api/routers/v1/utils.py +95 -0
  173. phoenix/server/api/schema.py +44 -241
  174. phoenix/server/api/subscriptions.py +597 -0
  175. phoenix/server/api/types/Annotation.py +21 -0
  176. phoenix/server/api/types/AnnotationSummary.py +55 -0
  177. phoenix/server/api/types/AnnotatorKind.py +16 -0
  178. phoenix/server/api/types/ApiKey.py +27 -0
  179. phoenix/server/api/types/AuthMethod.py +9 -0
  180. phoenix/server/api/types/ChatCompletionMessageRole.py +11 -0
  181. phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +46 -0
  182. phoenix/server/api/types/Cluster.py +25 -24
  183. phoenix/server/api/types/CreateDatasetPayload.py +8 -0
  184. phoenix/server/api/types/DataQualityMetric.py +31 -13
  185. phoenix/server/api/types/Dataset.py +288 -63
  186. phoenix/server/api/types/DatasetExample.py +85 -0
  187. phoenix/server/api/types/DatasetExampleRevision.py +34 -0
  188. phoenix/server/api/types/DatasetVersion.py +14 -0
  189. phoenix/server/api/types/Dimension.py +32 -31
  190. phoenix/server/api/types/DocumentEvaluationSummary.py +9 -8
  191. phoenix/server/api/types/EmbeddingDimension.py +56 -49
  192. phoenix/server/api/types/Evaluation.py +25 -31
  193. phoenix/server/api/types/EvaluationSummary.py +30 -50
  194. phoenix/server/api/types/Event.py +20 -20
  195. phoenix/server/api/types/ExampleRevisionInterface.py +14 -0
  196. phoenix/server/api/types/Experiment.py +152 -0
  197. phoenix/server/api/types/ExperimentAnnotationSummary.py +13 -0
  198. phoenix/server/api/types/ExperimentComparison.py +17 -0
  199. phoenix/server/api/types/ExperimentRun.py +119 -0
  200. phoenix/server/api/types/ExperimentRunAnnotation.py +56 -0
  201. phoenix/server/api/types/GenerativeModel.py +9 -0
  202. phoenix/server/api/types/GenerativeProvider.py +85 -0
  203. phoenix/server/api/types/Inferences.py +80 -0
  204. phoenix/server/api/types/InferencesRole.py +23 -0
  205. phoenix/server/api/types/LabelFraction.py +7 -0
  206. phoenix/server/api/types/MimeType.py +2 -2
  207. phoenix/server/api/types/Model.py +54 -54
  208. phoenix/server/api/types/PerformanceMetric.py +8 -5
  209. phoenix/server/api/types/Project.py +407 -142
  210. phoenix/server/api/types/ProjectSession.py +139 -0
  211. phoenix/server/api/types/Segments.py +4 -4
  212. phoenix/server/api/types/Span.py +221 -176
  213. phoenix/server/api/types/SpanAnnotation.py +43 -0
  214. phoenix/server/api/types/SpanIOValue.py +15 -0
  215. phoenix/server/api/types/SystemApiKey.py +9 -0
  216. phoenix/server/api/types/TemplateLanguage.py +10 -0
  217. phoenix/server/api/types/TimeSeries.py +19 -15
  218. phoenix/server/api/types/TokenUsage.py +11 -0
  219. phoenix/server/api/types/Trace.py +154 -0
  220. phoenix/server/api/types/TraceAnnotation.py +45 -0
  221. phoenix/server/api/types/UMAPPoints.py +7 -7
  222. phoenix/server/api/types/User.py +60 -0
  223. phoenix/server/api/types/UserApiKey.py +45 -0
  224. phoenix/server/api/types/UserRole.py +15 -0
  225. phoenix/server/api/types/node.py +4 -112
  226. phoenix/server/api/types/pagination.py +156 -57
  227. phoenix/server/api/utils.py +34 -0
  228. phoenix/server/app.py +864 -115
  229. phoenix/server/bearer_auth.py +163 -0
  230. phoenix/server/dml_event.py +136 -0
  231. phoenix/server/dml_event_handler.py +256 -0
  232. phoenix/server/email/__init__.py +0 -0
  233. phoenix/server/email/sender.py +97 -0
  234. phoenix/server/email/templates/__init__.py +0 -0
  235. phoenix/server/email/templates/password_reset.html +19 -0
  236. phoenix/server/email/types.py +11 -0
  237. phoenix/server/grpc_server.py +102 -0
  238. phoenix/server/jwt_store.py +505 -0
  239. phoenix/server/main.py +305 -116
  240. phoenix/server/oauth2.py +52 -0
  241. phoenix/server/openapi/__init__.py +0 -0
  242. phoenix/server/prometheus.py +111 -0
  243. phoenix/server/rate_limiters.py +188 -0
  244. phoenix/server/static/.vite/manifest.json +87 -0
  245. phoenix/server/static/assets/components-Cy9nwIvF.js +2125 -0
  246. phoenix/server/static/assets/index-BKvHIxkk.js +113 -0
  247. phoenix/server/static/assets/pages-CUi2xCVQ.js +4449 -0
  248. phoenix/server/static/assets/vendor-DvC8cT4X.js +894 -0
  249. phoenix/server/static/assets/vendor-DxkFTwjz.css +1 -0
  250. phoenix/server/static/assets/vendor-arizeai-Do1793cv.js +662 -0
  251. phoenix/server/static/assets/vendor-codemirror-BzwZPyJM.js +24 -0
  252. phoenix/server/static/assets/vendor-recharts-_Jb7JjhG.js +59 -0
  253. phoenix/server/static/assets/vendor-shiki-Cl9QBraO.js +5 -0
  254. phoenix/server/static/assets/vendor-three-DwGkEfCM.js +2998 -0
  255. phoenix/server/telemetry.py +68 -0
  256. phoenix/server/templates/index.html +82 -23
  257. phoenix/server/thread_server.py +3 -3
  258. phoenix/server/types.py +275 -0
  259. phoenix/services.py +27 -18
  260. phoenix/session/client.py +743 -68
  261. phoenix/session/data_extractor.py +31 -7
  262. phoenix/session/evaluation.py +3 -9
  263. phoenix/session/session.py +263 -219
  264. phoenix/settings.py +22 -0
  265. phoenix/trace/__init__.py +2 -22
  266. phoenix/trace/attributes.py +338 -0
  267. phoenix/trace/dsl/README.md +116 -0
  268. phoenix/trace/dsl/filter.py +663 -213
  269. phoenix/trace/dsl/helpers.py +73 -21
  270. phoenix/trace/dsl/query.py +574 -201
  271. phoenix/trace/exporter.py +24 -19
  272. phoenix/trace/fixtures.py +368 -32
  273. phoenix/trace/otel.py +71 -219
  274. phoenix/trace/projects.py +3 -2
  275. phoenix/trace/schemas.py +33 -11
  276. phoenix/trace/span_evaluations.py +21 -16
  277. phoenix/trace/span_json_decoder.py +6 -4
  278. phoenix/trace/span_json_encoder.py +2 -2
  279. phoenix/trace/trace_dataset.py +47 -32
  280. phoenix/trace/utils.py +21 -4
  281. phoenix/utilities/__init__.py +0 -26
  282. phoenix/utilities/client.py +132 -0
  283. phoenix/utilities/deprecation.py +31 -0
  284. phoenix/utilities/error_handling.py +3 -2
  285. phoenix/utilities/json.py +109 -0
  286. phoenix/utilities/logging.py +8 -0
  287. phoenix/utilities/project.py +2 -2
  288. phoenix/utilities/re.py +49 -0
  289. phoenix/utilities/span_store.py +0 -23
  290. phoenix/utilities/template_formatters.py +99 -0
  291. phoenix/version.py +1 -1
  292. arize_phoenix-3.16.1.dist-info/METADATA +0 -495
  293. arize_phoenix-3.16.1.dist-info/RECORD +0 -178
  294. phoenix/core/project.py +0 -619
  295. phoenix/core/traces.py +0 -96
  296. phoenix/experimental/evals/__init__.py +0 -73
  297. phoenix/experimental/evals/evaluators.py +0 -413
  298. phoenix/experimental/evals/functions/__init__.py +0 -4
  299. phoenix/experimental/evals/functions/classify.py +0 -453
  300. phoenix/experimental/evals/functions/executor.py +0 -353
  301. phoenix/experimental/evals/functions/generate.py +0 -138
  302. phoenix/experimental/evals/functions/processing.py +0 -76
  303. phoenix/experimental/evals/models/__init__.py +0 -14
  304. phoenix/experimental/evals/models/anthropic.py +0 -175
  305. phoenix/experimental/evals/models/base.py +0 -170
  306. phoenix/experimental/evals/models/bedrock.py +0 -221
  307. phoenix/experimental/evals/models/litellm.py +0 -134
  308. phoenix/experimental/evals/models/openai.py +0 -448
  309. phoenix/experimental/evals/models/rate_limiters.py +0 -246
  310. phoenix/experimental/evals/models/vertex.py +0 -173
  311. phoenix/experimental/evals/models/vertexai.py +0 -186
  312. phoenix/experimental/evals/retrievals.py +0 -96
  313. phoenix/experimental/evals/templates/__init__.py +0 -50
  314. phoenix/experimental/evals/templates/default_templates.py +0 -472
  315. phoenix/experimental/evals/templates/template.py +0 -195
  316. phoenix/experimental/evals/utils/__init__.py +0 -172
  317. phoenix/experimental/evals/utils/threads.py +0 -27
  318. phoenix/server/api/helpers.py +0 -11
  319. phoenix/server/api/routers/evaluation_handler.py +0 -109
  320. phoenix/server/api/routers/span_handler.py +0 -70
  321. phoenix/server/api/routers/trace_handler.py +0 -60
  322. phoenix/server/api/types/DatasetRole.py +0 -23
  323. phoenix/server/static/index.css +0 -6
  324. phoenix/server/static/index.js +0 -7447
  325. phoenix/storage/span_store/__init__.py +0 -23
  326. phoenix/storage/span_store/text_file.py +0 -85
  327. phoenix/trace/dsl/missing.py +0 -60
  328. phoenix/trace/langchain/__init__.py +0 -3
  329. phoenix/trace/langchain/instrumentor.py +0 -35
  330. phoenix/trace/llama_index/__init__.py +0 -3
  331. phoenix/trace/llama_index/callback.py +0 -102
  332. phoenix/trace/openai/__init__.py +0 -3
  333. phoenix/trace/openai/instrumentor.py +0 -30
  334. {arize_phoenix-3.16.1.dist-info → arize_phoenix-7.7.0.dist-info}/licenses/IP_NOTICE +0 -0
  335. {arize_phoenix-3.16.1.dist-info → arize_phoenix-7.7.0.dist-info}/licenses/LICENSE +0 -0
  336. /phoenix/{datasets → db/insertion}/__init__.py +0 -0
  337. /phoenix/{experimental → db/migrations}/__init__.py +0 -0
  338. /phoenix/{storage → db/migrations/data_migration_scripts}/__init__.py +0 -0
@@ -0,0 +1,308 @@
1
+ from datetime import datetime
2
+ from random import getrandbits
3
+ from typing import Any, Optional
4
+
5
+ from fastapi import APIRouter, HTTPException, Path
6
+ from pydantic import Field
7
+ from sqlalchemy import select
8
+ from starlette.requests import Request
9
+ from starlette.status import HTTP_404_NOT_FOUND
10
+ from strawberry.relay import GlobalID
11
+
12
+ from phoenix.db import models
13
+ from phoenix.db.helpers import SupportedSQLDialect
14
+ from phoenix.db.insertion.helpers import insert_on_conflict
15
+ from phoenix.server.api.types.node import from_global_id_with_expected_type
16
+ from phoenix.server.dml_event import ExperimentInsertEvent
17
+
18
+ from .pydantic_compat import V1RoutesBaseModel
19
+ from .utils import ResponseBody, add_errors_to_responses
20
+
21
+ router = APIRouter(tags=["experiments"], include_in_schema=True)
22
+
23
+
24
+ def _short_uuid() -> str:
25
+ return str(getrandbits(32).to_bytes(4, "big").hex())
26
+
27
+
28
+ def _generate_experiment_name(dataset_name: str) -> str:
29
+ """
30
+ Generate a semi-unique name for the experiment.
31
+ """
32
+ short_ds_name = dataset_name[:8].replace(" ", "-")
33
+ return f"{short_ds_name}-{_short_uuid()}"
34
+
35
+
36
+ class Experiment(V1RoutesBaseModel):
37
+ id: str = Field(description="The ID of the experiment")
38
+ dataset_id: str = Field(description="The ID of the dataset associated with the experiment")
39
+ dataset_version_id: str = Field(
40
+ description="The ID of the dataset version associated with the experiment"
41
+ )
42
+ repetitions: int = Field(description="Number of times the experiment is repeated")
43
+ metadata: dict[str, Any] = Field(description="Metadata of the experiment")
44
+ project_name: Optional[str] = Field(
45
+ description="The name of the project associated with the experiment"
46
+ )
47
+ created_at: datetime = Field(description="The creation timestamp of the experiment")
48
+ updated_at: datetime = Field(description="The last update timestamp of the experiment")
49
+
50
+
51
+ class CreateExperimentRequestBody(V1RoutesBaseModel):
52
+ """
53
+ Details of the experiment to be created
54
+ """
55
+
56
+ name: Optional[str] = Field(
57
+ default=None,
58
+ description=("Name of the experiment (if omitted, a random name will be generated)"),
59
+ )
60
+ description: Optional[str] = Field(
61
+ default=None, description="An optional description of the experiment"
62
+ )
63
+ metadata: Optional[dict[str, Any]] = Field(
64
+ default=None, description="Metadata for the experiment"
65
+ )
66
+ version_id: Optional[str] = Field(
67
+ default=None,
68
+ description=(
69
+ "ID of the dataset version over which the experiment will be run "
70
+ "(if omitted, the latest version will be used)"
71
+ ),
72
+ )
73
+ repetitions: int = Field(
74
+ default=1, description="Number of times the experiment should be repeated for each example"
75
+ )
76
+
77
+
78
+ class CreateExperimentResponseBody(ResponseBody[Experiment]):
79
+ pass
80
+
81
+
82
+ @router.post(
83
+ "/datasets/{dataset_id}/experiments",
84
+ operation_id="createExperiment",
85
+ summary="Create experiment on a dataset",
86
+ responses=add_errors_to_responses(
87
+ [{"status_code": HTTP_404_NOT_FOUND, "description": "Dataset or DatasetVersion not found"}]
88
+ ),
89
+ response_description="Experiment retrieved successfully",
90
+ )
91
+ async def create_experiment(
92
+ request: Request,
93
+ request_body: CreateExperimentRequestBody,
94
+ dataset_id: str = Path(..., title="Dataset ID"),
95
+ ) -> CreateExperimentResponseBody:
96
+ dataset_globalid = GlobalID.from_id(dataset_id)
97
+ try:
98
+ dataset_rowid = from_global_id_with_expected_type(dataset_globalid, "Dataset")
99
+ except ValueError:
100
+ raise HTTPException(
101
+ detail="Dataset with ID {dataset_globalid} does not exist",
102
+ status_code=HTTP_404_NOT_FOUND,
103
+ )
104
+
105
+ dataset_version_globalid_str = request_body.version_id
106
+ if dataset_version_globalid_str is not None:
107
+ try:
108
+ dataset_version_globalid = GlobalID.from_id(dataset_version_globalid_str)
109
+ dataset_version_id = from_global_id_with_expected_type(
110
+ dataset_version_globalid, "DatasetVersion"
111
+ )
112
+ except ValueError:
113
+ raise HTTPException(
114
+ detail=f"DatasetVersion with ID {dataset_version_globalid_str} does not exist",
115
+ status_code=HTTP_404_NOT_FOUND,
116
+ )
117
+
118
+ async with request.app.state.db() as session:
119
+ result = (
120
+ await session.execute(select(models.Dataset).where(models.Dataset.id == dataset_rowid))
121
+ ).scalar()
122
+ if result is None:
123
+ raise HTTPException(
124
+ detail=f"Dataset with ID {dataset_globalid} does not exist",
125
+ status_code=HTTP_404_NOT_FOUND,
126
+ )
127
+ dataset_name = result.name
128
+ if dataset_version_globalid_str is None:
129
+ dataset_version_result = await session.execute(
130
+ select(models.DatasetVersion)
131
+ .where(models.DatasetVersion.dataset_id == dataset_rowid)
132
+ .order_by(models.DatasetVersion.id.desc())
133
+ )
134
+ dataset_version = dataset_version_result.scalar()
135
+ if not dataset_version:
136
+ raise HTTPException(
137
+ detail=f"Dataset {dataset_globalid} does not have any versions",
138
+ status_code=HTTP_404_NOT_FOUND,
139
+ )
140
+ dataset_version_id = dataset_version.id
141
+ dataset_version_globalid = GlobalID("DatasetVersion", str(dataset_version_id))
142
+ else:
143
+ dataset_version = await session.execute(
144
+ select(models.DatasetVersion).where(models.DatasetVersion.id == dataset_version_id)
145
+ )
146
+ dataset_version = dataset_version.scalar()
147
+ if not dataset_version:
148
+ raise HTTPException(
149
+ detail=f"DatasetVersion with ID {dataset_version_globalid} does not exist",
150
+ status_code=HTTP_404_NOT_FOUND,
151
+ )
152
+
153
+ # generate a semi-unique name for the experiment
154
+ experiment_name = request_body.name or _generate_experiment_name(dataset_name)
155
+ project_name = f"Experiment-{getrandbits(96).to_bytes(12, 'big').hex()}"
156
+ project_description = (
157
+ f"dataset_id: {dataset_globalid}\ndataset_version_id: {dataset_version_globalid}"
158
+ )
159
+ experiment = models.Experiment(
160
+ dataset_id=int(dataset_rowid),
161
+ dataset_version_id=int(dataset_version_id),
162
+ name=experiment_name,
163
+ description=request_body.description,
164
+ repetitions=request_body.repetitions,
165
+ metadata_=request_body.metadata or {},
166
+ project_name=project_name,
167
+ )
168
+ session.add(experiment)
169
+ await session.flush()
170
+
171
+ dialect = SupportedSQLDialect(session.bind.dialect.name)
172
+ project_rowid = await session.scalar(
173
+ insert_on_conflict(
174
+ dict(
175
+ name=project_name,
176
+ description=project_description,
177
+ created_at=experiment.created_at,
178
+ updated_at=experiment.updated_at,
179
+ ),
180
+ dialect=dialect,
181
+ table=models.Project,
182
+ unique_by=("name",),
183
+ ).returning(models.Project.id)
184
+ )
185
+ assert project_rowid is not None
186
+
187
+ experiment_globalid = GlobalID("Experiment", str(experiment.id))
188
+ if dataset_version_globalid_str is None:
189
+ dataset_version_globalid = GlobalID(
190
+ "DatasetVersion", str(experiment.dataset_version_id)
191
+ )
192
+ request.state.event_queue.put(ExperimentInsertEvent((experiment.id,)))
193
+ return CreateExperimentResponseBody(
194
+ data=Experiment(
195
+ id=str(experiment_globalid),
196
+ dataset_id=str(dataset_globalid),
197
+ dataset_version_id=str(dataset_version_globalid),
198
+ repetitions=experiment.repetitions,
199
+ metadata=experiment.metadata_,
200
+ project_name=experiment.project_name,
201
+ created_at=experiment.created_at,
202
+ updated_at=experiment.updated_at,
203
+ )
204
+ )
205
+
206
+
207
+ class GetExperimentResponseBody(ResponseBody[Experiment]):
208
+ pass
209
+
210
+
211
+ @router.get(
212
+ "/experiments/{experiment_id}",
213
+ operation_id="getExperiment",
214
+ summary="Get experiment by ID",
215
+ responses=add_errors_to_responses(
216
+ [{"status_code": HTTP_404_NOT_FOUND, "description": "Experiment not found"}]
217
+ ),
218
+ response_description="Experiment retrieved successfully",
219
+ )
220
+ async def get_experiment(request: Request, experiment_id: str) -> GetExperimentResponseBody:
221
+ experiment_globalid = GlobalID.from_id(experiment_id)
222
+ try:
223
+ experiment_rowid = from_global_id_with_expected_type(experiment_globalid, "Experiment")
224
+ except ValueError:
225
+ raise HTTPException(
226
+ detail="Experiment with ID {experiment_globalid} does not exist",
227
+ status_code=HTTP_404_NOT_FOUND,
228
+ )
229
+
230
+ async with request.app.state.db() as session:
231
+ experiment = await session.execute(
232
+ select(models.Experiment).where(models.Experiment.id == experiment_rowid)
233
+ )
234
+ experiment = experiment.scalar()
235
+ if not experiment:
236
+ raise HTTPException(
237
+ detail=f"Experiment with ID {experiment_globalid} does not exist",
238
+ status_code=HTTP_404_NOT_FOUND,
239
+ )
240
+
241
+ dataset_globalid = GlobalID("Dataset", str(experiment.dataset_id))
242
+ dataset_version_globalid = GlobalID("DatasetVersion", str(experiment.dataset_version_id))
243
+ return GetExperimentResponseBody(
244
+ data=Experiment(
245
+ id=str(experiment_globalid),
246
+ dataset_id=str(dataset_globalid),
247
+ dataset_version_id=str(dataset_version_globalid),
248
+ repetitions=experiment.repetitions,
249
+ metadata=experiment.metadata_,
250
+ project_name=experiment.project_name,
251
+ created_at=experiment.created_at,
252
+ updated_at=experiment.updated_at,
253
+ )
254
+ )
255
+
256
+
257
+ class ListExperimentsResponseBody(ResponseBody[list[Experiment]]):
258
+ pass
259
+
260
+
261
+ @router.get(
262
+ "/datasets/{dataset_id}/experiments",
263
+ operation_id="listExperiments",
264
+ summary="List experiments by dataset",
265
+ response_description="Experiments retrieved successfully",
266
+ )
267
+ async def list_experiments(
268
+ request: Request,
269
+ dataset_id: str = Path(..., title="Dataset ID"),
270
+ ) -> ListExperimentsResponseBody:
271
+ dataset_gid = GlobalID.from_id(dataset_id)
272
+ try:
273
+ dataset_rowid = from_global_id_with_expected_type(dataset_gid, "Dataset")
274
+ except ValueError:
275
+ raise HTTPException(
276
+ detail=f"Dataset with ID {dataset_gid} does not exist",
277
+ status_code=HTTP_404_NOT_FOUND,
278
+ )
279
+ async with request.app.state.db() as session:
280
+ query = (
281
+ select(models.Experiment)
282
+ .where(models.Experiment.dataset_id == dataset_rowid)
283
+ .order_by(models.Experiment.id.desc())
284
+ )
285
+
286
+ result = await session.execute(query)
287
+ experiments = result.scalars().all()
288
+
289
+ if not experiments:
290
+ return ListExperimentsResponseBody(data=[])
291
+
292
+ data = [
293
+ Experiment(
294
+ id=str(GlobalID("Experiment", str(experiment.id))),
295
+ dataset_id=str(GlobalID("Dataset", str(experiment.dataset_id))),
296
+ dataset_version_id=str(
297
+ GlobalID("DatasetVersion", str(experiment.dataset_version_id))
298
+ ),
299
+ repetitions=experiment.repetitions,
300
+ metadata=experiment.metadata_,
301
+ project_name=None,
302
+ created_at=experiment.created_at,
303
+ updated_at=experiment.updated_at,
304
+ )
305
+ for experiment in experiments
306
+ ]
307
+
308
+ return ListExperimentsResponseBody(data=data)
@@ -0,0 +1,78 @@
1
+ from datetime import datetime
2
+ from enum import Enum
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from pydantic import BaseModel
6
+ from typing_extensions import assert_never
7
+
8
+
9
+ def datetime_encoder(dt: datetime) -> str:
10
+ """
11
+ Encodes a `datetime` object to an ISO-formatted timestamp string.
12
+
13
+ By default, Pydantic v2 serializes `datetime` objects in a format that
14
+ cannot be parsed by `datetime.fromisoformat`. Adding this encoder to the
15
+ `json_encoders` config for a Pydantic model ensures that the serialized
16
+ `datetime` objects are parseable.
17
+ """
18
+ return dt.isoformat()
19
+
20
+
21
+ class PydanticMajorVersion(Enum):
22
+ """
23
+ The major version of `pydantic`.
24
+ """
25
+
26
+ V1 = "v1"
27
+ V2 = "v2"
28
+
29
+
30
+ def get_pydantic_major_version() -> PydanticMajorVersion:
31
+ """
32
+ Returns the major version of `pydantic` or raises an error if `pydantic` is
33
+ not installed.
34
+ """
35
+ try:
36
+ pydantic_version = version("pydantic")
37
+ except PackageNotFoundError:
38
+ raise RuntimeError("Please install pydantic with `pip install pydantic`.")
39
+ if pydantic_version.startswith("1"):
40
+ return PydanticMajorVersion.V1
41
+ elif pydantic_version.startswith("2"):
42
+ return PydanticMajorVersion.V2
43
+ raise ValueError(f"Unsupported Pydantic version: {pydantic_version}")
44
+
45
+
46
+ if (pydantic_major_version := get_pydantic_major_version()) is PydanticMajorVersion.V1:
47
+
48
+ class V1RoutesBaseModel(BaseModel):
49
+ class Config:
50
+ json_encoders = {datetime: datetime_encoder}
51
+
52
+ elif pydantic_major_version is PydanticMajorVersion.V2:
53
+ from pydantic import ConfigDict
54
+
55
+ # `json_encoders` is a configuration setting from Pydantic v1 that was
56
+ # removed in Pydantic v2.0.* but restored in Pydantic v2.1.0 with a
57
+ # deprecation warning. At this time, it remains the simplest way to
58
+ # configure custom JSON serialization for specific data types in a manner
59
+ # that is consistent between Pydantic v1 and v2.
60
+ #
61
+ # For details, see:
62
+ # - https://github.com/pydantic/pydantic/pull/6811
63
+ # - https://github.com/pydantic/pydantic/releases/tag/v2.1.0
64
+ #
65
+ # The assertion below is added in case a future release of Pydantic v2 fully
66
+ # removes the `json_encoders` parameter.
67
+ assert "json_encoders" in ConfigDict.__annotations__, (
68
+ "If you encounter this error with `pydantic==2.0.*`, "
69
+ "please upgrade `pydantic` with `pip install -U pydantic>=2.1.0`. "
70
+ "If you encounter this error with `pydantic>=2.1.0`, "
71
+ "please upgrade `arize-phoenix` with `pip install -U arize-phoenix`, "
72
+ "or downgrade `pydantic` to a version that supports the `json_encoders` config setting."
73
+ )
74
+
75
+ class V1RoutesBaseModel(BaseModel): # type: ignore[no-redef]
76
+ model_config = ConfigDict({"json_encoders": {datetime: datetime_encoder}})
77
+ else:
78
+ assert_never(pydantic_major_version)
@@ -0,0 +1,267 @@
1
+ from asyncio import get_running_loop
2
+ from collections.abc import AsyncIterator
3
+ from datetime import datetime, timezone
4
+ from secrets import token_urlsafe
5
+ from typing import Any, Literal, Optional
6
+
7
+ import pandas as pd
8
+ from fastapi import APIRouter, Header, HTTPException, Query
9
+ from pydantic import Field
10
+ from sqlalchemy import select
11
+ from starlette.requests import Request
12
+ from starlette.responses import Response, StreamingResponse
13
+ from starlette.status import HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY
14
+ from strawberry.relay import GlobalID
15
+
16
+ from phoenix.config import DEFAULT_PROJECT_NAME
17
+ from phoenix.datetime_utils import normalize_datetime
18
+ from phoenix.db import models
19
+ from phoenix.db.helpers import SupportedSQLDialect
20
+ from phoenix.db.insertion.helpers import as_kv, insert_on_conflict
21
+ from phoenix.db.insertion.types import Precursors
22
+ from phoenix.server.api.routers.utils import df_to_bytes
23
+ from phoenix.server.dml_event import SpanAnnotationInsertEvent
24
+ from phoenix.trace.dsl import SpanQuery as SpanQuery_
25
+ from phoenix.utilities.json import encode_df_as_json_string
26
+
27
+ from .pydantic_compat import V1RoutesBaseModel
28
+ from .utils import RequestBody, ResponseBody, add_errors_to_responses
29
+
30
+ DEFAULT_SPAN_LIMIT = 1000
31
+
32
+ router = APIRouter(tags=["spans"])
33
+
34
+
35
+ class SpanQuery(V1RoutesBaseModel):
36
+ select: Optional[dict[str, Any]] = None
37
+ filter: Optional[dict[str, Any]] = None
38
+ explode: Optional[dict[str, Any]] = None
39
+ concat: Optional[dict[str, Any]] = None
40
+ rename: Optional[dict[str, Any]] = None
41
+ index: Optional[dict[str, Any]] = None
42
+
43
+
44
+ class QuerySpansRequestBody(V1RoutesBaseModel):
45
+ queries: list[SpanQuery]
46
+ start_time: Optional[datetime] = None
47
+ end_time: Optional[datetime] = None
48
+ limit: int = DEFAULT_SPAN_LIMIT
49
+ root_spans_only: Optional[bool] = None
50
+ project_name: Optional[str] = Field(
51
+ default=None,
52
+ description=(
53
+ "The name of the project to query. "
54
+ "This parameter has been deprecated, use the project_name query parameter instead."
55
+ ),
56
+ deprecated=True,
57
+ )
58
+ stop_time: Optional[datetime] = Field(
59
+ default=None,
60
+ description=(
61
+ "An upper bound on the time to query for. "
62
+ "This parameter has been deprecated, use the end_time parameter instead."
63
+ ),
64
+ deprecated=True,
65
+ )
66
+
67
+
68
+ # TODO: Add property details to SpanQuery schema
69
+ @router.post(
70
+ "/spans",
71
+ operation_id="querySpans",
72
+ summary="Query spans with query DSL",
73
+ responses=add_errors_to_responses([HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY]),
74
+ include_in_schema=False,
75
+ )
76
+ async def query_spans_handler(
77
+ request: Request,
78
+ request_body: QuerySpansRequestBody,
79
+ accept: Optional[str] = Header(None),
80
+ project_name: Optional[str] = Query(
81
+ default=None, description="The project name to get evaluations from"
82
+ ),
83
+ ) -> Response:
84
+ queries = request_body.queries
85
+ project_name = (
86
+ project_name
87
+ or request.query_params.get("project-name") # for backward compatibility
88
+ or request.headers.get(
89
+ "project-name"
90
+ ) # read from headers/payload for backward-compatibility
91
+ or request_body.project_name
92
+ or DEFAULT_PROJECT_NAME
93
+ )
94
+ end_time = request_body.end_time or request_body.stop_time
95
+ try:
96
+ span_queries = [SpanQuery_.from_dict(query.dict()) for query in queries]
97
+ except Exception as e:
98
+ raise HTTPException(
99
+ detail=f"Invalid query: {e}",
100
+ status_code=HTTP_422_UNPROCESSABLE_ENTITY,
101
+ )
102
+ async with request.app.state.db() as session:
103
+ results = []
104
+ for query in span_queries:
105
+ results.append(
106
+ await session.run_sync(
107
+ query,
108
+ project_name=project_name,
109
+ start_time=normalize_datetime(
110
+ request_body.start_time,
111
+ timezone.utc,
112
+ ),
113
+ end_time=normalize_datetime(
114
+ end_time,
115
+ timezone.utc,
116
+ ),
117
+ limit=request_body.limit,
118
+ root_spans_only=request_body.root_spans_only,
119
+ )
120
+ )
121
+ if not results:
122
+ raise HTTPException(status_code=HTTP_404_NOT_FOUND)
123
+
124
+ if accept == "application/json":
125
+ boundary_token = token_urlsafe(64)
126
+ return StreamingResponse(
127
+ content=_json_multipart(results, boundary_token),
128
+ media_type=f"multipart/mixed; boundary={boundary_token}",
129
+ )
130
+
131
+ async def content() -> AsyncIterator[bytes]:
132
+ for result in results:
133
+ yield df_to_bytes(result)
134
+
135
+ return StreamingResponse(
136
+ content=content(),
137
+ media_type="application/x-pandas-arrow",
138
+ )
139
+
140
+
141
+ async def _json_multipart(
142
+ results: list[pd.DataFrame],
143
+ boundary_token: str,
144
+ ) -> AsyncIterator[str]:
145
+ for df in results:
146
+ yield f"--{boundary_token}\r\n"
147
+ yield "Content-Type: application/json\r\n\r\n"
148
+ yield await get_running_loop().run_in_executor(None, encode_df_as_json_string, df)
149
+ yield "\r\n"
150
+ yield f"--{boundary_token}--\r\n"
151
+
152
+
153
+ @router.get("/spans", include_in_schema=False, deprecated=True)
154
+ async def get_spans_handler(
155
+ request: Request,
156
+ request_body: QuerySpansRequestBody,
157
+ project_name: Optional[str] = Query(
158
+ default=None, description="The project name to get evaluations from"
159
+ ),
160
+ ) -> Response:
161
+ return await query_spans_handler(request, request_body, project_name)
162
+
163
+
164
+ class SpanAnnotationResult(V1RoutesBaseModel):
165
+ label: Optional[str] = Field(default=None, description="The label assigned by the annotation")
166
+ score: Optional[float] = Field(default=None, description="The score assigned by the annotation")
167
+ explanation: Optional[str] = Field(
168
+ default=None, description="Explanation of the annotation result"
169
+ )
170
+
171
+
172
+ class SpanAnnotation(V1RoutesBaseModel):
173
+ span_id: str = Field(description="OpenTelemetry Span ID (hex format w/o 0x prefix)")
174
+ name: str = Field(description="The name of the annotation")
175
+ annotator_kind: Literal["LLM", "HUMAN"] = Field(
176
+ description="The kind of annotator used for the annotation"
177
+ )
178
+ result: Optional[SpanAnnotationResult] = Field(
179
+ default=None, description="The result of the annotation"
180
+ )
181
+ metadata: Optional[dict[str, Any]] = Field(
182
+ default=None, description="Metadata for the annotation"
183
+ )
184
+
185
+ def as_precursor(self) -> Precursors.SpanAnnotation:
186
+ return Precursors.SpanAnnotation(
187
+ self.span_id,
188
+ models.SpanAnnotation(
189
+ name=self.name,
190
+ annotator_kind=self.annotator_kind,
191
+ score=self.result.score if self.result else None,
192
+ label=self.result.label if self.result else None,
193
+ explanation=self.result.explanation if self.result else None,
194
+ metadata_=self.metadata or {},
195
+ ),
196
+ )
197
+
198
+
199
+ class AnnotateSpansRequestBody(RequestBody[list[SpanAnnotation]]):
200
+ data: list[SpanAnnotation]
201
+
202
+
203
+ class InsertedSpanAnnotation(V1RoutesBaseModel):
204
+ id: str = Field(description="The ID of the inserted span annotation")
205
+
206
+
207
+ class AnnotateSpansResponseBody(ResponseBody[list[InsertedSpanAnnotation]]):
208
+ pass
209
+
210
+
211
+ @router.post(
212
+ "/span_annotations",
213
+ operation_id="annotateSpans",
214
+ summary="Create or update span annotations",
215
+ responses=add_errors_to_responses(
216
+ [{"status_code": HTTP_404_NOT_FOUND, "description": "Span not found"}]
217
+ ),
218
+ response_description="Span annotations inserted successfully",
219
+ include_in_schema=True,
220
+ )
221
+ async def annotate_spans(
222
+ request: Request,
223
+ request_body: AnnotateSpansRequestBody,
224
+ sync: bool = Query(default=False, description="If true, fulfill request synchronously."),
225
+ ) -> AnnotateSpansResponseBody:
226
+ if not request_body.data:
227
+ return AnnotateSpansResponseBody(data=[])
228
+ precursors = [d.as_precursor() for d in request_body.data]
229
+ if not sync:
230
+ await request.state.enqueue(*precursors)
231
+ return AnnotateSpansResponseBody(data=[])
232
+
233
+ span_ids = {p.span_id for p in precursors}
234
+ async with request.app.state.db() as session:
235
+ existing_spans = {
236
+ span.span_id: span.id
237
+ async for span in await session.stream_scalars(
238
+ select(models.Span).filter(models.Span.span_id.in_(span_ids))
239
+ )
240
+ }
241
+
242
+ missing_span_ids = span_ids - set(existing_spans.keys())
243
+ if missing_span_ids:
244
+ raise HTTPException(
245
+ detail=f"Spans with IDs {', '.join(missing_span_ids)} do not exist.",
246
+ status_code=HTTP_404_NOT_FOUND,
247
+ )
248
+ inserted_ids = []
249
+ dialect = SupportedSQLDialect(session.bind.dialect.name)
250
+ for p in precursors:
251
+ values = dict(as_kv(p.as_insertable(existing_spans[p.span_id]).row))
252
+ span_annotation_id = await session.scalar(
253
+ insert_on_conflict(
254
+ values,
255
+ dialect=dialect,
256
+ table=models.SpanAnnotation,
257
+ unique_by=("name", "span_rowid"),
258
+ ).returning(models.SpanAnnotation.id)
259
+ )
260
+ inserted_ids.append(span_annotation_id)
261
+ request.state.event_queue.put(SpanAnnotationInsertEvent(tuple(inserted_ids)))
262
+ return AnnotateSpansResponseBody(
263
+ data=[
264
+ InsertedSpanAnnotation(id=str(GlobalID("SpanAnnotation", str(id_))))
265
+ for id_ in inserted_ids
266
+ ]
267
+ )