arize-phoenix 10.0.4__py3-none-any.whl → 12.28.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/METADATA +124 -72
  2. arize_phoenix-12.28.1.dist-info/RECORD +499 -0
  3. {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/WHEEL +1 -1
  4. {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/IP_NOTICE +1 -1
  5. phoenix/__generated__/__init__.py +0 -0
  6. phoenix/__generated__/classification_evaluator_configs/__init__.py +20 -0
  7. phoenix/__generated__/classification_evaluator_configs/_document_relevance_classification_evaluator_config.py +17 -0
  8. phoenix/__generated__/classification_evaluator_configs/_hallucination_classification_evaluator_config.py +17 -0
  9. phoenix/__generated__/classification_evaluator_configs/_models.py +18 -0
  10. phoenix/__generated__/classification_evaluator_configs/_tool_selection_classification_evaluator_config.py +17 -0
  11. phoenix/__init__.py +5 -4
  12. phoenix/auth.py +39 -2
  13. phoenix/config.py +1763 -91
  14. phoenix/datetime_utils.py +120 -2
  15. phoenix/db/README.md +595 -25
  16. phoenix/db/bulk_inserter.py +145 -103
  17. phoenix/db/engines.py +140 -33
  18. phoenix/db/enums.py +3 -12
  19. phoenix/db/facilitator.py +302 -35
  20. phoenix/db/helpers.py +1000 -65
  21. phoenix/db/iam_auth.py +64 -0
  22. phoenix/db/insertion/dataset.py +135 -2
  23. phoenix/db/insertion/document_annotation.py +9 -6
  24. phoenix/db/insertion/evaluation.py +2 -3
  25. phoenix/db/insertion/helpers.py +17 -2
  26. phoenix/db/insertion/session_annotation.py +176 -0
  27. phoenix/db/insertion/span.py +15 -11
  28. phoenix/db/insertion/span_annotation.py +3 -4
  29. phoenix/db/insertion/trace_annotation.py +3 -4
  30. phoenix/db/insertion/types.py +50 -20
  31. phoenix/db/migrations/versions/01a8342c9cdf_add_user_id_on_datasets.py +40 -0
  32. phoenix/db/migrations/versions/0df286449799_add_session_annotations_table.py +105 -0
  33. phoenix/db/migrations/versions/272b66ff50f8_drop_single_indices.py +119 -0
  34. phoenix/db/migrations/versions/58228d933c91_dataset_labels.py +67 -0
  35. phoenix/db/migrations/versions/699f655af132_experiment_tags.py +57 -0
  36. phoenix/db/migrations/versions/735d3d93c33e_add_composite_indices.py +41 -0
  37. phoenix/db/migrations/versions/a20694b15f82_cost.py +196 -0
  38. phoenix/db/migrations/versions/ab513d89518b_add_user_id_on_dataset_versions.py +40 -0
  39. phoenix/db/migrations/versions/d0690a79ea51_users_on_experiments.py +40 -0
  40. phoenix/db/migrations/versions/deb2c81c0bb2_dataset_splits.py +139 -0
  41. phoenix/db/migrations/versions/e76cbd66ffc3_add_experiments_dataset_examples.py +87 -0
  42. phoenix/db/models.py +669 -56
  43. phoenix/db/pg_config.py +10 -0
  44. phoenix/db/types/model_provider.py +4 -0
  45. phoenix/db/types/token_price_customization.py +29 -0
  46. phoenix/db/types/trace_retention.py +23 -15
  47. phoenix/experiments/evaluators/utils.py +3 -3
  48. phoenix/experiments/functions.py +160 -52
  49. phoenix/experiments/tracing.py +2 -2
  50. phoenix/experiments/types.py +1 -1
  51. phoenix/inferences/inferences.py +1 -2
  52. phoenix/server/api/auth.py +38 -7
  53. phoenix/server/api/auth_messages.py +46 -0
  54. phoenix/server/api/context.py +100 -4
  55. phoenix/server/api/dataloaders/__init__.py +79 -5
  56. phoenix/server/api/dataloaders/annotation_configs_by_project.py +31 -0
  57. phoenix/server/api/dataloaders/annotation_summaries.py +60 -8
  58. phoenix/server/api/dataloaders/average_experiment_repeated_run_group_latency.py +50 -0
  59. phoenix/server/api/dataloaders/average_experiment_run_latency.py +17 -24
  60. phoenix/server/api/dataloaders/cache/two_tier_cache.py +1 -2
  61. phoenix/server/api/dataloaders/dataset_dataset_splits.py +52 -0
  62. phoenix/server/api/dataloaders/dataset_example_revisions.py +0 -1
  63. phoenix/server/api/dataloaders/dataset_example_splits.py +40 -0
  64. phoenix/server/api/dataloaders/dataset_examples_and_versions_by_experiment_run.py +47 -0
  65. phoenix/server/api/dataloaders/dataset_labels.py +36 -0
  66. phoenix/server/api/dataloaders/document_evaluation_summaries.py +2 -2
  67. phoenix/server/api/dataloaders/document_evaluations.py +6 -9
  68. phoenix/server/api/dataloaders/experiment_annotation_summaries.py +88 -34
  69. phoenix/server/api/dataloaders/experiment_dataset_splits.py +43 -0
  70. phoenix/server/api/dataloaders/experiment_error_rates.py +21 -28
  71. phoenix/server/api/dataloaders/experiment_repeated_run_group_annotation_summaries.py +77 -0
  72. phoenix/server/api/dataloaders/experiment_repeated_run_groups.py +57 -0
  73. phoenix/server/api/dataloaders/experiment_runs_by_experiment_and_example.py +44 -0
  74. phoenix/server/api/dataloaders/last_used_times_by_generative_model_id.py +35 -0
  75. phoenix/server/api/dataloaders/latency_ms_quantile.py +40 -8
  76. phoenix/server/api/dataloaders/record_counts.py +37 -10
  77. phoenix/server/api/dataloaders/session_annotations_by_session.py +29 -0
  78. phoenix/server/api/dataloaders/span_cost_by_span.py +24 -0
  79. phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_generative_model.py +56 -0
  80. phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_project_session.py +57 -0
  81. phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_span.py +43 -0
  82. phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_trace.py +56 -0
  83. phoenix/server/api/dataloaders/span_cost_details_by_span_cost.py +27 -0
  84. phoenix/server/api/dataloaders/span_cost_summary_by_experiment.py +57 -0
  85. phoenix/server/api/dataloaders/span_cost_summary_by_experiment_repeated_run_group.py +64 -0
  86. phoenix/server/api/dataloaders/span_cost_summary_by_experiment_run.py +58 -0
  87. phoenix/server/api/dataloaders/span_cost_summary_by_generative_model.py +55 -0
  88. phoenix/server/api/dataloaders/span_cost_summary_by_project.py +152 -0
  89. phoenix/server/api/dataloaders/span_cost_summary_by_project_session.py +56 -0
  90. phoenix/server/api/dataloaders/span_cost_summary_by_trace.py +55 -0
  91. phoenix/server/api/dataloaders/span_costs.py +29 -0
  92. phoenix/server/api/dataloaders/table_fields.py +2 -2
  93. phoenix/server/api/dataloaders/token_prices_by_model.py +30 -0
  94. phoenix/server/api/dataloaders/trace_annotations_by_trace.py +27 -0
  95. phoenix/server/api/dataloaders/types.py +29 -0
  96. phoenix/server/api/exceptions.py +11 -1
  97. phoenix/server/api/helpers/dataset_helpers.py +5 -1
  98. phoenix/server/api/helpers/playground_clients.py +1243 -292
  99. phoenix/server/api/helpers/playground_registry.py +2 -2
  100. phoenix/server/api/helpers/playground_spans.py +8 -4
  101. phoenix/server/api/helpers/playground_users.py +26 -0
  102. phoenix/server/api/helpers/prompts/conversions/aws.py +83 -0
  103. phoenix/server/api/helpers/prompts/conversions/google.py +103 -0
  104. phoenix/server/api/helpers/prompts/models.py +205 -22
  105. phoenix/server/api/input_types/{SpanAnnotationFilter.py → AnnotationFilter.py} +22 -14
  106. phoenix/server/api/input_types/ChatCompletionInput.py +6 -2
  107. phoenix/server/api/input_types/CreateProjectInput.py +27 -0
  108. phoenix/server/api/input_types/CreateProjectSessionAnnotationInput.py +37 -0
  109. phoenix/server/api/input_types/DatasetFilter.py +17 -0
  110. phoenix/server/api/input_types/ExperimentRunSort.py +237 -0
  111. phoenix/server/api/input_types/GenerativeCredentialInput.py +9 -0
  112. phoenix/server/api/input_types/GenerativeModelInput.py +5 -0
  113. phoenix/server/api/input_types/ProjectSessionSort.py +161 -1
  114. phoenix/server/api/input_types/PromptFilter.py +14 -0
  115. phoenix/server/api/input_types/PromptVersionInput.py +52 -1
  116. phoenix/server/api/input_types/SpanSort.py +44 -7
  117. phoenix/server/api/input_types/TimeBinConfig.py +23 -0
  118. phoenix/server/api/input_types/UpdateAnnotationInput.py +34 -0
  119. phoenix/server/api/input_types/UserRoleInput.py +1 -0
  120. phoenix/server/api/mutations/__init__.py +10 -0
  121. phoenix/server/api/mutations/annotation_config_mutations.py +8 -8
  122. phoenix/server/api/mutations/api_key_mutations.py +19 -23
  123. phoenix/server/api/mutations/chat_mutations.py +154 -47
  124. phoenix/server/api/mutations/dataset_label_mutations.py +243 -0
  125. phoenix/server/api/mutations/dataset_mutations.py +21 -16
  126. phoenix/server/api/mutations/dataset_split_mutations.py +351 -0
  127. phoenix/server/api/mutations/experiment_mutations.py +2 -2
  128. phoenix/server/api/mutations/export_events_mutations.py +3 -3
  129. phoenix/server/api/mutations/model_mutations.py +210 -0
  130. phoenix/server/api/mutations/project_mutations.py +49 -10
  131. phoenix/server/api/mutations/project_session_annotations_mutations.py +158 -0
  132. phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +8 -4
  133. phoenix/server/api/mutations/prompt_label_mutations.py +74 -65
  134. phoenix/server/api/mutations/prompt_mutations.py +65 -129
  135. phoenix/server/api/mutations/prompt_version_tag_mutations.py +11 -8
  136. phoenix/server/api/mutations/span_annotations_mutations.py +15 -10
  137. phoenix/server/api/mutations/trace_annotations_mutations.py +14 -10
  138. phoenix/server/api/mutations/trace_mutations.py +47 -3
  139. phoenix/server/api/mutations/user_mutations.py +66 -41
  140. phoenix/server/api/queries.py +768 -293
  141. phoenix/server/api/routers/__init__.py +2 -2
  142. phoenix/server/api/routers/auth.py +154 -88
  143. phoenix/server/api/routers/ldap.py +229 -0
  144. phoenix/server/api/routers/oauth2.py +369 -106
  145. phoenix/server/api/routers/v1/__init__.py +24 -4
  146. phoenix/server/api/routers/v1/annotation_configs.py +23 -31
  147. phoenix/server/api/routers/v1/annotations.py +481 -17
  148. phoenix/server/api/routers/v1/datasets.py +395 -81
  149. phoenix/server/api/routers/v1/documents.py +142 -0
  150. phoenix/server/api/routers/v1/evaluations.py +24 -31
  151. phoenix/server/api/routers/v1/experiment_evaluations.py +19 -8
  152. phoenix/server/api/routers/v1/experiment_runs.py +337 -59
  153. phoenix/server/api/routers/v1/experiments.py +479 -48
  154. phoenix/server/api/routers/v1/models.py +7 -0
  155. phoenix/server/api/routers/v1/projects.py +18 -49
  156. phoenix/server/api/routers/v1/prompts.py +54 -40
  157. phoenix/server/api/routers/v1/sessions.py +108 -0
  158. phoenix/server/api/routers/v1/spans.py +1091 -81
  159. phoenix/server/api/routers/v1/traces.py +132 -78
  160. phoenix/server/api/routers/v1/users.py +389 -0
  161. phoenix/server/api/routers/v1/utils.py +3 -7
  162. phoenix/server/api/subscriptions.py +305 -88
  163. phoenix/server/api/types/Annotation.py +90 -23
  164. phoenix/server/api/types/ApiKey.py +13 -17
  165. phoenix/server/api/types/AuthMethod.py +1 -0
  166. phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +1 -0
  167. phoenix/server/api/types/CostBreakdown.py +12 -0
  168. phoenix/server/api/types/Dataset.py +226 -72
  169. phoenix/server/api/types/DatasetExample.py +88 -18
  170. phoenix/server/api/types/DatasetExperimentAnnotationSummary.py +10 -0
  171. phoenix/server/api/types/DatasetLabel.py +57 -0
  172. phoenix/server/api/types/DatasetSplit.py +98 -0
  173. phoenix/server/api/types/DatasetVersion.py +49 -4
  174. phoenix/server/api/types/DocumentAnnotation.py +212 -0
  175. phoenix/server/api/types/Experiment.py +264 -59
  176. phoenix/server/api/types/ExperimentComparison.py +5 -10
  177. phoenix/server/api/types/ExperimentRepeatedRunGroup.py +155 -0
  178. phoenix/server/api/types/ExperimentRepeatedRunGroupAnnotationSummary.py +9 -0
  179. phoenix/server/api/types/ExperimentRun.py +169 -65
  180. phoenix/server/api/types/ExperimentRunAnnotation.py +158 -39
  181. phoenix/server/api/types/GenerativeModel.py +245 -3
  182. phoenix/server/api/types/GenerativeProvider.py +70 -11
  183. phoenix/server/api/types/{Model.py → InferenceModel.py} +1 -1
  184. phoenix/server/api/types/ModelInterface.py +16 -0
  185. phoenix/server/api/types/PlaygroundModel.py +20 -0
  186. phoenix/server/api/types/Project.py +1278 -216
  187. phoenix/server/api/types/ProjectSession.py +188 -28
  188. phoenix/server/api/types/ProjectSessionAnnotation.py +187 -0
  189. phoenix/server/api/types/ProjectTraceRetentionPolicy.py +1 -1
  190. phoenix/server/api/types/Prompt.py +119 -39
  191. phoenix/server/api/types/PromptLabel.py +42 -25
  192. phoenix/server/api/types/PromptVersion.py +11 -8
  193. phoenix/server/api/types/PromptVersionTag.py +65 -25
  194. phoenix/server/api/types/ServerStatus.py +6 -0
  195. phoenix/server/api/types/Span.py +167 -123
  196. phoenix/server/api/types/SpanAnnotation.py +189 -42
  197. phoenix/server/api/types/SpanCostDetailSummaryEntry.py +10 -0
  198. phoenix/server/api/types/SpanCostSummary.py +10 -0
  199. phoenix/server/api/types/SystemApiKey.py +65 -1
  200. phoenix/server/api/types/TokenPrice.py +16 -0
  201. phoenix/server/api/types/TokenUsage.py +3 -3
  202. phoenix/server/api/types/Trace.py +223 -51
  203. phoenix/server/api/types/TraceAnnotation.py +149 -50
  204. phoenix/server/api/types/User.py +137 -32
  205. phoenix/server/api/types/UserApiKey.py +73 -26
  206. phoenix/server/api/types/node.py +10 -0
  207. phoenix/server/api/types/pagination.py +11 -2
  208. phoenix/server/app.py +290 -45
  209. phoenix/server/authorization.py +38 -3
  210. phoenix/server/bearer_auth.py +34 -24
  211. phoenix/server/cost_tracking/cost_details_calculator.py +196 -0
  212. phoenix/server/cost_tracking/cost_model_lookup.py +179 -0
  213. phoenix/server/cost_tracking/helpers.py +68 -0
  214. phoenix/server/cost_tracking/model_cost_manifest.json +3657 -830
  215. phoenix/server/cost_tracking/regex_specificity.py +397 -0
  216. phoenix/server/cost_tracking/token_cost_calculator.py +57 -0
  217. phoenix/server/daemons/__init__.py +0 -0
  218. phoenix/server/daemons/db_disk_usage_monitor.py +214 -0
  219. phoenix/server/daemons/generative_model_store.py +103 -0
  220. phoenix/server/daemons/span_cost_calculator.py +99 -0
  221. phoenix/server/dml_event.py +17 -0
  222. phoenix/server/dml_event_handler.py +5 -0
  223. phoenix/server/email/sender.py +56 -3
  224. phoenix/server/email/templates/db_disk_usage_notification.html +19 -0
  225. phoenix/server/email/types.py +11 -0
  226. phoenix/server/experiments/__init__.py +0 -0
  227. phoenix/server/experiments/utils.py +14 -0
  228. phoenix/server/grpc_server.py +11 -11
  229. phoenix/server/jwt_store.py +17 -15
  230. phoenix/server/ldap.py +1449 -0
  231. phoenix/server/main.py +26 -10
  232. phoenix/server/oauth2.py +330 -12
  233. phoenix/server/prometheus.py +66 -6
  234. phoenix/server/rate_limiters.py +4 -9
  235. phoenix/server/retention.py +33 -20
  236. phoenix/server/session_filters.py +49 -0
  237. phoenix/server/static/.vite/manifest.json +55 -51
  238. phoenix/server/static/assets/components-BreFUQQa.js +6702 -0
  239. phoenix/server/static/assets/{index-E0M82BdE.js → index-CTQoemZv.js} +140 -56
  240. phoenix/server/static/assets/pages-DBE5iYM3.js +9524 -0
  241. phoenix/server/static/assets/vendor-BGzfc4EU.css +1 -0
  242. phoenix/server/static/assets/vendor-DCE4v-Ot.js +920 -0
  243. phoenix/server/static/assets/vendor-codemirror-D5f205eT.js +25 -0
  244. phoenix/server/static/assets/vendor-recharts-V9cwpXsm.js +37 -0
  245. phoenix/server/static/assets/vendor-shiki-Do--csgv.js +5 -0
  246. phoenix/server/static/assets/vendor-three-CmB8bl_y.js +3840 -0
  247. phoenix/server/templates/index.html +40 -6
  248. phoenix/server/thread_server.py +1 -2
  249. phoenix/server/types.py +14 -4
  250. phoenix/server/utils.py +74 -0
  251. phoenix/session/client.py +56 -3
  252. phoenix/session/data_extractor.py +5 -0
  253. phoenix/session/evaluation.py +14 -5
  254. phoenix/session/session.py +45 -9
  255. phoenix/settings.py +5 -0
  256. phoenix/trace/attributes.py +80 -13
  257. phoenix/trace/dsl/helpers.py +90 -1
  258. phoenix/trace/dsl/query.py +8 -6
  259. phoenix/trace/projects.py +5 -0
  260. phoenix/utilities/template_formatters.py +1 -1
  261. phoenix/version.py +1 -1
  262. arize_phoenix-10.0.4.dist-info/RECORD +0 -405
  263. phoenix/server/api/types/Evaluation.py +0 -39
  264. phoenix/server/cost_tracking/cost_lookup.py +0 -255
  265. phoenix/server/static/assets/components-DULKeDfL.js +0 -4365
  266. phoenix/server/static/assets/pages-Cl0A-0U2.js +0 -7430
  267. phoenix/server/static/assets/vendor-WIZid84E.css +0 -1
  268. phoenix/server/static/assets/vendor-arizeai-Dy-0mSNw.js +0 -649
  269. phoenix/server/static/assets/vendor-codemirror-DBtifKNr.js +0 -33
  270. phoenix/server/static/assets/vendor-oB4u9zuV.js +0 -905
  271. phoenix/server/static/assets/vendor-recharts-D-T4KPz2.js +0 -59
  272. phoenix/server/static/assets/vendor-shiki-BMn4O_9F.js +0 -5
  273. phoenix/server/static/assets/vendor-three-C5WAXd5r.js +0 -2998
  274. phoenix/utilities/deprecation.py +0 -31
  275. {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/entry_points.txt +0 -0
  276. {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,210 @@
1
+ import re
2
+ from datetime import datetime, timezone
3
+ from typing import Optional
4
+
5
+ import sqlalchemy as sa
6
+ import strawberry
7
+ from sqlalchemy import delete
8
+ from sqlalchemy.exc import IntegrityError as PostgreSQLIntegrityError
9
+ from sqlalchemy.orm import joinedload
10
+ from sqlean.dbapi2 import IntegrityError as SQLiteIntegrityError # type: ignore[import-untyped]
11
+ from strawberry.relay import GlobalID
12
+ from strawberry.types import Info
13
+
14
+ from phoenix.db import models
15
+ from phoenix.server.api.auth import IsNotReadOnly, IsNotViewer
16
+ from phoenix.server.api.context import Context
17
+ from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound
18
+ from phoenix.server.api.queries import Query
19
+ from phoenix.server.api.types.GenerativeModel import GenerativeModel
20
+ from phoenix.server.api.types.node import from_global_id_with_expected_type
21
+ from phoenix.server.api.types.TokenPrice import TokenKind
22
+
23
+
24
+ @strawberry.input
25
+ class TokenPriceInput:
26
+ token_type: str
27
+ cost_per_million_tokens: float
28
+ kind: TokenKind
29
+
30
+ @property
31
+ def token_prices(self) -> models.TokenPrice:
32
+ """Generate TokenPrice instances based on the input."""
33
+ return models.TokenPrice(
34
+ token_type=self.token_type,
35
+ is_prompt=self.kind == TokenKind.PROMPT,
36
+ base_rate=self.cost_per_million_tokens / 1_000_000,
37
+ )
38
+
39
+
40
+ @strawberry.input
41
+ class CreateModelMutationInput:
42
+ name: str
43
+ provider: Optional[str] = None
44
+ name_pattern: str
45
+ costs: list[TokenPriceInput]
46
+ start_time: Optional[datetime] = None
47
+
48
+
49
+ @strawberry.type
50
+ class CreateModelMutationPayload:
51
+ model: GenerativeModel
52
+ query: Query
53
+
54
+
55
+ @strawberry.input
56
+ class UpdateModelMutationInput:
57
+ id: GlobalID
58
+ name: str
59
+ provider: Optional[str]
60
+ name_pattern: str
61
+ costs: list[TokenPriceInput]
62
+ start_time: Optional[datetime] = None
63
+
64
+
65
+ @strawberry.type
66
+ class UpdateModelMutationPayload:
67
+ model: GenerativeModel
68
+ query: Query
69
+
70
+
71
+ @strawberry.input
72
+ class DeleteModelMutationInput:
73
+ id: GlobalID
74
+
75
+
76
+ @strawberry.type
77
+ class DeleteModelMutationPayload:
78
+ model: GenerativeModel
79
+ query: Query
80
+
81
+
82
+ @strawberry.type
83
+ class ModelMutationMixin:
84
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
85
+ async def create_model(
86
+ self,
87
+ info: Info[Context, None],
88
+ input: CreateModelMutationInput,
89
+ ) -> CreateModelMutationPayload:
90
+ cost_types = set(cost.token_type for cost in input.costs)
91
+ if "input" not in cost_types:
92
+ raise BadRequest("input cost is required")
93
+ if "output" not in cost_types:
94
+ raise BadRequest("output cost is required")
95
+ name_pattern = _compile_regular_expression(input.name_pattern)
96
+ token_prices = [cost.token_prices for cost in input.costs]
97
+ model = models.GenerativeModel(
98
+ name=input.name,
99
+ provider=input.provider,
100
+ name_pattern=name_pattern,
101
+ is_built_in=False,
102
+ token_prices=token_prices,
103
+ start_time=input.start_time,
104
+ )
105
+ async with info.context.db() as session:
106
+ session.add(model)
107
+ try:
108
+ await session.flush()
109
+ except (PostgreSQLIntegrityError, SQLiteIntegrityError):
110
+ raise Conflict(f"Model with name '{input.name}' already exists")
111
+
112
+ return CreateModelMutationPayload(
113
+ model=GenerativeModel(id=model.id, db_record=model),
114
+ query=Query(),
115
+ )
116
+
117
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
118
+ async def update_model(
119
+ self,
120
+ info: Info[Context, None],
121
+ input: UpdateModelMutationInput,
122
+ ) -> UpdateModelMutationPayload:
123
+ try:
124
+ model_id = from_global_id_with_expected_type(input.id, GenerativeModel.__name__)
125
+ except ValueError:
126
+ raise BadRequest(f'Invalid model id: "{input.id}"')
127
+
128
+ cost_types = set(cost.token_type for cost in input.costs)
129
+ if "input" not in cost_types:
130
+ raise BadRequest("input cost is required")
131
+ if "output" not in cost_types:
132
+ raise BadRequest("output cost is required")
133
+ name_pattern = _compile_regular_expression(input.name_pattern)
134
+ token_prices = [cost.token_prices for cost in input.costs]
135
+ async with info.context.db() as session:
136
+ model = await session.scalar(
137
+ sa.select(models.GenerativeModel)
138
+ .where(models.GenerativeModel.deleted_at.is_(None))
139
+ .where(models.GenerativeModel.id == model_id)
140
+ .options(joinedload(models.GenerativeModel.token_prices))
141
+ )
142
+ if model is None:
143
+ raise NotFound(f'Model "{input.id}" not found')
144
+ if model.is_built_in:
145
+ raise BadRequest("Cannot update built-in model")
146
+
147
+ await session.execute(
148
+ delete(models.TokenPrice).where(models.TokenPrice.model_id == model.id)
149
+ )
150
+
151
+ await session.refresh(model)
152
+
153
+ model.name = input.name
154
+ model.provider = input.provider or ""
155
+ model.name_pattern = name_pattern
156
+ model.token_prices = token_prices
157
+ model.start_time = input.start_time
158
+ # Explicitly set updated_at so the GenerativeModelStore daemon picks up this
159
+ # change (SQLAlchemy's onupdate may not trigger for relationship-only changes).
160
+ model.updated_at = datetime.now(timezone.utc)
161
+ session.add(model)
162
+ try:
163
+ await session.flush()
164
+ except (PostgreSQLIntegrityError, SQLiteIntegrityError):
165
+ raise Conflict(f"Model with name '{input.name}' already exists")
166
+
167
+ return UpdateModelMutationPayload(
168
+ model=GenerativeModel(id=model.id, db_record=model),
169
+ query=Query(),
170
+ )
171
+
172
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
173
+ async def delete_model(
174
+ self,
175
+ info: Info[Context, None],
176
+ input: DeleteModelMutationInput,
177
+ ) -> DeleteModelMutationPayload:
178
+ try:
179
+ model_id = from_global_id_with_expected_type(input.id, GenerativeModel.__name__)
180
+ except ValueError:
181
+ raise BadRequest(f'Invalid model id: "{input.id}"')
182
+
183
+ async with info.context.db() as session:
184
+ model = await session.scalar(
185
+ sa.update(models.GenerativeModel)
186
+ .values(deleted_at=datetime.now(timezone.utc))
187
+ .where(models.GenerativeModel.deleted_at.is_(None))
188
+ .where(models.GenerativeModel.id == model_id)
189
+ .returning(models.GenerativeModel)
190
+ )
191
+ if model is None:
192
+ raise NotFound(f'Model "{input.id}" not found')
193
+ if model.is_built_in:
194
+ await session.rollback()
195
+ raise BadRequest("Cannot delete built-in model")
196
+ return DeleteModelMutationPayload(
197
+ model=GenerativeModel(id=model.id, db_record=model),
198
+ query=Query(),
199
+ )
200
+
201
+
202
+ def _compile_regular_expression(maybe_regex: str) -> re.Pattern[str]:
203
+ """
204
+ Compile the given string as a regular expression.
205
+ Raises a BadRequest error if the given string is not a valid regex.
206
+ """
207
+ try:
208
+ return re.compile(maybe_regex)
209
+ except re.error as error:
210
+ raise BadRequest(f"Invalid regex: {str(error)}")
@@ -1,22 +1,58 @@
1
1
  import strawberry
2
2
  from sqlalchemy import delete, select
3
+ from sqlalchemy.exc import IntegrityError as PostgreSQLIntegrityError
3
4
  from sqlalchemy.orm import load_only
5
+ from sqlean.dbapi2 import IntegrityError as SQLiteIntegrityError # type: ignore[import-untyped]
4
6
  from strawberry.relay import GlobalID
5
7
  from strawberry.types import Info
6
8
 
7
9
  from phoenix.config import DEFAULT_PROJECT_NAME
8
10
  from phoenix.db import models
9
- from phoenix.server.api.auth import IsNotReadOnly
11
+ from phoenix.server.api.auth import IsNotReadOnly, IsNotViewer
10
12
  from phoenix.server.api.context import Context
13
+ from phoenix.server.api.exceptions import BadRequest, Conflict
11
14
  from phoenix.server.api.input_types.ClearProjectInput import ClearProjectInput
15
+ from phoenix.server.api.input_types.CreateProjectInput import CreateProjectInput
12
16
  from phoenix.server.api.queries import Query
13
17
  from phoenix.server.api.types.node import from_global_id_with_expected_type
14
- from phoenix.server.dml_event import ProjectDeleteEvent, SpanDeleteEvent
18
+ from phoenix.server.api.types.Project import Project, to_gql_project
19
+ from phoenix.server.dml_event import ProjectDeleteEvent, ProjectInsertEvent, SpanDeleteEvent
20
+
21
+
22
+ @strawberry.type
23
+ class ProjectMutationPayload:
24
+ project: Project
25
+ query: Query
15
26
 
16
27
 
17
28
  @strawberry.type
18
29
  class ProjectMutationMixin:
19
- @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
30
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
31
+ async def create_project(
32
+ self,
33
+ info: Info[Context, None],
34
+ input: CreateProjectInput,
35
+ ) -> ProjectMutationPayload:
36
+ if not (name := input.name.strip()):
37
+ raise BadRequest("Name cannot be empty")
38
+ description = (input.description or "").strip() or None
39
+ gradient_start_color = (input.gradient_start_color or "").strip() or None
40
+ gradient_end_color = (input.gradient_end_color or "").strip() or None
41
+ project = models.Project(
42
+ name=name,
43
+ description=description,
44
+ gradient_start_color=gradient_start_color,
45
+ gradient_end_color=gradient_end_color,
46
+ )
47
+ try:
48
+ async with info.context.db() as session:
49
+ session.add(project)
50
+ except (PostgreSQLIntegrityError, SQLiteIntegrityError):
51
+ raise Conflict(f"Project with name '{name}' already exists")
52
+ info.context.event_queue.put(ProjectInsertEvent((project.id,)))
53
+ return ProjectMutationPayload(project=to_gql_project(project), query=Query())
54
+
55
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
20
56
  async def delete_project(self, info: Info[Context, None], id: GlobalID) -> Query:
21
57
  project_id = from_global_id_with_expected_type(global_id=id, expected_type_name="Project")
22
58
  async with info.context.db() as session:
@@ -33,7 +69,7 @@ class ProjectMutationMixin:
33
69
  info.context.event_queue.put(ProjectDeleteEvent((project_id,)))
34
70
  return Query()
35
71
 
36
- @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
72
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
37
73
  async def clear_project(self, info: Info[Context, None], input: ClearProjectInput) -> Query:
38
74
  project_id = from_global_id_with_expected_type(
39
75
  global_id=input.id, expected_type_name="Project"
@@ -47,11 +83,14 @@ class ProjectMutationMixin:
47
83
  delete_statement = delete_statement.where(models.Trace.start_time < input.end_time)
48
84
  async with info.context.db() as session:
49
85
  deleted_trace_project_session_ids = await session.scalars(delete_statement)
50
- if deleted_trace_project_session_ids:
51
- await session.execute(
52
- delete(models.ProjectSession).where(
53
- models.ProjectSession.id.in_(set(deleted_trace_project_session_ids))
54
- )
55
- )
86
+ session_ids_to_delete = [
87
+ id_ for id_ in set(deleted_trace_project_session_ids) if id_ is not None
88
+ ]
89
+ # Process deletions in chunks of 10000 to avoid PostgreSQL argument limit
90
+ chunk_size = 10000
91
+ stmt = delete(models.ProjectSession)
92
+ for i in range(0, len(session_ids_to_delete), chunk_size):
93
+ chunk = session_ids_to_delete[i : i + chunk_size]
94
+ await session.execute(stmt.where(models.ProjectSession.id.in_(chunk)))
56
95
  info.context.event_queue.put(SpanDeleteEvent((project_id,)))
57
96
  return Query()
@@ -0,0 +1,158 @@
1
+ from typing import Optional
2
+
3
+ import strawberry
4
+ from sqlalchemy.exc import IntegrityError as PostgreSQLIntegrityError
5
+ from sqlean.dbapi2 import IntegrityError as SQLiteIntegrityError # type: ignore[import-untyped]
6
+ from starlette.requests import Request
7
+ from strawberry import Info
8
+ from strawberry.relay import GlobalID
9
+
10
+ from phoenix.db import models
11
+ from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
12
+ from phoenix.server.api.context import Context
13
+ from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound, Unauthorized
14
+ from phoenix.server.api.helpers.annotations import get_user_identifier
15
+ from phoenix.server.api.input_types.CreateProjectSessionAnnotationInput import (
16
+ CreateProjectSessionAnnotationInput,
17
+ )
18
+ from phoenix.server.api.input_types.UpdateAnnotationInput import UpdateAnnotationInput
19
+ from phoenix.server.api.queries import Query
20
+ from phoenix.server.api.types.AnnotationSource import AnnotationSource
21
+ from phoenix.server.api.types.node import from_global_id_with_expected_type
22
+ from phoenix.server.api.types.ProjectSessionAnnotation import ProjectSessionAnnotation
23
+ from phoenix.server.bearer_auth import PhoenixUser
24
+ from phoenix.server.dml_event import (
25
+ ProjectSessionAnnotationDeleteEvent,
26
+ ProjectSessionAnnotationInsertEvent,
27
+ )
28
+
29
+
30
+ @strawberry.type
31
+ class ProjectSessionAnnotationMutationPayload:
32
+ project_session_annotation: ProjectSessionAnnotation
33
+ query: Query
34
+
35
+
36
+ @strawberry.type
37
+ class ProjectSessionAnnotationMutationMixin:
38
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
39
+ async def create_project_session_annotations(
40
+ self, info: Info[Context, None], input: CreateProjectSessionAnnotationInput
41
+ ) -> ProjectSessionAnnotationMutationPayload:
42
+ assert isinstance(request := info.context.request, Request)
43
+ user_id: Optional[int] = None
44
+ if "user" in request.scope and isinstance((user := info.context.user), PhoenixUser):
45
+ user_id = int(user.identity)
46
+
47
+ try:
48
+ project_session_id = from_global_id_with_expected_type(
49
+ input.project_session_id, "ProjectSession"
50
+ )
51
+ except ValueError:
52
+ raise BadRequest(f"Invalid session ID: {input.project_session_id}")
53
+
54
+ identifier = ""
55
+ if isinstance(input.identifier, str):
56
+ identifier = input.identifier # Already trimmed in __post_init__
57
+ elif input.source == AnnotationSource.APP and user_id is not None:
58
+ identifier = get_user_identifier(user_id)
59
+
60
+ try:
61
+ async with info.context.db() as session:
62
+ anno = models.ProjectSessionAnnotation(
63
+ project_session_id=project_session_id,
64
+ name=input.name,
65
+ label=input.label,
66
+ score=input.score,
67
+ explanation=input.explanation,
68
+ annotator_kind=input.annotator_kind.value,
69
+ metadata_=input.metadata,
70
+ identifier=identifier,
71
+ source=input.source.value,
72
+ user_id=user_id,
73
+ )
74
+ session.add(anno)
75
+ except (PostgreSQLIntegrityError, SQLiteIntegrityError) as e:
76
+ raise Conflict(f"Error creating annotation: {e}")
77
+
78
+ info.context.event_queue.put(ProjectSessionAnnotationInsertEvent((anno.id,)))
79
+
80
+ return ProjectSessionAnnotationMutationPayload(
81
+ project_session_annotation=ProjectSessionAnnotation(id=anno.id, db_record=anno),
82
+ query=Query(),
83
+ )
84
+
85
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
86
+ async def update_project_session_annotations(
87
+ self, info: Info[Context, None], input: UpdateAnnotationInput
88
+ ) -> ProjectSessionAnnotationMutationPayload:
89
+ assert isinstance(request := info.context.request, Request)
90
+ user_id: Optional[int] = None
91
+ if "user" in request.scope and isinstance((user := info.context.user), PhoenixUser):
92
+ user_id = int(user.identity)
93
+
94
+ try:
95
+ id_ = from_global_id_with_expected_type(input.id, "ProjectSessionAnnotation")
96
+ except ValueError:
97
+ raise BadRequest(f"Invalid session annotation ID: {input.id}")
98
+
99
+ async with info.context.db() as session:
100
+ if not (anno := await session.get(models.ProjectSessionAnnotation, id_)):
101
+ raise NotFound(f"Could not find session annotation with ID: {input.id}")
102
+ if anno.user_id != user_id:
103
+ raise Unauthorized("Session annotation is not associated with the current user.")
104
+
105
+ # Update the annotation fields
106
+ anno.name = input.name
107
+ anno.label = input.label
108
+ anno.score = input.score
109
+ anno.explanation = input.explanation
110
+ anno.annotator_kind = input.annotator_kind.value
111
+ anno.metadata_ = input.metadata
112
+ anno.source = input.source.value
113
+
114
+ session.add(anno)
115
+ try:
116
+ await session.flush()
117
+ except (PostgreSQLIntegrityError, SQLiteIntegrityError) as e:
118
+ raise Conflict(f"Error updating annotation: {e}")
119
+
120
+ info.context.event_queue.put(ProjectSessionAnnotationInsertEvent((anno.id,)))
121
+ return ProjectSessionAnnotationMutationPayload(
122
+ project_session_annotation=ProjectSessionAnnotation(id=anno.id, db_record=anno),
123
+ query=Query(),
124
+ )
125
+
126
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
127
+ async def delete_project_session_annotation(
128
+ self, info: Info[Context, None], id: GlobalID
129
+ ) -> ProjectSessionAnnotationMutationPayload:
130
+ try:
131
+ id_ = from_global_id_with_expected_type(id, "ProjectSessionAnnotation")
132
+ except ValueError:
133
+ raise BadRequest(f"Invalid session annotation ID: {id}")
134
+
135
+ assert isinstance(request := info.context.request, Request)
136
+ user_id: Optional[int] = None
137
+ user_is_admin = False
138
+ if "user" in request.scope and isinstance((user := info.context.user), PhoenixUser):
139
+ user_id = int(user.identity)
140
+ user_is_admin = user.is_admin
141
+
142
+ async with info.context.db() as session:
143
+ if not (anno := await session.get(models.ProjectSessionAnnotation, id_)):
144
+ raise NotFound(f"Could not find session annotation with ID: {id}")
145
+
146
+ if not user_is_admin and anno.user_id != user_id:
147
+ raise Unauthorized(
148
+ "Session annotation is not associated with the current user and "
149
+ "the current user is not an admin."
150
+ )
151
+
152
+ await session.delete(anno)
153
+
154
+ deleted_gql_annotation = ProjectSessionAnnotation(id=anno.id, db_record=anno)
155
+ info.context.event_queue.put(ProjectSessionAnnotationDeleteEvent((id_,)))
156
+ return ProjectSessionAnnotationMutationPayload(
157
+ project_session_annotation=deleted_gql_annotation, query=Query()
158
+ )
@@ -16,7 +16,7 @@ from phoenix.db.types.trace_retention import (
16
16
  TraceRetentionCronExpression,
17
17
  TraceRetentionRule,
18
18
  )
19
- from phoenix.server.api.auth import IsAdminIfAuthEnabled, IsLocked, IsNotReadOnly
19
+ from phoenix.server.api.auth import IsAdminIfAuthEnabled, IsLocked, IsNotReadOnly, IsNotViewer
20
20
  from phoenix.server.api.context import Context
21
21
  from phoenix.server.api.exceptions import BadRequest, NotFound
22
22
  from phoenix.server.api.queries import Query
@@ -113,7 +113,9 @@ class ProjectTraceRetentionPolicyMutationPayload:
113
113
 
114
114
  @strawberry.type
115
115
  class ProjectTraceRetentionPolicyMutationMixin:
116
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdminIfAuthEnabled, IsLocked]) # type: ignore
116
+ @strawberry.mutation(
117
+ permission_classes=[IsNotReadOnly, IsNotViewer, IsAdminIfAuthEnabled, IsLocked]
118
+ ) # type: ignore
117
119
  async def create_project_trace_retention_policy(
118
120
  self,
119
121
  info: Info[Context, None],
@@ -146,7 +148,9 @@ class ProjectTraceRetentionPolicyMutationMixin:
146
148
  node=ProjectTraceRetentionPolicy(id=policy.id, db_policy=policy),
147
149
  )
148
150
 
149
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdminIfAuthEnabled, IsLocked]) # type: ignore
151
+ @strawberry.mutation(
152
+ permission_classes=[IsNotReadOnly, IsNotViewer, IsAdminIfAuthEnabled, IsLocked]
153
+ ) # type: ignore
150
154
  async def patch_project_trace_retention_policy(
151
155
  self,
152
156
  info: Info[Context, None],
@@ -204,7 +208,7 @@ class ProjectTraceRetentionPolicyMutationMixin:
204
208
  node=ProjectTraceRetentionPolicy(id=policy.id, db_policy=policy),
205
209
  )
206
210
 
207
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdminIfAuthEnabled]) # type: ignore
211
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdminIfAuthEnabled]) # type: ignore
208
212
  async def delete_project_trace_retention_policy(
209
213
  self,
210
214
  info: Info[Context, None],