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
phoenix/db/models.py CHANGED
@@ -1,12 +1,16 @@
1
+ import re
1
2
  from datetime import datetime, timezone
2
3
  from typing import Any, Iterable, Literal, Optional, Sequence, TypedDict, cast
3
4
 
5
+ import orjson
6
+ import sqlalchemy as sa
4
7
  import sqlalchemy.sql as sql
5
8
  from openinference.semconv.trace import RerankerAttributes, SpanAttributes
6
9
  from sqlalchemy import (
7
10
  JSON,
8
11
  NUMERIC,
9
12
  TIMESTAMP,
13
+ Boolean,
10
14
  CheckConstraint,
11
15
  ColumnElement,
12
16
  Dialect,
@@ -16,6 +20,7 @@ from sqlalchemy import (
16
20
  Integer,
17
21
  MetaData,
18
22
  Null,
23
+ PrimaryKeyConstraint,
19
24
  String,
20
25
  TypeDecorator,
21
26
  UniqueConstraint,
@@ -39,6 +44,7 @@ from sqlalchemy.orm import (
39
44
  )
40
45
  from sqlalchemy.sql import Values, column, compiler, expression, literal, roles, union_all
41
46
  from sqlalchemy.sql.compiler import SQLCompiler
47
+ from sqlalchemy.sql.elements import Case
42
48
  from sqlalchemy.sql.functions import coalesce
43
49
  from typing_extensions import TypeAlias
44
50
 
@@ -52,6 +58,10 @@ from phoenix.db.types.annotation_configs import (
52
58
  )
53
59
  from phoenix.db.types.identifier import Identifier
54
60
  from phoenix.db.types.model_provider import ModelProvider
61
+ from phoenix.db.types.token_price_customization import (
62
+ TokenPriceCustomization,
63
+ TokenPriceCustomizationParser,
64
+ )
55
65
  from phoenix.db.types.trace_retention import TraceRetentionCronExpression, TraceRetentionRule
56
66
  from phoenix.server.api.helpers.prompts.models import (
57
67
  PromptInvocationParameters,
@@ -146,6 +156,7 @@ def render_values_w_union(
146
156
  return compiler.process(subquery, from_linter=from_linter, **kw)
147
157
 
148
158
 
159
+ UserRoleName: TypeAlias = Literal["SYSTEM", "ADMIN", "MEMBER", "VIEWER"]
149
160
  AuthMethod: TypeAlias = Literal["LOCAL", "OAUTH2"]
150
161
 
151
162
 
@@ -181,6 +192,9 @@ class JsonDict(TypeDecorator[dict[str, Any]]):
181
192
  def process_bind_param(self, value: Optional[dict[str, Any]], _: Dialect) -> dict[str, Any]:
182
193
  return value if isinstance(value, dict) else {}
183
194
 
195
+ def process_result_value(self, value: Optional[Any], _: Dialect) -> Optional[dict[str, Any]]:
196
+ return orjson.loads(orjson.dumps(value)) if isinstance(value, dict) and value else value
197
+
184
198
 
185
199
  class JsonList(TypeDecorator[list[Any]]):
186
200
  # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
@@ -190,6 +204,9 @@ class JsonList(TypeDecorator[list[Any]]):
190
204
  def process_bind_param(self, value: Optional[list[Any]], _: Dialect) -> list[Any]:
191
205
  return value if isinstance(value, list) else []
192
206
 
207
+ def process_result_value(self, value: Optional[Any], _: Dialect) -> Optional[list[Any]]:
208
+ return orjson.loads(orjson.dumps(value)) if isinstance(value, list) and value else value
209
+
193
210
 
194
211
  class UtcTimeStamp(TypeDecorator[datetime]):
195
212
  # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
@@ -390,12 +407,69 @@ class _AnnotationConfig(TypeDecorator[AnnotationConfigType]):
390
407
  return AnnotationConfigModel.model_validate(value).root if value is not None else None
391
408
 
392
409
 
410
+ class _TokenCustomization(TypeDecorator[TokenPriceCustomization]):
411
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
412
+ cache_ok = True
413
+ impl = JSON
414
+
415
+ def process_bind_param(
416
+ self, value: Optional[TokenPriceCustomization], _: Dialect
417
+ ) -> Optional[dict[str, Any]]:
418
+ return value.model_dump() if value is not None else None
419
+
420
+ def process_result_value(
421
+ self, value: Optional[dict[str, Any]], _: Dialect
422
+ ) -> Optional[TokenPriceCustomization]:
423
+ return TokenPriceCustomizationParser.parse(value)
424
+
425
+
426
+ class _RegexStr(TypeDecorator[re.Pattern[str]]):
427
+ # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
428
+ cache_ok = True
429
+ impl = String
430
+
431
+ def process_bind_param(self, value: Optional[re.Pattern[str]], _: Dialect) -> Optional[str]:
432
+ if value is None:
433
+ return None
434
+ if not isinstance(value, re.Pattern):
435
+ raise TypeError(f"Expected a regex pattern, got {type(value)}")
436
+ pattern = value.pattern
437
+ if not isinstance(pattern, str):
438
+ raise ValueError(f"Expected a string, got {type(pattern)}")
439
+ return pattern
440
+
441
+ def process_result_value(self, value: Optional[str], _: Dialect) -> Optional[re.Pattern[str]]:
442
+ if value is None:
443
+ return None
444
+ return re.compile(value)
445
+
446
+
447
+ _HEX_COLOR_PATTERN = re.compile(r"^#([0-9a-f]{6})$")
448
+
449
+
450
+ class _HexColor(TypeDecorator[str]):
451
+ # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
452
+ cache_ok = True
453
+ impl = String
454
+
455
+ def process_bind_param(self, value: Optional[str], _: Dialect) -> Optional[str]:
456
+ if value is None:
457
+ return None
458
+ if not _HEX_COLOR_PATTERN.match(value):
459
+ raise ValueError(f"Expected a hex color, got {value}")
460
+ return value
461
+
462
+ def process_result_value(self, value: Optional[str], _: Dialect) -> Optional[str]:
463
+ if value is None:
464
+ return None
465
+ return value
466
+
467
+
393
468
  class ExperimentRunOutput(TypedDict, total=False):
394
469
  task_output: Any
395
470
 
396
471
 
397
472
  class Base(DeclarativeBase):
398
- id: Mapped[int] = mapped_column(Integer, primary_key=True)
399
473
  # Enforce best practices for naming constraints
400
474
  # https://alembic.sqlalchemy.org/en/latest/naming.html#integration-of-naming-conventions-into-operations-autogenerate
401
475
  metadata = MetaData(
@@ -415,9 +489,13 @@ class Base(DeclarativeBase):
415
489
  }
416
490
 
417
491
 
418
- class ProjectTraceRetentionPolicy(Base):
492
+ class HasId(Base):
493
+ __abstract__ = True
494
+ id: Mapped[int] = mapped_column(primary_key=True)
495
+
496
+
497
+ class ProjectTraceRetentionPolicy(HasId):
419
498
  __tablename__ = "project_trace_retention_policies"
420
- id: Mapped[int] = mapped_column(Integer, primary_key=True)
421
499
  name: Mapped[str] = mapped_column(String, nullable=False)
422
500
  cron_expression: Mapped[TraceRetentionCronExpression] = mapped_column(
423
501
  _TraceRetentionCronExpression, nullable=False
@@ -428,7 +506,7 @@ class ProjectTraceRetentionPolicy(Base):
428
506
  )
429
507
 
430
508
 
431
- class Project(Base):
509
+ class Project(HasId):
432
510
  __tablename__ = "projects"
433
511
  name: Mapped[str]
434
512
  description: Mapped[Optional[str]]
@@ -468,7 +546,7 @@ class Project(Base):
468
546
  )
469
547
 
470
548
 
471
- class ProjectSession(Base):
549
+ class ProjectSession(HasId):
472
550
  __tablename__ = "project_sessions"
473
551
  session_id: Mapped[str] = mapped_column(String, nullable=False, unique=True)
474
552
  project_id: Mapped[int] = mapped_column(
@@ -485,7 +563,7 @@ class ProjectSession(Base):
485
563
  )
486
564
 
487
565
 
488
- class Trace(Base):
566
+ class Trace(HasId):
489
567
  __tablename__ = "traces"
490
568
  project_rowid: Mapped[int] = mapped_column(
491
569
  ForeignKey("projects.id", ondelete="CASCADE"),
@@ -527,6 +605,12 @@ class Trace(Base):
527
605
  primaryjoin="foreign(ExperimentRun.trace_id) == Trace.trace_id",
528
606
  back_populates="trace",
529
607
  )
608
+ span_costs: Mapped[list["SpanCost"]] = relationship(
609
+ "SpanCost",
610
+ back_populates="trace",
611
+ cascade="all, delete-orphan",
612
+ uselist=True,
613
+ )
530
614
  __table_args__ = (
531
615
  UniqueConstraint(
532
616
  "trace_id",
@@ -534,13 +618,13 @@ class Trace(Base):
534
618
  )
535
619
 
536
620
 
537
- class Span(Base):
621
+ class Span(HasId):
538
622
  __tablename__ = "spans"
539
623
  trace_rowid: Mapped[int] = mapped_column(
540
624
  ForeignKey("traces.id", ondelete="CASCADE"),
541
625
  index=True,
542
626
  )
543
- span_id: Mapped[str] = mapped_column(index=True)
627
+ span_id: Mapped[str]
544
628
  parent_id: Mapped[Optional[str]] = mapped_column(index=True)
545
629
  name: Mapped[str]
546
630
  span_kind: Mapped[str]
@@ -684,6 +768,7 @@ class Span(Base):
684
768
  span_annotations: Mapped[list["SpanAnnotation"]] = relationship(back_populates="span")
685
769
  document_annotations: Mapped[list["DocumentAnnotation"]] = relationship(back_populates="span")
686
770
  dataset_examples: Mapped[list["DatasetExample"]] = relationship(back_populates="span")
771
+ span_cost: Mapped[Optional["SpanCost"]] = relationship(back_populates="span")
687
772
 
688
773
  __table_args__ = (
689
774
  UniqueConstraint(
@@ -745,14 +830,27 @@ class NumDocuments(expression.FunctionElement[int]):
745
830
  @compiles(NumDocuments)
746
831
  def _(element: Any, compiler: SQLCompiler, **kw: Any) -> Any:
747
832
  # See https://docs.sqlalchemy.org/en/20/core/compiler.html
748
- array_length = (
749
- func.json_array_length if isinstance(compiler, SQLiteCompiler) else func.jsonb_array_length
750
- )
751
833
  attributes, span_kind = list(element.clauses)
752
834
  retrieval_docs = attributes[RETRIEVAL_DOCUMENTS]
753
- num_retrieval_docs = coalesce(array_length(retrieval_docs), 0)
835
+ num_retrieval_docs: Case[Any] | coalesce[Any]
754
836
  reranker_docs = attributes[RERANKER_OUTPUT_DOCUMENTS]
755
- num_reranker_docs = coalesce(array_length(reranker_docs), 0)
837
+ num_reranker_docs: Case[Any] | coalesce[Any]
838
+ if isinstance(compiler, SQLiteCompiler):
839
+ # SQLite's json_array_length returns 0 for non-array values
840
+ num_retrieval_docs = coalesce(func.json_array_length(retrieval_docs), 0)
841
+ num_reranker_docs = coalesce(func.json_array_length(reranker_docs), 0)
842
+ else:
843
+ # PostgreSQL's jsonb_array_length throws "cannot get array length of a scalar"
844
+ # for non-array values, so check the type first
845
+ num_retrieval_docs = sql.case(
846
+ (func.jsonb_typeof(retrieval_docs) == "array", func.jsonb_array_length(retrieval_docs)),
847
+ else_=0,
848
+ )
849
+ num_reranker_docs = sql.case(
850
+ (func.jsonb_typeof(reranker_docs) == "array", func.jsonb_array_length(reranker_docs)),
851
+ else_=0,
852
+ )
853
+
756
854
  return compiler.process(
757
855
  sql.case(
758
856
  (func.upper(span_kind) == "RERANKER", num_reranker_docs),
@@ -790,6 +888,41 @@ def _(element: Any, compiler: Any, **kw: Any) -> Any:
790
888
  return compiler.process(func.text_contains(string, substring) > 0, **kw)
791
889
 
792
890
 
891
+ class CaseInsensitiveContains(expression.FunctionElement[bool]):
892
+ # See https://docs.sqlalchemy.org/en/20/core/compiler.html
893
+ inherit_cache = True
894
+ type = Boolean()
895
+ name = "case_insensitive_contains"
896
+
897
+
898
+ @compiles(CaseInsensitiveContains)
899
+ def _(element: Any, compiler: Any, **kw: Any) -> Any:
900
+ string, substring = list(element.clauses)
901
+ result = compiler.process(func.lower(string).contains(func.lower(substring)), **kw)
902
+ return result
903
+
904
+
905
+ @compiles(CaseInsensitiveContains, "postgresql")
906
+ def _(element: Any, compiler: Any, **kw: Any) -> Any:
907
+ string, substring = list(element.clauses)
908
+ escaped = func.replace(
909
+ func.replace(func.replace(substring, "\\", "\\\\"), "%", "\\%"), "_", "\\_"
910
+ )
911
+ pattern = func.concat("%", escaped, "%")
912
+ result = compiler.process(string.ilike(pattern), **kw)
913
+ return result
914
+
915
+
916
+ @compiles(CaseInsensitiveContains, "sqlite")
917
+ def _(element: Any, compiler: Any, **kw: Any) -> Any:
918
+ # Use sqlean's `text_lower` to handle non-ASCII characters
919
+ string, substring = list(element.clauses)
920
+ result = compiler.process(
921
+ func.text_contains(func.text_lower(string), func.text_lower(substring)), **kw
922
+ )
923
+ return result
924
+
925
+
793
926
  async def init_models(engine: AsyncEngine) -> None:
794
927
  async with engine.begin() as conn:
795
928
  await conn.run_sync(Base.metadata.create_all)
@@ -801,15 +934,15 @@ async def init_models(engine: AsyncEngine) -> None:
801
934
  )
802
935
 
803
936
 
804
- class SpanAnnotation(Base):
937
+ class SpanAnnotation(HasId):
805
938
  __tablename__ = "span_annotations"
806
939
  span_rowid: Mapped[int] = mapped_column(
807
940
  ForeignKey("spans.id", ondelete="CASCADE"),
808
941
  index=True,
809
942
  )
810
943
  name: Mapped[str]
811
- label: Mapped[Optional[str]] = mapped_column(String, index=True)
812
- score: Mapped[Optional[float]] = mapped_column(Float, index=True)
944
+ label: Mapped[Optional[str]]
945
+ score: Mapped[Optional[float]]
813
946
  explanation: Mapped[Optional[str]]
814
947
  metadata_: Mapped[dict[str, Any]] = mapped_column("metadata")
815
948
  annotator_kind: Mapped[Literal["LLM", "CODE", "HUMAN"]] = mapped_column(
@@ -841,15 +974,15 @@ class SpanAnnotation(Base):
841
974
  )
842
975
 
843
976
 
844
- class TraceAnnotation(Base):
977
+ class TraceAnnotation(HasId):
845
978
  __tablename__ = "trace_annotations"
846
979
  trace_rowid: Mapped[int] = mapped_column(
847
980
  ForeignKey("traces.id", ondelete="CASCADE"),
848
981
  index=True,
849
982
  )
850
983
  name: Mapped[str]
851
- label: Mapped[Optional[str]] = mapped_column(String, index=True)
852
- score: Mapped[Optional[float]] = mapped_column(Float, index=True)
984
+ label: Mapped[Optional[str]]
985
+ score: Mapped[Optional[float]]
853
986
  explanation: Mapped[Optional[str]]
854
987
  metadata_: Mapped[dict[str, Any]] = mapped_column("metadata")
855
988
  annotator_kind: Mapped[Literal["LLM", "CODE", "HUMAN"]] = mapped_column(
@@ -878,7 +1011,7 @@ class TraceAnnotation(Base):
878
1011
  )
879
1012
 
880
1013
 
881
- class DocumentAnnotation(Base):
1014
+ class DocumentAnnotation(HasId):
882
1015
  __tablename__ = "document_annotations"
883
1016
  span_rowid: Mapped[int] = mapped_column(
884
1017
  ForeignKey("spans.id", ondelete="CASCADE"),
@@ -886,8 +1019,8 @@ class DocumentAnnotation(Base):
886
1019
  )
887
1020
  document_position: Mapped[int]
888
1021
  name: Mapped[str]
889
- label: Mapped[Optional[str]] = mapped_column(String, index=True)
890
- score: Mapped[Optional[float]] = mapped_column(Float, index=True)
1022
+ label: Mapped[Optional[str]]
1023
+ score: Mapped[Optional[float]]
891
1024
  explanation: Mapped[Optional[str]]
892
1025
  metadata_: Mapped[dict[str, Any]] = mapped_column("metadata")
893
1026
  annotator_kind: Mapped[Literal["LLM", "CODE", "HUMAN"]] = mapped_column(
@@ -919,7 +1052,44 @@ class DocumentAnnotation(Base):
919
1052
  )
920
1053
 
921
1054
 
922
- class Dataset(Base):
1055
+ class ProjectSessionAnnotation(HasId):
1056
+ __tablename__ = "project_session_annotations"
1057
+ project_session_id: Mapped[int] = mapped_column(
1058
+ ForeignKey("project_sessions.id", ondelete="CASCADE"),
1059
+ index=True,
1060
+ )
1061
+ name: Mapped[str]
1062
+ label: Mapped[Optional[str]]
1063
+ score: Mapped[Optional[float]]
1064
+ explanation: Mapped[Optional[str]]
1065
+ metadata_: Mapped[dict[str, Any]] = mapped_column("metadata")
1066
+ annotator_kind: Mapped[Literal["LLM", "CODE", "HUMAN"]] = mapped_column(
1067
+ CheckConstraint("annotator_kind IN ('LLM', 'CODE', 'HUMAN')", name="valid_annotator_kind"),
1068
+ )
1069
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
1070
+ updated_at: Mapped[datetime] = mapped_column(
1071
+ UtcTimeStamp, server_default=func.now(), onupdate=func.now()
1072
+ )
1073
+ identifier: Mapped[str] = mapped_column(
1074
+ String,
1075
+ server_default="",
1076
+ nullable=False,
1077
+ )
1078
+ source: Mapped[Literal["API", "APP"]] = mapped_column(
1079
+ CheckConstraint("source IN ('API', 'APP')", name="valid_source"),
1080
+ )
1081
+ user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
1082
+
1083
+ __table_args__ = (
1084
+ UniqueConstraint(
1085
+ "name",
1086
+ "project_session_id",
1087
+ "identifier",
1088
+ ),
1089
+ )
1090
+
1091
+
1092
+ class Dataset(HasId):
923
1093
  __tablename__ = "datasets"
924
1094
  name: Mapped[str] = mapped_column(unique=True)
925
1095
  description: Mapped[Optional[str]]
@@ -928,6 +1098,14 @@ class Dataset(Base):
928
1098
  updated_at: Mapped[datetime] = mapped_column(
929
1099
  UtcTimeStamp, server_default=func.now(), onupdate=func.now()
930
1100
  )
1101
+ user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
1102
+ user: Mapped[Optional["User"]] = relationship("User")
1103
+ experiment_tags: Mapped[list["ExperimentTag"]] = relationship(
1104
+ "ExperimentTag", back_populates="dataset"
1105
+ )
1106
+ datasets_dataset_labels: Mapped[list["DatasetsDatasetLabel"]] = relationship(
1107
+ "DatasetsDatasetLabel", back_populates="dataset"
1108
+ )
931
1109
 
932
1110
  @hybrid_property
933
1111
  def example_count(self) -> Optional[int]:
@@ -978,7 +1156,45 @@ class Dataset(Base):
978
1156
  )
979
1157
 
980
1158
 
981
- class DatasetVersion(Base):
1159
+ class DatasetLabel(HasId):
1160
+ __tablename__ = "dataset_labels"
1161
+ name: Mapped[str] = mapped_column(unique=True)
1162
+ description: Mapped[Optional[str]]
1163
+ color: Mapped[str] = mapped_column(_HexColor, nullable=False)
1164
+ datasets_dataset_labels: Mapped[list["DatasetsDatasetLabel"]] = relationship(
1165
+ "DatasetsDatasetLabel", back_populates="dataset_label"
1166
+ )
1167
+ user_id: Mapped[Optional[int]] = mapped_column(
1168
+ ForeignKey("users.id", ondelete="SET NULL"),
1169
+ nullable=True,
1170
+ )
1171
+ user: Mapped[Optional["User"]] = relationship("User")
1172
+
1173
+
1174
+ class DatasetsDatasetLabel(Base):
1175
+ __tablename__ = "datasets_dataset_labels"
1176
+ dataset_id: Mapped[int] = mapped_column(
1177
+ ForeignKey("datasets.id", ondelete="CASCADE"),
1178
+ )
1179
+ dataset_label_id: Mapped[int] = mapped_column(
1180
+ ForeignKey("dataset_labels.id", ondelete="CASCADE"),
1181
+ # index on the second element of the composite primary key
1182
+ index=True,
1183
+ )
1184
+ dataset: Mapped["Dataset"] = relationship("Dataset", back_populates="datasets_dataset_labels")
1185
+ dataset_label: Mapped["DatasetLabel"] = relationship(
1186
+ "DatasetLabel", back_populates="datasets_dataset_labels"
1187
+ )
1188
+
1189
+ __table_args__ = (
1190
+ PrimaryKeyConstraint(
1191
+ "dataset_id",
1192
+ "dataset_label_id",
1193
+ ),
1194
+ )
1195
+
1196
+
1197
+ class DatasetVersion(HasId):
982
1198
  __tablename__ = "dataset_versions"
983
1199
  dataset_id: Mapped[int] = mapped_column(
984
1200
  ForeignKey("datasets.id", ondelete="CASCADE"),
@@ -987,9 +1203,11 @@ class DatasetVersion(Base):
987
1203
  description: Mapped[Optional[str]]
988
1204
  metadata_: Mapped[dict[str, Any]] = mapped_column("metadata")
989
1205
  created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
1206
+ user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
1207
+ user: Mapped[Optional["User"]] = relationship("User")
990
1208
 
991
1209
 
992
- class DatasetExample(Base):
1210
+ class DatasetExample(HasId):
993
1211
  __tablename__ = "dataset_examples"
994
1212
  dataset_id: Mapped[int] = mapped_column(
995
1213
  ForeignKey("datasets.id", ondelete="CASCADE"),
@@ -1003,13 +1221,20 @@ class DatasetExample(Base):
1003
1221
  created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
1004
1222
 
1005
1223
  span: Mapped[Optional[Span]] = relationship(back_populates="dataset_examples")
1224
+ dataset_splits_dataset_examples: Mapped[list["DatasetSplitDatasetExample"]] = relationship(
1225
+ "DatasetSplitDatasetExample",
1226
+ back_populates="dataset_example",
1227
+ )
1228
+ experiment_dataset_examples: Mapped[list["ExperimentDatasetExample"]] = relationship(
1229
+ "ExperimentDatasetExample",
1230
+ back_populates="dataset_example",
1231
+ )
1006
1232
 
1007
1233
 
1008
- class DatasetExampleRevision(Base):
1234
+ class DatasetExampleRevision(HasId):
1009
1235
  __tablename__ = "dataset_example_revisions"
1010
1236
  dataset_example_id: Mapped[int] = mapped_column(
1011
1237
  ForeignKey("dataset_examples.id", ondelete="CASCADE"),
1012
- index=True,
1013
1238
  )
1014
1239
  dataset_version_id: Mapped[int] = mapped_column(
1015
1240
  ForeignKey("dataset_versions.id", ondelete="CASCADE"),
@@ -1025,6 +1250,11 @@ class DatasetExampleRevision(Base):
1025
1250
  )
1026
1251
  created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
1027
1252
 
1253
+ experiment_dataset_examples: Mapped[list["ExperimentDatasetExample"]] = relationship(
1254
+ "ExperimentDatasetExample",
1255
+ back_populates="dataset_example_revision",
1256
+ )
1257
+
1028
1258
  __table_args__ = (
1029
1259
  UniqueConstraint(
1030
1260
  "dataset_example_id",
@@ -1033,7 +1263,56 @@ class DatasetExampleRevision(Base):
1033
1263
  )
1034
1264
 
1035
1265
 
1036
- class Experiment(Base):
1266
+ class DatasetSplit(HasId):
1267
+ __tablename__ = "dataset_splits"
1268
+
1269
+ user_id: Mapped[Optional[int]] = mapped_column(
1270
+ ForeignKey("users.id", ondelete="SET NULL"),
1271
+ nullable=True,
1272
+ index=True,
1273
+ )
1274
+ name: Mapped[str] = mapped_column(String, nullable=False, unique=True)
1275
+ description: Mapped[Optional[str]]
1276
+ color: Mapped[str] = mapped_column(String, nullable=False)
1277
+ metadata_: Mapped[dict[str, Any]] = mapped_column("metadata")
1278
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
1279
+ updated_at: Mapped[datetime] = mapped_column(
1280
+ UtcTimeStamp, server_default=func.now(), onupdate=func.now()
1281
+ )
1282
+ dataset_splits_dataset_examples: Mapped[list["DatasetSplitDatasetExample"]] = relationship(
1283
+ "DatasetSplitDatasetExample",
1284
+ back_populates="dataset_split",
1285
+ )
1286
+ experiment_dataset_splits: Mapped[list["ExperimentDatasetSplit"]] = relationship(
1287
+ "ExperimentDatasetSplit",
1288
+ back_populates="dataset_split",
1289
+ )
1290
+
1291
+
1292
+ class DatasetSplitDatasetExample(Base):
1293
+ __tablename__ = "dataset_splits_dataset_examples"
1294
+ dataset_split_id: Mapped[int] = mapped_column(
1295
+ ForeignKey("dataset_splits.id", ondelete="CASCADE"),
1296
+ )
1297
+ dataset_example_id: Mapped[int] = mapped_column(
1298
+ ForeignKey("dataset_examples.id", ondelete="CASCADE"),
1299
+ index=True,
1300
+ )
1301
+ dataset_split: Mapped["DatasetSplit"] = relationship(
1302
+ "DatasetSplit", back_populates="dataset_splits_dataset_examples"
1303
+ )
1304
+ dataset_example: Mapped["DatasetExample"] = relationship(
1305
+ "DatasetExample", back_populates="dataset_splits_dataset_examples"
1306
+ )
1307
+ __table_args__ = (
1308
+ PrimaryKeyConstraint(
1309
+ "dataset_split_id",
1310
+ "dataset_example_id",
1311
+ ),
1312
+ )
1313
+
1314
+
1315
+ class Experiment(HasId):
1037
1316
  __tablename__ = "experiments"
1038
1317
  dataset_id: Mapped[int] = mapped_column(
1039
1318
  ForeignKey("datasets.id", ondelete="CASCADE"),
@@ -1048,18 +1327,83 @@ class Experiment(Base):
1048
1327
  repetitions: Mapped[int]
1049
1328
  metadata_: Mapped[dict[str, Any]] = mapped_column("metadata")
1050
1329
  project_name: Mapped[Optional[str]] = mapped_column(index=True)
1330
+ user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
1051
1331
  created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
1052
1332
  updated_at: Mapped[datetime] = mapped_column(
1053
1333
  UtcTimeStamp, server_default=func.now(), onupdate=func.now()
1054
1334
  )
1335
+ user: Mapped[Optional["User"]] = relationship("User")
1336
+ experiment_dataset_splits: Mapped[list["ExperimentDatasetSplit"]] = relationship(
1337
+ "ExperimentDatasetSplit",
1338
+ back_populates="experiment",
1339
+ )
1340
+ experiment_dataset_examples: Mapped[list["ExperimentDatasetExample"]] = relationship(
1341
+ "ExperimentDatasetExample",
1342
+ back_populates="experiment",
1343
+ )
1344
+ experiment_tags: Mapped[list["ExperimentTag"]] = relationship(
1345
+ "ExperimentTag", back_populates="experiment"
1346
+ )
1055
1347
 
1056
1348
 
1057
- class ExperimentRun(Base):
1058
- __tablename__ = "experiment_runs"
1349
+ class ExperimentDatasetSplit(Base):
1350
+ __tablename__ = "experiments_dataset_splits"
1059
1351
  experiment_id: Mapped[int] = mapped_column(
1060
1352
  ForeignKey("experiments.id", ondelete="CASCADE"),
1353
+ )
1354
+ dataset_split_id: Mapped[int] = mapped_column(
1355
+ ForeignKey("dataset_splits.id", ondelete="CASCADE"),
1356
+ index=True,
1357
+ )
1358
+ experiment: Mapped["Experiment"] = relationship(
1359
+ "Experiment", back_populates="experiment_dataset_splits"
1360
+ )
1361
+ dataset_split: Mapped["DatasetSplit"] = relationship(
1362
+ "DatasetSplit", back_populates="experiment_dataset_splits"
1363
+ )
1364
+ __table_args__ = (
1365
+ PrimaryKeyConstraint(
1366
+ "experiment_id",
1367
+ "dataset_split_id",
1368
+ ),
1369
+ )
1370
+
1371
+
1372
+ class ExperimentDatasetExample(Base):
1373
+ __tablename__ = "experiments_dataset_examples"
1374
+ experiment_id: Mapped[int] = mapped_column(
1375
+ ForeignKey("experiments.id", ondelete="CASCADE"),
1376
+ )
1377
+ dataset_example_id: Mapped[int] = mapped_column(
1378
+ ForeignKey("dataset_examples.id", ondelete="CASCADE"),
1061
1379
  index=True,
1062
1380
  )
1381
+ dataset_example_revision_id: Mapped[int] = mapped_column(
1382
+ ForeignKey("dataset_example_revisions.id", ondelete="CASCADE"),
1383
+ index=True,
1384
+ )
1385
+ experiment: Mapped["Experiment"] = relationship(
1386
+ "Experiment", back_populates="experiment_dataset_examples"
1387
+ )
1388
+ dataset_example: Mapped["DatasetExample"] = relationship(
1389
+ "DatasetExample", back_populates="experiment_dataset_examples"
1390
+ )
1391
+ dataset_example_revision: Mapped["DatasetExampleRevision"] = relationship(
1392
+ "DatasetExampleRevision", back_populates="experiment_dataset_examples"
1393
+ )
1394
+ __table_args__ = (
1395
+ PrimaryKeyConstraint(
1396
+ "experiment_id",
1397
+ "dataset_example_id",
1398
+ ),
1399
+ )
1400
+
1401
+
1402
+ class ExperimentRun(HasId):
1403
+ __tablename__ = "experiment_runs"
1404
+ experiment_id: Mapped[int] = mapped_column(
1405
+ ForeignKey("experiments.id", ondelete="CASCADE"),
1406
+ )
1063
1407
  dataset_example_id: Mapped[int] = mapped_column(
1064
1408
  ForeignKey("dataset_examples.id", ondelete="CASCADE"),
1065
1409
  index=True,
@@ -1099,11 +1443,10 @@ class ExperimentRun(Base):
1099
1443
  )
1100
1444
 
1101
1445
 
1102
- class ExperimentRunAnnotation(Base):
1446
+ class ExperimentRunAnnotation(HasId):
1103
1447
  __tablename__ = "experiment_run_annotations"
1104
1448
  experiment_run_id: Mapped[int] = mapped_column(
1105
1449
  ForeignKey("experiment_runs.id", ondelete="CASCADE"),
1106
- index=True,
1107
1450
  )
1108
1451
  name: Mapped[str]
1109
1452
  annotator_kind: Mapped[str] = mapped_column(
@@ -1130,13 +1473,36 @@ class ExperimentRunAnnotation(Base):
1130
1473
  )
1131
1474
 
1132
1475
 
1133
- class UserRole(Base):
1476
+ class ExperimentTag(HasId):
1477
+ __tablename__ = "experiment_tags"
1478
+ experiment_id: Mapped[int] = mapped_column(
1479
+ ForeignKey("experiments.id", ondelete="CASCADE"),
1480
+ index=True,
1481
+ )
1482
+ dataset_id: Mapped[int] = mapped_column(
1483
+ ForeignKey("datasets.id", ondelete="CASCADE"),
1484
+ )
1485
+ user_id: Mapped[Optional[int]] = mapped_column(
1486
+ ForeignKey("users.id", ondelete="SET NULL"),
1487
+ index=True,
1488
+ nullable=True,
1489
+ )
1490
+ name: Mapped[str]
1491
+ description: Mapped[Optional[str]]
1492
+ experiment: Mapped["Experiment"] = relationship("Experiment", back_populates="experiment_tags")
1493
+ dataset: Mapped["Dataset"] = relationship("Dataset", back_populates="experiment_tags")
1494
+ user: Mapped[Optional["User"]] = relationship("User")
1495
+
1496
+ __table_args__ = (UniqueConstraint("dataset_id", "name"),)
1497
+
1498
+
1499
+ class UserRole(HasId):
1134
1500
  __tablename__ = "user_roles"
1135
- name: Mapped[str] = mapped_column(unique=True, index=True)
1501
+ name: Mapped[UserRoleName] = mapped_column(unique=True, index=True)
1136
1502
  users: Mapped[list["User"]] = relationship("User", back_populates="role")
1137
1503
 
1138
1504
 
1139
- class User(Base):
1505
+ class User(HasId):
1140
1506
  __tablename__ = "users"
1141
1507
  user_role_id: Mapped[int] = mapped_column(
1142
1508
  ForeignKey("user_roles.id", ondelete="CASCADE"),
@@ -1231,6 +1597,8 @@ class OAuth2User(User):
1231
1597
  *,
1232
1598
  email: str,
1233
1599
  username: str,
1600
+ oauth2_client_id: Optional[str] = None,
1601
+ oauth2_user_id: Optional[str] = None,
1234
1602
  user_role_id: Optional[int] = None,
1235
1603
  ) -> None:
1236
1604
  super().__init__(
@@ -1239,10 +1607,46 @@ class OAuth2User(User):
1239
1607
  user_role_id=user_role_id,
1240
1608
  reset_password=False,
1241
1609
  auth_method="OAUTH2",
1610
+ oauth2_client_id=oauth2_client_id,
1611
+ oauth2_user_id=oauth2_user_id,
1242
1612
  )
1243
1613
 
1244
1614
 
1245
- class PasswordResetToken(Base):
1615
+ def LDAPUser(
1616
+ *,
1617
+ email: str,
1618
+ username: str,
1619
+ unique_id: str | None = None,
1620
+ user_role_id: int | None = None,
1621
+ ) -> OAuth2User:
1622
+ """Factory function to create an LDAP user stored as OAuth2User.
1623
+
1624
+ This is a zero-migration approach: LDAP users are stored in the existing
1625
+ OAuth2User table with a special Unicode marker in oauth2_client_id to
1626
+ distinguish them from actual OAuth2 users. This avoids schema changes
1627
+ while allowing LDAP authentication to coexist with OAuth2.
1628
+
1629
+ Args:
1630
+ email: User's email address
1631
+ username: User's display name
1632
+ unique_id: User's LDAP unique ID (stored in oauth2_user_id)
1633
+ user_role_id: Phoenix role ID (ADMIN, MEMBER, VIEWER)
1634
+
1635
+ Returns:
1636
+ OAuth2User instance configured as an LDAP user
1637
+ """
1638
+ from phoenix.server.ldap import LDAP_CLIENT_ID_MARKER
1639
+
1640
+ return OAuth2User(
1641
+ email=email,
1642
+ username=username,
1643
+ oauth2_client_id=LDAP_CLIENT_ID_MARKER,
1644
+ oauth2_user_id=unique_id,
1645
+ user_role_id=user_role_id,
1646
+ )
1647
+
1648
+
1649
+ class PasswordResetToken(HasId):
1246
1650
  __tablename__ = "password_reset_tokens"
1247
1651
  user_id: Mapped[int] = mapped_column(
1248
1652
  ForeignKey("users.id", ondelete="CASCADE"),
@@ -1251,11 +1655,11 @@ class PasswordResetToken(Base):
1251
1655
  )
1252
1656
  user: Mapped["User"] = relationship("User", back_populates="password_reset_token")
1253
1657
  created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
1254
- expires_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp, nullable=False, index=True)
1658
+ expires_at: Mapped[datetime] = mapped_column(UtcTimeStamp, nullable=False, index=True)
1255
1659
  __table_args__ = (dict(sqlite_autoincrement=True),)
1256
1660
 
1257
1661
 
1258
- class RefreshToken(Base):
1662
+ class RefreshToken(HasId):
1259
1663
  __tablename__ = "refresh_tokens"
1260
1664
  user_id: Mapped[int] = mapped_column(
1261
1665
  ForeignKey("users.id", ondelete="CASCADE"),
@@ -1263,11 +1667,11 @@ class RefreshToken(Base):
1263
1667
  )
1264
1668
  user: Mapped["User"] = relationship("User", back_populates="refresh_tokens")
1265
1669
  created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
1266
- expires_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp, nullable=False, index=True)
1670
+ expires_at: Mapped[datetime] = mapped_column(UtcTimeStamp, nullable=False, index=True)
1267
1671
  __table_args__ = (dict(sqlite_autoincrement=True),)
1268
1672
 
1269
1673
 
1270
- class AccessToken(Base):
1674
+ class AccessToken(HasId):
1271
1675
  __tablename__ = "access_tokens"
1272
1676
  user_id: Mapped[int] = mapped_column(
1273
1677
  ForeignKey("users.id", ondelete="CASCADE"),
@@ -1275,7 +1679,7 @@ class AccessToken(Base):
1275
1679
  )
1276
1680
  user: Mapped["User"] = relationship("User", back_populates="access_tokens")
1277
1681
  created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
1278
- expires_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp, nullable=False, index=True)
1682
+ expires_at: Mapped[datetime] = mapped_column(UtcTimeStamp, nullable=False, index=True)
1279
1683
  refresh_token_id: Mapped[int] = mapped_column(
1280
1684
  ForeignKey("refresh_tokens.id", ondelete="CASCADE"),
1281
1685
  index=True,
@@ -1284,7 +1688,7 @@ class AccessToken(Base):
1284
1688
  __table_args__ = (dict(sqlite_autoincrement=True),)
1285
1689
 
1286
1690
 
1287
- class ApiKey(Base):
1691
+ class ApiKey(HasId):
1288
1692
  __tablename__ = "api_keys"
1289
1693
  user_id: Mapped[int] = mapped_column(
1290
1694
  ForeignKey("users.id", ondelete="CASCADE"),
@@ -1298,9 +1702,86 @@ class ApiKey(Base):
1298
1702
  __table_args__ = (dict(sqlite_autoincrement=True),)
1299
1703
 
1300
1704
 
1301
- class PromptLabel(Base):
1302
- __tablename__ = "prompt_labels"
1705
+ CostType: TypeAlias = Literal["DEFAULT", "OVERRIDE"]
1706
+
1707
+
1708
+ class GenerativeModel(HasId):
1709
+ __tablename__ = "generative_models"
1710
+ name: Mapped[str] = mapped_column(String, nullable=False)
1711
+ provider: Mapped[str]
1712
+ start_time: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp)
1713
+ name_pattern: Mapped[re.Pattern[str]] = mapped_column(_RegexStr, nullable=False)
1714
+ is_built_in: Mapped[bool] = mapped_column(
1715
+ Boolean,
1716
+ nullable=False,
1717
+ )
1718
+ created_at: Mapped[datetime] = mapped_column(
1719
+ UtcTimeStamp,
1720
+ server_default=func.now(),
1721
+ )
1722
+ updated_at: Mapped[datetime] = mapped_column(
1723
+ UtcTimeStamp,
1724
+ server_default=func.now(),
1725
+ onupdate=func.now(),
1726
+ )
1727
+ deleted_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp)
1728
+
1729
+ token_prices: Mapped[list["TokenPrice"]] = relationship(
1730
+ "TokenPrice",
1731
+ back_populates="model",
1732
+ cascade="all, delete-orphan",
1733
+ uselist=True,
1734
+ )
1735
+
1736
+ __table_args__ = (
1737
+ Index(
1738
+ "ix_generative_models_match_criteria",
1739
+ "name_pattern",
1740
+ "provider",
1741
+ "is_built_in",
1742
+ postgresql_where=sa.text("deleted_at IS NULL"),
1743
+ sqlite_where=sa.text("deleted_at IS NULL"),
1744
+ unique=True,
1745
+ ),
1746
+ Index(
1747
+ "ix_generative_models_name_is_built_in",
1748
+ "name",
1749
+ "is_built_in",
1750
+ postgresql_where=sa.text("deleted_at IS NULL"),
1751
+ sqlite_where=sa.text("deleted_at IS NULL"),
1752
+ unique=True,
1753
+ ),
1754
+ )
1755
+
1303
1756
 
1757
+ class TokenPrice(HasId):
1758
+ __tablename__ = "token_prices"
1759
+ model_id: Mapped[int] = mapped_column(
1760
+ ForeignKey("generative_models.id", ondelete="CASCADE"),
1761
+ nullable=False,
1762
+ index=True,
1763
+ )
1764
+ token_type: Mapped[str]
1765
+ is_prompt: Mapped[bool]
1766
+ base_rate: Mapped[float]
1767
+ customization: Mapped[TokenPriceCustomization] = mapped_column(_TokenCustomization)
1768
+
1769
+ model: Mapped["GenerativeModel"] = relationship(
1770
+ "GenerativeModel",
1771
+ back_populates="token_prices",
1772
+ )
1773
+
1774
+ __table_args__ = (
1775
+ UniqueConstraint(
1776
+ "model_id",
1777
+ "token_type",
1778
+ "is_prompt",
1779
+ ),
1780
+ )
1781
+
1782
+
1783
+ class PromptLabel(HasId):
1784
+ __tablename__ = "prompt_labels"
1304
1785
  name: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
1305
1786
  description: Mapped[Optional[str]]
1306
1787
  color: Mapped[str] = mapped_column(String, nullable=True)
@@ -1313,9 +1794,8 @@ class PromptLabel(Base):
1313
1794
  )
1314
1795
 
1315
1796
 
1316
- class Prompt(Base):
1797
+ class Prompt(HasId):
1317
1798
  __tablename__ = "prompts"
1318
-
1319
1799
  source_prompt_id: Mapped[Optional[int]] = mapped_column(
1320
1800
  ForeignKey("prompts.id", ondelete="SET NULL"),
1321
1801
  index=True,
@@ -1351,9 +1831,8 @@ class Prompt(Base):
1351
1831
  )
1352
1832
 
1353
1833
 
1354
- class PromptPromptLabel(Base):
1834
+ class PromptPromptLabel(HasId):
1355
1835
  __tablename__ = "prompts_prompt_labels"
1356
-
1357
1836
  prompt_label_id: Mapped[int] = mapped_column(
1358
1837
  ForeignKey("prompt_labels.id", ondelete="CASCADE"),
1359
1838
  index=True,
@@ -1373,7 +1852,7 @@ class PromptPromptLabel(Base):
1373
1852
  __table_args__ = (UniqueConstraint("prompt_label_id", "prompt_id"),)
1374
1853
 
1375
1854
 
1376
- class PromptVersion(Base):
1855
+ class PromptVersion(HasId):
1377
1856
  __tablename__ = "prompt_versions"
1378
1857
 
1379
1858
  prompt_id: Mapped[int] = mapped_column(
@@ -1422,7 +1901,7 @@ class PromptVersion(Base):
1422
1901
  )
1423
1902
 
1424
1903
 
1425
- class PromptVersionTag(Base):
1904
+ class PromptVersionTag(HasId):
1426
1905
  __tablename__ = "prompt_version_tags"
1427
1906
 
1428
1907
  name: Mapped[Identifier] = mapped_column(_Identifier, nullable=False)
@@ -1451,18 +1930,14 @@ class PromptVersionTag(Base):
1451
1930
  __table_args__ = (UniqueConstraint("name", "prompt_id"),)
1452
1931
 
1453
1932
 
1454
- class AnnotationConfig(Base):
1933
+ class AnnotationConfig(HasId):
1455
1934
  __tablename__ = "annotation_configs"
1456
-
1457
- id: Mapped[int] = mapped_column(primary_key=True)
1458
1935
  name: Mapped[str] = mapped_column(String, nullable=False, unique=True)
1459
1936
  config: Mapped[AnnotationConfigType] = mapped_column(_AnnotationConfig, nullable=False)
1460
1937
 
1461
1938
 
1462
- class ProjectAnnotationConfig(Base):
1939
+ class ProjectAnnotationConfig(HasId):
1463
1940
  __tablename__ = "project_annotation_configs"
1464
-
1465
- id: Mapped[int] = mapped_column(primary_key=True)
1466
1941
  project_id: Mapped[int] = mapped_column(
1467
1942
  ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
1468
1943
  )
@@ -1471,3 +1946,141 @@ class ProjectAnnotationConfig(Base):
1471
1946
  )
1472
1947
 
1473
1948
  __table_args__ = (UniqueConstraint("project_id", "annotation_config_id"),)
1949
+
1950
+
1951
+ class SpanCost(HasId):
1952
+ __tablename__ = "span_costs"
1953
+
1954
+ span_rowid: Mapped[int] = mapped_column(
1955
+ ForeignKey("spans.id", ondelete="CASCADE"),
1956
+ nullable=False,
1957
+ index=True,
1958
+ )
1959
+ trace_rowid: Mapped[int] = mapped_column(
1960
+ ForeignKey("traces.id", ondelete="CASCADE"),
1961
+ nullable=False,
1962
+ index=True,
1963
+ )
1964
+ span_start_time: Mapped[datetime] = mapped_column(
1965
+ UtcTimeStamp,
1966
+ nullable=False,
1967
+ index=True,
1968
+ )
1969
+ model_id: Mapped[Optional[int]] = mapped_column(
1970
+ sa.Integer,
1971
+ ForeignKey(
1972
+ "generative_models.id",
1973
+ ondelete="RESTRICT",
1974
+ ),
1975
+ nullable=True,
1976
+ )
1977
+ total_cost: Mapped[Optional[float]]
1978
+ total_tokens: Mapped[Optional[float]]
1979
+
1980
+ @hybrid_property
1981
+ def total_cost_per_token(self) -> Optional[float]:
1982
+ return ((self.total_cost or 0) / self.total_tokens) if self.total_tokens else None
1983
+
1984
+ @total_cost_per_token.inplace.expression
1985
+ @classmethod
1986
+ def _total_cost_per_token_expression(cls) -> ColumnElement[Optional[float]]:
1987
+ return sql.case(
1988
+ (
1989
+ sa.and_(cls.total_tokens.isnot(None), cls.total_tokens != 0),
1990
+ cls.total_cost / cls.total_tokens,
1991
+ )
1992
+ )
1993
+
1994
+ prompt_cost: Mapped[Optional[float]]
1995
+ prompt_tokens: Mapped[Optional[float]]
1996
+
1997
+ @hybrid_property
1998
+ def prompt_cost_per_token(self) -> Optional[float]:
1999
+ return ((self.prompt_cost or 0) / self.prompt_tokens) if self.prompt_tokens else None
2000
+
2001
+ @prompt_cost_per_token.inplace.expression
2002
+ @classmethod
2003
+ def _prompt_cost_per_token_expression(cls) -> ColumnElement[Optional[float]]:
2004
+ return sql.case(
2005
+ (
2006
+ sa.and_(cls.prompt_tokens.isnot(None), cls.prompt_tokens != 0),
2007
+ cls.prompt_cost / cls.prompt_tokens,
2008
+ )
2009
+ )
2010
+
2011
+ completion_cost: Mapped[Optional[float]]
2012
+ completion_tokens: Mapped[Optional[float]]
2013
+
2014
+ @hybrid_property
2015
+ def completion_cost_per_token(self) -> Optional[float]:
2016
+ return (
2017
+ ((self.completion_cost or 0) / self.completion_tokens)
2018
+ if self.completion_tokens
2019
+ else None
2020
+ )
2021
+
2022
+ @completion_cost_per_token.inplace.expression
2023
+ @classmethod
2024
+ def _completion_cost_per_token_expression(cls) -> ColumnElement[Optional[float]]:
2025
+ return sql.case(
2026
+ (
2027
+ sa.and_(cls.completion_tokens.isnot(None), cls.completion_tokens != 0),
2028
+ cls.completion_cost / cls.completion_tokens,
2029
+ )
2030
+ )
2031
+
2032
+ span: Mapped["Span"] = relationship("Span", back_populates="span_cost")
2033
+ trace: Mapped["Trace"] = relationship("Trace", back_populates="span_costs")
2034
+ span_cost_details: Mapped[list["SpanCostDetail"]] = relationship(
2035
+ "SpanCostDetail",
2036
+ back_populates="span_cost",
2037
+ cascade="all, delete-orphan",
2038
+ uselist=True,
2039
+ )
2040
+
2041
+ __table_args__ = (
2042
+ Index(
2043
+ "ix_span_costs_model_id_span_start_time",
2044
+ "model_id",
2045
+ "span_start_time",
2046
+ ),
2047
+ )
2048
+
2049
+ def append_detail(self, detail: "SpanCostDetail") -> None:
2050
+ self.span_cost_details.append(detail)
2051
+ if cost := detail.cost:
2052
+ if detail.is_prompt:
2053
+ self.prompt_cost = (self.prompt_cost or 0) + cost
2054
+ else:
2055
+ self.completion_cost = (self.completion_cost or 0) + cost
2056
+ self.total_cost = (self.total_cost or 0) + cost
2057
+ if tokens := detail.tokens:
2058
+ if detail.is_prompt:
2059
+ self.prompt_tokens = (self.prompt_tokens or 0) + tokens
2060
+ else:
2061
+ self.completion_tokens = (self.completion_tokens or 0) + tokens
2062
+ self.total_tokens = (self.total_tokens or 0) + tokens
2063
+
2064
+
2065
+ class SpanCostDetail(HasId):
2066
+ __tablename__ = "span_cost_details"
2067
+ span_cost_id: Mapped[int] = mapped_column(
2068
+ ForeignKey("span_costs.id", ondelete="CASCADE"),
2069
+ nullable=False,
2070
+ )
2071
+ token_type: Mapped[str] = mapped_column(index=True)
2072
+ is_prompt: Mapped[bool]
2073
+
2074
+ cost: Mapped[Optional[float]]
2075
+ tokens: Mapped[Optional[float]]
2076
+ cost_per_token: Mapped[Optional[float]]
2077
+
2078
+ span_cost: Mapped["SpanCost"] = relationship("SpanCost", back_populates="span_cost_details")
2079
+
2080
+ __table_args__ = (
2081
+ UniqueConstraint(
2082
+ "span_cost_id",
2083
+ "token_type",
2084
+ "is_prompt",
2085
+ ),
2086
+ )