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,54 +1,70 @@
1
1
  import gzip
2
2
  import zlib
3
- from typing import Any, Literal, Optional
3
+ from typing import Optional
4
4
 
5
- from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, Query
6
- from google.protobuf.json_format import MessageToJson
5
+ from fastapi import APIRouter, BackgroundTasks, Depends, Header, HTTPException, Path, Query
7
6
  from google.protobuf.message import DecodeError
8
7
  from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import (
9
8
  ExportTraceServiceRequest,
10
9
  ExportTraceServiceResponse,
11
10
  )
12
11
  from pydantic import Field
13
- from sqlalchemy import insert, select
12
+ from sqlalchemy import delete, select
14
13
  from starlette.concurrency import run_in_threadpool
15
14
  from starlette.datastructures import State
16
15
  from starlette.requests import Request
17
- from starlette.responses import JSONResponse
18
- from starlette.status import (
19
- HTTP_404_NOT_FOUND,
20
- HTTP_415_UNSUPPORTED_MEDIA_TYPE,
21
- HTTP_422_UNPROCESSABLE_ENTITY,
22
- )
16
+ from starlette.responses import Response
23
17
  from strawberry.relay import GlobalID
24
18
 
25
19
  from phoenix.db import models
26
- from phoenix.db.insertion.helpers import as_kv
27
- from phoenix.db.insertion.types import Precursors
20
+ from phoenix.db.helpers import SupportedSQLDialect
21
+ from phoenix.db.insertion.helpers import as_kv, insert_on_conflict
22
+ from phoenix.server.api.routers.v1.annotations import TraceAnnotationData
23
+ from phoenix.server.api.types.node import from_global_id_with_expected_type
24
+ from phoenix.server.authorization import is_not_locked
28
25
  from phoenix.server.bearer_auth import PhoenixUser
29
- from phoenix.server.dml_event import TraceAnnotationInsertEvent
26
+ from phoenix.server.dml_event import SpanDeleteEvent, TraceAnnotationInsertEvent
27
+ from phoenix.server.prometheus import SPAN_QUEUE_REJECTIONS
30
28
  from phoenix.trace.otel import decode_otlp_span
31
29
  from phoenix.utilities.project import get_project_name
32
30
 
33
31
  from .models import V1RoutesBaseModel
34
- from .utils import RequestBody, ResponseBody, add_errors_to_responses
32
+ from .utils import (
33
+ RequestBody,
34
+ ResponseBody,
35
+ add_errors_to_responses,
36
+ )
35
37
 
36
38
  router = APIRouter(tags=["traces"])
37
39
 
38
40
 
41
+ def is_not_at_capacity(request: Request) -> None:
42
+ if request.app.state.span_queue_is_full():
43
+ SPAN_QUEUE_REJECTIONS.inc()
44
+ raise HTTPException(
45
+ detail="Server is at capacity and cannot process more requests",
46
+ status_code=503,
47
+ )
48
+
49
+
39
50
  @router.post(
40
51
  "/traces",
52
+ dependencies=[Depends(is_not_locked), Depends(is_not_at_capacity)],
41
53
  operation_id="addTraces",
42
54
  summary="Send traces",
43
55
  responses=add_errors_to_responses(
44
56
  [
45
57
  {
46
- "status_code": HTTP_415_UNSUPPORTED_MEDIA_TYPE,
58
+ "status_code": 415,
47
59
  "description": (
48
60
  "Unsupported content type (only `application/x-protobuf` is supported)"
49
61
  ),
50
62
  },
51
- {"status_code": HTTP_422_UNPROCESSABLE_ENTITY, "description": "Invalid request body"},
63
+ {"status_code": 422, "description": "Invalid request body"},
64
+ {
65
+ "status_code": 503,
66
+ "description": "Server is at capacity and cannot process more requests",
67
+ },
52
68
  ]
53
69
  ),
54
70
  openapi_extra={
@@ -66,16 +82,16 @@ async def post_traces(
66
82
  background_tasks: BackgroundTasks,
67
83
  content_type: Optional[str] = Header(default=None),
68
84
  content_encoding: Optional[str] = Header(default=None),
69
- ) -> JSONResponse:
85
+ ) -> Response:
70
86
  if content_type != "application/x-protobuf":
71
87
  raise HTTPException(
72
88
  detail=f"Unsupported content type: {content_type}",
73
- status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE,
89
+ status_code=415,
74
90
  )
75
91
  if content_encoding and content_encoding not in ("gzip", "deflate"):
76
92
  raise HTTPException(
77
93
  detail=f"Unsupported content encoding: {content_encoding}",
78
- status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE,
94
+ status_code=415,
79
95
  )
80
96
  body = await request.body()
81
97
  if content_encoding == "gzip":
@@ -88,59 +104,22 @@ async def post_traces(
88
104
  except DecodeError:
89
105
  raise HTTPException(
90
106
  detail="Request body is invalid ExportTraceServiceRequest",
91
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
107
+ status_code=422,
92
108
  )
93
109
  background_tasks.add_task(_add_spans, req, request.state)
94
- return JSONResponse(MessageToJson(ExportTraceServiceResponse()))
95
110
 
96
-
97
- class TraceAnnotationResult(V1RoutesBaseModel):
98
- label: Optional[str] = Field(default=None, description="The label assigned by the annotation")
99
- score: Optional[float] = Field(default=None, description="The score assigned by the annotation")
100
- explanation: Optional[str] = Field(
101
- default=None, description="Explanation of the annotation result"
111
+ # "The server MUST use the same Content-Type in the response as it received in the request"
112
+ response_message = ExportTraceServiceResponse()
113
+ response_bytes = response_message.SerializeToString()
114
+ return Response(
115
+ content=response_bytes,
116
+ media_type="application/x-protobuf",
117
+ status_code=200,
102
118
  )
103
119
 
104
120
 
105
- class TraceAnnotation(V1RoutesBaseModel):
106
- trace_id: str = Field(description="OpenTelemetry Trace ID (hex format w/o 0x prefix)")
107
- name: str = Field(description="The name of the annotation")
108
- annotator_kind: Literal["LLM", "HUMAN"] = Field(
109
- description="The kind of annotator used for the annotation"
110
- )
111
- result: Optional[TraceAnnotationResult] = Field(
112
- default=None, description="The result of the annotation"
113
- )
114
- metadata: Optional[dict[str, Any]] = Field(
115
- default=None, description="Metadata for the annotation"
116
- )
117
- identifier: str = Field(
118
- default="",
119
- description=(
120
- "The identifier of the annotation. "
121
- "If provided, the annotation will be updated if it already exists."
122
- ),
123
- )
124
-
125
- def as_precursor(self, *, user_id: Optional[int] = None) -> Precursors.TraceAnnotation:
126
- return Precursors.TraceAnnotation(
127
- self.trace_id,
128
- models.TraceAnnotation(
129
- name=self.name,
130
- annotator_kind=self.annotator_kind,
131
- score=self.result.score if self.result else None,
132
- label=self.result.label if self.result else None,
133
- explanation=self.result.explanation if self.result else None,
134
- metadata_=self.metadata or {},
135
- identifier=self.identifier,
136
- source="APP",
137
- user_id=user_id,
138
- ),
139
- )
140
-
141
-
142
- class AnnotateTracesRequestBody(RequestBody[list[TraceAnnotation]]):
143
- data: list[TraceAnnotation] = Field(description="The trace annotations to be upserted")
121
+ class AnnotateTracesRequestBody(RequestBody[list[TraceAnnotationData]]):
122
+ data: list[TraceAnnotationData] = Field(description="The trace annotations to be upserted")
144
123
 
145
124
 
146
125
  class InsertedTraceAnnotation(V1RoutesBaseModel):
@@ -153,17 +132,15 @@ class AnnotateTracesResponseBody(ResponseBody[list[InsertedTraceAnnotation]]):
153
132
 
154
133
  @router.post(
155
134
  "/trace_annotations",
135
+ dependencies=[Depends(is_not_locked)],
156
136
  operation_id="annotateTraces",
157
137
  summary="Create trace annotations",
158
- responses=add_errors_to_responses(
159
- [{"status_code": HTTP_404_NOT_FOUND, "description": "Trace not found"}]
160
- ),
161
- include_in_schema=False,
138
+ responses=add_errors_to_responses([{"status_code": 404, "description": "Trace not found"}]),
162
139
  )
163
140
  async def annotate_traces(
164
141
  request: Request,
165
142
  request_body: AnnotateTracesRequestBody,
166
- sync: bool = Query(default=True, description="If true, fulfill request synchronously."),
143
+ sync: bool = Query(default=False, description="If true, fulfill request synchronously."),
167
144
  ) -> AnnotateTracesResponseBody:
168
145
  if not request_body.data:
169
146
  return AnnotateTracesResponseBody(data=[])
@@ -174,15 +151,17 @@ async def annotate_traces(
174
151
 
175
152
  precursors = [d.as_precursor(user_id=user_id) for d in request_body.data]
176
153
  if not sync:
177
- await request.state.enqueue(*precursors)
154
+ await request.state.enqueue_annotations(*precursors)
178
155
  return AnnotateTracesResponseBody(data=[])
179
156
 
180
157
  trace_ids = {p.trace_id for p in precursors}
181
158
  async with request.app.state.db() as session:
182
159
  existing_traces = {
183
- trace.trace_id: trace.id
184
- async for trace in await session.stream_scalars(
185
- select(models.Trace).filter(models.Trace.trace_id.in_(trace_ids))
160
+ trace_id: id_
161
+ async for trace_id, id_ in await session.stream(
162
+ select(models.Trace.trace_id, models.Trace.id).filter(
163
+ models.Trace.trace_id.in_(trace_ids)
164
+ )
186
165
  )
187
166
  }
188
167
 
@@ -190,13 +169,19 @@ async def annotate_traces(
190
169
  if missing_trace_ids:
191
170
  raise HTTPException(
192
171
  detail=f"Traces with IDs {', '.join(missing_trace_ids)} do not exist.",
193
- status_code=HTTP_404_NOT_FOUND,
172
+ status_code=404,
194
173
  )
195
174
  inserted_ids = []
175
+ dialect = SupportedSQLDialect(session.bind.dialect.name)
196
176
  for p in precursors:
197
177
  values = dict(as_kv(p.as_insertable(existing_traces[p.trace_id]).row))
198
178
  trace_annotation_id = await session.scalar(
199
- insert(models.TraceAnnotation).values(**values).returning(models.TraceAnnotation.id)
179
+ insert_on_conflict(
180
+ values,
181
+ dialect=dialect,
182
+ table=models.TraceAnnotation,
183
+ unique_by=("name", "trace_rowid", "identifier"),
184
+ ).returning(models.TraceAnnotation.id)
200
185
  )
201
186
  inserted_ids.append(trace_annotation_id)
202
187
  request.state.event_queue.put(TraceAnnotationInsertEvent(tuple(inserted_ids)))
@@ -214,4 +199,73 @@ async def _add_spans(req: ExportTraceServiceRequest, state: State) -> None:
214
199
  for scope_span in resource_spans.scope_spans:
215
200
  for otlp_span in scope_span.spans:
216
201
  span = await run_in_threadpool(decode_otlp_span, otlp_span)
217
- await state.queue_span_for_bulk_insert(span, project_name)
202
+ await state.enqueue_span(span, project_name)
203
+
204
+
205
+ @router.delete(
206
+ "/traces/{trace_identifier}",
207
+ operation_id="deleteTrace",
208
+ summary="Delete a trace by identifier",
209
+ description=(
210
+ "Delete an entire trace by its identifier. The identifier can be either:\n"
211
+ "1. A Relay node ID (base64-encoded)\n"
212
+ "2. An OpenTelemetry trace_id (hex string)\n\n"
213
+ "This will permanently remove all spans in the trace and their associated data."
214
+ ),
215
+ responses=add_errors_to_responses([404]),
216
+ status_code=204, # No Content for successful deletion
217
+ )
218
+ async def delete_trace(
219
+ request: Request,
220
+ trace_identifier: str = Path(
221
+ description="The trace identifier: either a relay GlobalID or OpenTelemetry trace_id"
222
+ ),
223
+ ) -> None:
224
+ """
225
+ Delete a trace by identifier (relay GlobalID or OpenTelemetry trace_id).
226
+
227
+ This endpoint will:
228
+ 1. Delete the trace by identifier (relay GlobalID or OpenTelemetry trace_id)
229
+ 2. Get project_id from the deletion for cache invalidation
230
+ 3. Trigger cache invalidation events
231
+ 4. Return 204 No Content on success
232
+
233
+ Note: This deletes the entire trace, including all spans, which maintains data consistency
234
+ and avoids orphaned spans or inconsistent cached cumulative fields.
235
+ """
236
+ async with request.app.state.db() as session:
237
+ # Try to parse as GlobalID first, then fall back to trace_id
238
+ try:
239
+ trace_rowid = from_global_id_with_expected_type(
240
+ GlobalID.from_id(trace_identifier),
241
+ "Trace",
242
+ )
243
+ # Delete by database rowid
244
+ delete_stmt = (
245
+ delete(models.Trace)
246
+ .where(models.Trace.id == trace_rowid)
247
+ .returning(models.Trace.project_rowid)
248
+ )
249
+ error_detail = f"Trace with relay ID '{trace_identifier}' not found"
250
+ except Exception:
251
+ # Delete by OpenTelemetry trace_id
252
+ delete_stmt = (
253
+ delete(models.Trace)
254
+ .where(models.Trace.trace_id == trace_identifier)
255
+ .returning(models.Trace.project_rowid)
256
+ )
257
+ error_detail = f"Trace with trace_id '{trace_identifier}' not found"
258
+
259
+ project_id = await session.scalar(delete_stmt)
260
+
261
+ if project_id is None:
262
+ raise HTTPException(
263
+ status_code=404,
264
+ detail=error_detail,
265
+ )
266
+
267
+ # Trigger cache invalidation event
268
+ request.state.event_queue.put(SpanDeleteEvent((project_id,)))
269
+
270
+ # Return 204 No Content (successful deletion with no response body)
271
+ return None