arize-phoenix 11.23.1__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 (221) hide show
  1. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/METADATA +61 -36
  2. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/RECORD +212 -162
  3. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/WHEEL +1 -1
  4. {arize_phoenix-11.23.1.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 +2 -1
  12. phoenix/auth.py +27 -2
  13. phoenix/config.py +1594 -81
  14. phoenix/db/README.md +546 -28
  15. phoenix/db/bulk_inserter.py +119 -116
  16. phoenix/db/engines.py +140 -33
  17. phoenix/db/facilitator.py +22 -1
  18. phoenix/db/helpers.py +818 -65
  19. phoenix/db/iam_auth.py +64 -0
  20. phoenix/db/insertion/dataset.py +133 -1
  21. phoenix/db/insertion/document_annotation.py +9 -6
  22. phoenix/db/insertion/evaluation.py +2 -3
  23. phoenix/db/insertion/helpers.py +2 -2
  24. phoenix/db/insertion/session_annotation.py +176 -0
  25. phoenix/db/insertion/span_annotation.py +3 -4
  26. phoenix/db/insertion/trace_annotation.py +3 -4
  27. phoenix/db/insertion/types.py +41 -18
  28. phoenix/db/migrations/versions/01a8342c9cdf_add_user_id_on_datasets.py +40 -0
  29. phoenix/db/migrations/versions/0df286449799_add_session_annotations_table.py +105 -0
  30. phoenix/db/migrations/versions/272b66ff50f8_drop_single_indices.py +119 -0
  31. phoenix/db/migrations/versions/58228d933c91_dataset_labels.py +67 -0
  32. phoenix/db/migrations/versions/699f655af132_experiment_tags.py +57 -0
  33. phoenix/db/migrations/versions/735d3d93c33e_add_composite_indices.py +41 -0
  34. phoenix/db/migrations/versions/ab513d89518b_add_user_id_on_dataset_versions.py +40 -0
  35. phoenix/db/migrations/versions/d0690a79ea51_users_on_experiments.py +40 -0
  36. phoenix/db/migrations/versions/deb2c81c0bb2_dataset_splits.py +139 -0
  37. phoenix/db/migrations/versions/e76cbd66ffc3_add_experiments_dataset_examples.py +87 -0
  38. phoenix/db/models.py +364 -56
  39. phoenix/db/pg_config.py +10 -0
  40. phoenix/db/types/trace_retention.py +7 -6
  41. phoenix/experiments/functions.py +69 -19
  42. phoenix/inferences/inferences.py +1 -2
  43. phoenix/server/api/auth.py +9 -0
  44. phoenix/server/api/auth_messages.py +46 -0
  45. phoenix/server/api/context.py +60 -0
  46. phoenix/server/api/dataloaders/__init__.py +36 -0
  47. phoenix/server/api/dataloaders/annotation_summaries.py +60 -8
  48. phoenix/server/api/dataloaders/average_experiment_repeated_run_group_latency.py +50 -0
  49. phoenix/server/api/dataloaders/average_experiment_run_latency.py +17 -24
  50. phoenix/server/api/dataloaders/cache/two_tier_cache.py +1 -2
  51. phoenix/server/api/dataloaders/dataset_dataset_splits.py +52 -0
  52. phoenix/server/api/dataloaders/dataset_example_revisions.py +0 -1
  53. phoenix/server/api/dataloaders/dataset_example_splits.py +40 -0
  54. phoenix/server/api/dataloaders/dataset_examples_and_versions_by_experiment_run.py +47 -0
  55. phoenix/server/api/dataloaders/dataset_labels.py +36 -0
  56. phoenix/server/api/dataloaders/document_evaluation_summaries.py +2 -2
  57. phoenix/server/api/dataloaders/document_evaluations.py +6 -9
  58. phoenix/server/api/dataloaders/experiment_annotation_summaries.py +88 -34
  59. phoenix/server/api/dataloaders/experiment_dataset_splits.py +43 -0
  60. phoenix/server/api/dataloaders/experiment_error_rates.py +21 -28
  61. phoenix/server/api/dataloaders/experiment_repeated_run_group_annotation_summaries.py +77 -0
  62. phoenix/server/api/dataloaders/experiment_repeated_run_groups.py +57 -0
  63. phoenix/server/api/dataloaders/experiment_runs_by_experiment_and_example.py +44 -0
  64. phoenix/server/api/dataloaders/latency_ms_quantile.py +40 -8
  65. phoenix/server/api/dataloaders/record_counts.py +37 -10
  66. phoenix/server/api/dataloaders/session_annotations_by_session.py +29 -0
  67. phoenix/server/api/dataloaders/span_cost_summary_by_experiment_repeated_run_group.py +64 -0
  68. phoenix/server/api/dataloaders/span_cost_summary_by_project.py +28 -14
  69. phoenix/server/api/dataloaders/span_costs.py +3 -9
  70. phoenix/server/api/dataloaders/table_fields.py +2 -2
  71. phoenix/server/api/dataloaders/token_prices_by_model.py +30 -0
  72. phoenix/server/api/dataloaders/trace_annotations_by_trace.py +27 -0
  73. phoenix/server/api/exceptions.py +5 -1
  74. phoenix/server/api/helpers/playground_clients.py +263 -83
  75. phoenix/server/api/helpers/playground_spans.py +2 -1
  76. phoenix/server/api/helpers/playground_users.py +26 -0
  77. phoenix/server/api/helpers/prompts/conversions/google.py +103 -0
  78. phoenix/server/api/helpers/prompts/models.py +61 -19
  79. phoenix/server/api/input_types/{SpanAnnotationFilter.py → AnnotationFilter.py} +22 -14
  80. phoenix/server/api/input_types/ChatCompletionInput.py +3 -0
  81. phoenix/server/api/input_types/CreateProjectSessionAnnotationInput.py +37 -0
  82. phoenix/server/api/input_types/DatasetFilter.py +5 -2
  83. phoenix/server/api/input_types/ExperimentRunSort.py +237 -0
  84. phoenix/server/api/input_types/GenerativeModelInput.py +3 -0
  85. phoenix/server/api/input_types/ProjectSessionSort.py +158 -1
  86. phoenix/server/api/input_types/PromptVersionInput.py +47 -1
  87. phoenix/server/api/input_types/SpanSort.py +3 -2
  88. phoenix/server/api/input_types/UpdateAnnotationInput.py +34 -0
  89. phoenix/server/api/input_types/UserRoleInput.py +1 -0
  90. phoenix/server/api/mutations/__init__.py +8 -0
  91. phoenix/server/api/mutations/annotation_config_mutations.py +8 -8
  92. phoenix/server/api/mutations/api_key_mutations.py +15 -20
  93. phoenix/server/api/mutations/chat_mutations.py +106 -37
  94. phoenix/server/api/mutations/dataset_label_mutations.py +243 -0
  95. phoenix/server/api/mutations/dataset_mutations.py +21 -16
  96. phoenix/server/api/mutations/dataset_split_mutations.py +351 -0
  97. phoenix/server/api/mutations/experiment_mutations.py +2 -2
  98. phoenix/server/api/mutations/export_events_mutations.py +3 -3
  99. phoenix/server/api/mutations/model_mutations.py +11 -9
  100. phoenix/server/api/mutations/project_mutations.py +4 -4
  101. phoenix/server/api/mutations/project_session_annotations_mutations.py +158 -0
  102. phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +8 -4
  103. phoenix/server/api/mutations/prompt_label_mutations.py +74 -65
  104. phoenix/server/api/mutations/prompt_mutations.py +65 -129
  105. phoenix/server/api/mutations/prompt_version_tag_mutations.py +11 -8
  106. phoenix/server/api/mutations/span_annotations_mutations.py +15 -10
  107. phoenix/server/api/mutations/trace_annotations_mutations.py +13 -8
  108. phoenix/server/api/mutations/trace_mutations.py +3 -3
  109. phoenix/server/api/mutations/user_mutations.py +55 -26
  110. phoenix/server/api/queries.py +501 -617
  111. phoenix/server/api/routers/__init__.py +2 -2
  112. phoenix/server/api/routers/auth.py +141 -87
  113. phoenix/server/api/routers/ldap.py +229 -0
  114. phoenix/server/api/routers/oauth2.py +349 -101
  115. phoenix/server/api/routers/v1/__init__.py +22 -4
  116. phoenix/server/api/routers/v1/annotation_configs.py +19 -30
  117. phoenix/server/api/routers/v1/annotations.py +455 -13
  118. phoenix/server/api/routers/v1/datasets.py +355 -68
  119. phoenix/server/api/routers/v1/documents.py +142 -0
  120. phoenix/server/api/routers/v1/evaluations.py +20 -28
  121. phoenix/server/api/routers/v1/experiment_evaluations.py +16 -6
  122. phoenix/server/api/routers/v1/experiment_runs.py +335 -59
  123. phoenix/server/api/routers/v1/experiments.py +475 -47
  124. phoenix/server/api/routers/v1/projects.py +16 -50
  125. phoenix/server/api/routers/v1/prompts.py +50 -39
  126. phoenix/server/api/routers/v1/sessions.py +108 -0
  127. phoenix/server/api/routers/v1/spans.py +156 -96
  128. phoenix/server/api/routers/v1/traces.py +51 -77
  129. phoenix/server/api/routers/v1/users.py +64 -24
  130. phoenix/server/api/routers/v1/utils.py +3 -7
  131. phoenix/server/api/subscriptions.py +257 -93
  132. phoenix/server/api/types/Annotation.py +90 -23
  133. phoenix/server/api/types/ApiKey.py +13 -17
  134. phoenix/server/api/types/AuthMethod.py +1 -0
  135. phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +1 -0
  136. phoenix/server/api/types/Dataset.py +199 -72
  137. phoenix/server/api/types/DatasetExample.py +88 -18
  138. phoenix/server/api/types/DatasetExperimentAnnotationSummary.py +10 -0
  139. phoenix/server/api/types/DatasetLabel.py +57 -0
  140. phoenix/server/api/types/DatasetSplit.py +98 -0
  141. phoenix/server/api/types/DatasetVersion.py +49 -4
  142. phoenix/server/api/types/DocumentAnnotation.py +212 -0
  143. phoenix/server/api/types/Experiment.py +215 -68
  144. phoenix/server/api/types/ExperimentComparison.py +3 -9
  145. phoenix/server/api/types/ExperimentRepeatedRunGroup.py +155 -0
  146. phoenix/server/api/types/ExperimentRepeatedRunGroupAnnotationSummary.py +9 -0
  147. phoenix/server/api/types/ExperimentRun.py +120 -70
  148. phoenix/server/api/types/ExperimentRunAnnotation.py +158 -39
  149. phoenix/server/api/types/GenerativeModel.py +95 -42
  150. phoenix/server/api/types/GenerativeProvider.py +1 -1
  151. phoenix/server/api/types/ModelInterface.py +7 -2
  152. phoenix/server/api/types/PlaygroundModel.py +12 -2
  153. phoenix/server/api/types/Project.py +218 -185
  154. phoenix/server/api/types/ProjectSession.py +146 -29
  155. phoenix/server/api/types/ProjectSessionAnnotation.py +187 -0
  156. phoenix/server/api/types/ProjectTraceRetentionPolicy.py +1 -1
  157. phoenix/server/api/types/Prompt.py +119 -39
  158. phoenix/server/api/types/PromptLabel.py +42 -25
  159. phoenix/server/api/types/PromptVersion.py +11 -8
  160. phoenix/server/api/types/PromptVersionTag.py +65 -25
  161. phoenix/server/api/types/Span.py +130 -123
  162. phoenix/server/api/types/SpanAnnotation.py +189 -42
  163. phoenix/server/api/types/SystemApiKey.py +65 -1
  164. phoenix/server/api/types/Trace.py +184 -53
  165. phoenix/server/api/types/TraceAnnotation.py +149 -50
  166. phoenix/server/api/types/User.py +128 -33
  167. phoenix/server/api/types/UserApiKey.py +73 -26
  168. phoenix/server/api/types/node.py +10 -0
  169. phoenix/server/api/types/pagination.py +11 -2
  170. phoenix/server/app.py +154 -36
  171. phoenix/server/authorization.py +5 -4
  172. phoenix/server/bearer_auth.py +13 -5
  173. phoenix/server/cost_tracking/cost_model_lookup.py +42 -14
  174. phoenix/server/cost_tracking/model_cost_manifest.json +1085 -194
  175. phoenix/server/daemons/generative_model_store.py +61 -9
  176. phoenix/server/daemons/span_cost_calculator.py +10 -8
  177. phoenix/server/dml_event.py +13 -0
  178. phoenix/server/email/sender.py +29 -2
  179. phoenix/server/grpc_server.py +9 -9
  180. phoenix/server/jwt_store.py +8 -6
  181. phoenix/server/ldap.py +1449 -0
  182. phoenix/server/main.py +9 -3
  183. phoenix/server/oauth2.py +330 -12
  184. phoenix/server/prometheus.py +43 -6
  185. phoenix/server/rate_limiters.py +4 -9
  186. phoenix/server/retention.py +33 -20
  187. phoenix/server/session_filters.py +49 -0
  188. phoenix/server/static/.vite/manifest.json +51 -53
  189. phoenix/server/static/assets/components-BreFUQQa.js +6702 -0
  190. phoenix/server/static/assets/{index-BPCwGQr8.js → index-CTQoemZv.js} +42 -35
  191. phoenix/server/static/assets/pages-DBE5iYM3.js +9524 -0
  192. phoenix/server/static/assets/vendor-BGzfc4EU.css +1 -0
  193. phoenix/server/static/assets/vendor-DCE4v-Ot.js +920 -0
  194. phoenix/server/static/assets/vendor-codemirror-D5f205eT.js +25 -0
  195. phoenix/server/static/assets/{vendor-recharts-Bw30oz1A.js → vendor-recharts-V9cwpXsm.js} +7 -7
  196. phoenix/server/static/assets/{vendor-shiki-DZajAPeq.js → vendor-shiki-Do--csgv.js} +1 -1
  197. phoenix/server/static/assets/vendor-three-CmB8bl_y.js +3840 -0
  198. phoenix/server/templates/index.html +7 -1
  199. phoenix/server/thread_server.py +1 -2
  200. phoenix/server/utils.py +74 -0
  201. phoenix/session/client.py +55 -1
  202. phoenix/session/data_extractor.py +5 -0
  203. phoenix/session/evaluation.py +8 -4
  204. phoenix/session/session.py +44 -8
  205. phoenix/settings.py +2 -0
  206. phoenix/trace/attributes.py +80 -13
  207. phoenix/trace/dsl/query.py +2 -0
  208. phoenix/trace/projects.py +5 -0
  209. phoenix/utilities/template_formatters.py +1 -1
  210. phoenix/version.py +1 -1
  211. phoenix/server/api/types/Evaluation.py +0 -39
  212. phoenix/server/static/assets/components-D0DWAf0l.js +0 -5650
  213. phoenix/server/static/assets/pages-Creyamao.js +0 -8612
  214. phoenix/server/static/assets/vendor-CU36oj8y.js +0 -905
  215. phoenix/server/static/assets/vendor-CqDb5u4o.css +0 -1
  216. phoenix/server/static/assets/vendor-arizeai-Ctgw0e1G.js +0 -168
  217. phoenix/server/static/assets/vendor-codemirror-Cojjzqb9.js +0 -25
  218. phoenix/server/static/assets/vendor-three-BLWp5bic.js +0 -2998
  219. phoenix/utilities/deprecation.py +0 -31
  220. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/entry_points.txt +0 -0
  221. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
1
- from typing import Any, Optional, Union, cast
1
+ from typing import Any, Optional
2
2
 
3
3
  import strawberry
4
4
  from fastapi import Request
@@ -7,23 +7,17 @@ from sqlalchemy import delete, select, update
7
7
  from sqlalchemy.exc import IntegrityError as PostgreSQLIntegrityError
8
8
  from sqlalchemy.orm import joinedload
9
9
  from sqlean.dbapi2 import IntegrityError as SQLiteIntegrityError # type: ignore[import-untyped]
10
+ from strawberry import UNSET
10
11
  from strawberry.relay.types import GlobalID
11
12
  from strawberry.types import Info
12
13
 
13
14
  from phoenix.db import models
14
15
  from phoenix.db.types.identifier import Identifier as IdentifierModel
15
- from phoenix.db.types.model_provider import ModelProvider
16
- from phoenix.server.api.auth import IsLocked, IsNotReadOnly
16
+ from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
17
17
  from phoenix.server.api.context import Context
18
18
  from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound
19
- from phoenix.server.api.helpers.prompts.models import (
20
- normalize_response_format,
21
- normalize_tools,
22
- validate_invocation_parameters,
23
- )
24
19
  from phoenix.server.api.input_types.PromptVersionInput import (
25
20
  ChatPromptVersionInput,
26
- to_pydantic_prompt_chat_template_v1,
27
21
  )
28
22
  from phoenix.server.api.mutations.prompt_version_tag_mutations import (
29
23
  SetPromptVersionTagInput,
@@ -32,7 +26,7 @@ from phoenix.server.api.mutations.prompt_version_tag_mutations import (
32
26
  from phoenix.server.api.queries import Query
33
27
  from phoenix.server.api.types.Identifier import Identifier
34
28
  from phoenix.server.api.types.node import from_global_id_with_expected_type
35
- from phoenix.server.api.types.Prompt import Prompt, to_gql_prompt_from_orm
29
+ from phoenix.server.api.types.Prompt import Prompt
36
30
  from phoenix.server.bearer_auth import PhoenixUser
37
31
 
38
32
 
@@ -41,6 +35,7 @@ class CreateChatPromptInput:
41
35
  name: Identifier
42
36
  description: Optional[str] = None
43
37
  prompt_version: ChatPromptVersionInput
38
+ metadata: Optional[strawberry.scalars.JSON] = None
44
39
 
45
40
 
46
41
  @strawberry.input
@@ -58,14 +53,16 @@ class DeletePromptInput:
58
53
  @strawberry.input
59
54
  class ClonePromptInput:
60
55
  name: Identifier
61
- description: Optional[str] = None
62
56
  prompt_id: GlobalID
57
+ description: Optional[str] = UNSET
58
+ metadata: Optional[strawberry.scalars.JSON] = UNSET
63
59
 
64
60
 
65
61
  @strawberry.input
66
62
  class PatchPromptInput:
67
63
  prompt_id: GlobalID
68
- description: str
64
+ description: Optional[str] = UNSET
65
+ metadata: Optional[strawberry.scalars.JSON] = UNSET
69
66
 
70
67
 
71
68
  @strawberry.type
@@ -75,7 +72,7 @@ class DeletePromptMutationPayload:
75
72
 
76
73
  @strawberry.type
77
74
  class PromptMutationMixin:
78
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
75
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
79
76
  async def create_chat_prompt(
80
77
  self, info: Info[Context, None], input: CreateChatPromptInput
81
78
  ) -> Prompt:
@@ -84,65 +81,26 @@ class PromptMutationMixin:
84
81
  if "user" in request.scope:
85
82
  assert isinstance(user := request.user, PhoenixUser)
86
83
  user_id = int(user.identity)
87
-
88
- input_prompt_version = input.prompt_version
89
- tool_definitions = [tool.definition for tool in input_prompt_version.tools]
90
- tool_choice = cast(
91
- Optional[Union[str, dict[str, Any]]],
92
- cast(dict[str, Any], input.prompt_version.invocation_parameters).pop(
93
- "tool_choice", None
94
- ),
95
- )
96
- model_provider = ModelProvider(input_prompt_version.model_provider)
97
84
  try:
98
- tools = (
99
- normalize_tools(tool_definitions, model_provider, tool_choice)
100
- if tool_definitions
101
- else None
102
- )
103
- template = to_pydantic_prompt_chat_template_v1(input_prompt_version.template)
104
- response_format = (
105
- normalize_response_format(
106
- input_prompt_version.response_format.definition,
107
- model_provider,
108
- )
109
- if input_prompt_version.response_format
110
- else None
111
- )
112
- invocation_parameters = validate_invocation_parameters(
113
- input_prompt_version.invocation_parameters,
114
- model_provider,
115
- )
85
+ prompt_version = input.prompt_version.to_orm_prompt_version(user_id)
116
86
  except ValidationError as error:
117
87
  raise BadRequest(str(error))
118
-
88
+ name = IdentifierModel.model_validate(str(input.name))
89
+ prompt = models.Prompt(
90
+ name=name,
91
+ description=input.description,
92
+ metadata_=input.metadata or {},
93
+ prompt_versions=[prompt_version],
94
+ )
119
95
  async with info.context.db() as session:
120
- prompt_version = models.PromptVersion(
121
- description=input_prompt_version.description,
122
- user_id=user_id,
123
- template_type="CHAT",
124
- template_format=input_prompt_version.template_format,
125
- template=template,
126
- invocation_parameters=invocation_parameters,
127
- tools=tools,
128
- response_format=response_format,
129
- model_provider=input_prompt_version.model_provider,
130
- model_name=input_prompt_version.model_name,
131
- )
132
- name = IdentifierModel.model_validate(str(input.name))
133
- prompt = models.Prompt(
134
- name=name,
135
- description=input.description,
136
- prompt_versions=[prompt_version],
137
- )
138
96
  session.add(prompt)
139
97
  try:
140
98
  await session.commit()
141
99
  except (PostgreSQLIntegrityError, SQLiteIntegrityError):
142
100
  raise Conflict(f"A prompt named '{input.name}' already exists")
143
- return to_gql_prompt_from_orm(prompt)
101
+ return Prompt(id=prompt.id, db_record=prompt)
144
102
 
145
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
103
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
146
104
  async def create_chat_prompt_version(
147
105
  self,
148
106
  info: Info[Context, None],
@@ -153,74 +111,28 @@ class PromptMutationMixin:
153
111
  if "user" in request.scope:
154
112
  assert isinstance(user := request.user, PhoenixUser)
155
113
  user_id = int(user.identity)
156
-
157
- input_prompt_version = input.prompt_version
158
- tool_definitions = [tool.definition for tool in input.prompt_version.tools]
159
- tool_choice = cast(
160
- Optional[Union[str, dict[str, Any]]],
161
- cast(dict[str, Any], input.prompt_version.invocation_parameters).pop(
162
- "tool_choice", None
163
- ),
164
- )
165
- model_provider = ModelProvider(input_prompt_version.model_provider)
166
114
  try:
167
- tools = (
168
- normalize_tools(tool_definitions, model_provider, tool_choice)
169
- if tool_definitions
170
- else None
171
- )
172
- template = to_pydantic_prompt_chat_template_v1(input_prompt_version.template)
173
- response_format = (
174
- normalize_response_format(
175
- input_prompt_version.response_format.definition,
176
- model_provider,
177
- )
178
- if input_prompt_version.response_format
179
- else None
180
- )
181
- invocation_parameters = validate_invocation_parameters(
182
- input_prompt_version.invocation_parameters,
183
- model_provider,
184
- )
115
+ prompt_version = input.prompt_version.to_orm_prompt_version(user_id)
185
116
  except ValidationError as error:
186
117
  raise BadRequest(str(error))
187
-
188
118
  prompt_id = from_global_id_with_expected_type(
189
119
  global_id=input.prompt_id, expected_type_name=Prompt.__name__
190
120
  )
121
+ prompt_version.prompt_id = prompt_id
191
122
  async with info.context.db() as session:
192
- prompt = await session.get(models.Prompt, prompt_id)
193
- if not prompt:
194
- raise NotFound(f"Prompt with ID '{input.prompt_id}' not found")
195
-
196
- prompt_version = models.PromptVersion(
197
- prompt_id=prompt_id,
198
- description=input.prompt_version.description,
199
- user_id=user_id,
200
- template_type="CHAT",
201
- template_format=input.prompt_version.template_format,
202
- template=template,
203
- invocation_parameters=invocation_parameters,
204
- tools=tools,
205
- response_format=response_format,
206
- model_provider=input.prompt_version.model_provider,
207
- model_name=input.prompt_version.model_name,
208
- )
209
123
  session.add(prompt_version)
210
-
211
- # ensure prompt_version is flushed to the database before creating tags against the
212
- # prompt_version id
213
- await session.flush()
214
-
215
- if input.tags:
216
- for tag in input.tags:
217
- await upsert_prompt_version_tag(
218
- session, prompt_id, prompt_version.id, tag.name, tag.description
219
- )
220
-
221
- return to_gql_prompt_from_orm(prompt)
222
-
223
- @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
124
+ try:
125
+ await session.flush()
126
+ except (PostgreSQLIntegrityError, SQLiteIntegrityError):
127
+ raise NotFound(f"Prompt with ID '{input.prompt_id}' not found")
128
+ if input.tags:
129
+ for tag in input.tags:
130
+ await upsert_prompt_version_tag(
131
+ session, prompt_id, prompt_version.id, tag.name, tag.description
132
+ )
133
+ return Prompt(id=prompt_id)
134
+
135
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
224
136
  async def delete_prompt(
225
137
  self, info: Info[Context, None], input: DeletePromptInput
226
138
  ) -> DeletePromptMutationPayload:
@@ -231,13 +143,13 @@ class PromptMutationMixin:
231
143
  stmt = delete(models.Prompt).where(models.Prompt.id == prompt_id)
232
144
  result = await session.execute(stmt)
233
145
 
234
- if result.rowcount == 0:
146
+ if result.rowcount == 0: # type: ignore[attr-defined]
235
147
  raise NotFound(f"Prompt with ID '{input.prompt_id}' not found")
236
148
 
237
149
  await session.commit()
238
150
  return DeletePromptMutationPayload(query=Query())
239
151
 
240
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
152
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
241
153
  async def clone_prompt(self, info: Info[Context, None], input: ClonePromptInput) -> Prompt:
242
154
  prompt_id = from_global_id_with_expected_type(
243
155
  global_id=input.prompt_id, expected_type_name=Prompt.__name__
@@ -256,10 +168,23 @@ class PromptMutationMixin:
256
168
 
257
169
  # Create new prompt
258
170
  name = IdentifierModel.model_validate(str(input.name))
171
+ # Handle description: inherit if UNSET, otherwise use value (can be None)
172
+ if input.description is UNSET:
173
+ description = prompt.description
174
+ else:
175
+ description = input.description.strip() if input.description is not None else None
176
+
177
+ # Handle metadata: inherit if UNSET, clear to empty dict if None, or use value
178
+ if input.metadata is UNSET:
179
+ metadata = prompt.metadata_
180
+ else:
181
+ metadata = input.metadata or {}
182
+
259
183
  new_prompt = models.Prompt(
260
184
  name=name,
261
- description=input.description,
262
185
  source_prompt_id=prompt_id,
186
+ description=description,
187
+ metadata_=metadata,
263
188
  )
264
189
 
265
190
  # Create copies of all versions
@@ -288,19 +213,30 @@ class PromptMutationMixin:
288
213
  await session.commit()
289
214
  except (PostgreSQLIntegrityError, SQLiteIntegrityError):
290
215
  raise Conflict(f"A prompt named '{input.name}' already exists")
291
- return to_gql_prompt_from_orm(new_prompt)
216
+ return Prompt(id=new_prompt.id, db_record=new_prompt)
292
217
 
293
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
218
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
294
219
  async def patch_prompt(self, info: Info[Context, None], input: PatchPromptInput) -> Prompt:
295
220
  prompt_id = from_global_id_with_expected_type(
296
221
  global_id=input.prompt_id, expected_type_name=Prompt.__name__
297
222
  )
298
223
 
224
+ values: dict[str, Any] = {}
225
+ if input.description is not UNSET:
226
+ values["description"] = (
227
+ input.description.strip() if input.description is not None else None
228
+ )
229
+ if input.metadata is not UNSET:
230
+ values["metadata_"] = input.metadata or {}
231
+
232
+ if not values:
233
+ raise BadRequest("No fields provided to update")
234
+
299
235
  async with info.context.db() as session:
300
236
  stmt = (
301
237
  update(models.Prompt)
302
238
  .where(models.Prompt.id == prompt_id)
303
- .values(description=input.description)
239
+ .values(**values)
304
240
  .returning(models.Prompt)
305
241
  )
306
242
 
@@ -310,4 +246,4 @@ class PromptMutationMixin:
310
246
  if prompt is None:
311
247
  raise NotFound(f"Prompt with ID '{input.prompt_id}' not found")
312
248
 
313
- return to_gql_prompt_from_orm(prompt)
249
+ return Prompt(id=prompt.id, db_record=prompt)
@@ -10,15 +10,15 @@ from strawberry.types import Info
10
10
 
11
11
  from phoenix.db import models
12
12
  from phoenix.db.types.identifier import Identifier as IdentifierModel
13
- from phoenix.server.api.auth import IsLocked, IsNotReadOnly
13
+ from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
14
14
  from phoenix.server.api.context import Context
15
15
  from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound
16
16
  from phoenix.server.api.queries import Query
17
17
  from phoenix.server.api.types.Identifier import Identifier
18
18
  from phoenix.server.api.types.node import from_global_id_with_expected_type
19
- from phoenix.server.api.types.Prompt import Prompt, to_gql_prompt_from_orm
19
+ from phoenix.server.api.types.Prompt import Prompt
20
20
  from phoenix.server.api.types.PromptVersion import PromptVersion
21
- from phoenix.server.api.types.PromptVersionTag import PromptVersionTag, to_gql_prompt_version_tag
21
+ from phoenix.server.api.types.PromptVersionTag import PromptVersionTag
22
22
 
23
23
 
24
24
  @strawberry.input
@@ -42,7 +42,7 @@ class PromptVersionTagMutationPayload:
42
42
 
43
43
  @strawberry.type
44
44
  class PromptVersionTagMutationMixin:
45
- @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
45
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
46
46
  async def delete_prompt_version_tag(
47
47
  self, info: Info[Context, None], input: DeletePromptVersionTagInput
48
48
  ) -> PromptVersionTagMutationPayload:
@@ -75,10 +75,12 @@ class PromptVersionTagMutationMixin:
75
75
  await session.delete(prompt_version_tag)
76
76
  await session.commit()
77
77
  return PromptVersionTagMutationPayload(
78
- prompt_version_tag=None, query=Query(), prompt=to_gql_prompt_from_orm(prompt)
78
+ prompt_version_tag=None,
79
+ query=Query(),
80
+ prompt=Prompt(id=prompt.id, db_record=prompt),
79
81
  )
80
82
 
81
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
83
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
82
84
  async def set_prompt_version_tag(
83
85
  self, info: Info[Context, None], input: SetPromptVersionTagInput
84
86
  ) -> PromptVersionTagMutationPayload:
@@ -111,9 +113,10 @@ class PromptVersionTagMutationMixin:
111
113
  except (PostgreSQLIntegrityError, SQLiteIntegrityError):
112
114
  raise Conflict("Failed to update PromptVersionTag.")
113
115
 
114
- version_tag = to_gql_prompt_version_tag(updated_tag)
115
116
  return PromptVersionTagMutationPayload(
116
- prompt_version_tag=version_tag, prompt=to_gql_prompt_from_orm(prompt), query=Query()
117
+ prompt_version_tag=PromptVersionTag(id=updated_tag.id, db_record=updated_tag),
118
+ prompt=Prompt(id=prompt.id, db_record=prompt),
119
+ query=Query(),
117
120
  )
118
121
 
119
122
 
@@ -7,7 +7,7 @@ from starlette.requests import Request
7
7
  from strawberry import UNSET, Info
8
8
 
9
9
  from phoenix.db import models
10
- from phoenix.server.api.auth import IsLocked, IsNotReadOnly
10
+ from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
11
11
  from phoenix.server.api.context import Context
12
12
  from phoenix.server.api.exceptions import BadRequest, NotFound, Unauthorized
13
13
  from phoenix.server.api.helpers.annotations import get_user_identifier
@@ -21,7 +21,7 @@ from phoenix.server.api.queries import Query
21
21
  from phoenix.server.api.types.AnnotationSource import AnnotationSource
22
22
  from phoenix.server.api.types.AnnotatorKind import AnnotatorKind
23
23
  from phoenix.server.api.types.node import from_global_id_with_expected_type
24
- from phoenix.server.api.types.SpanAnnotation import SpanAnnotation, to_gql_span_annotation
24
+ from phoenix.server.api.types.SpanAnnotation import SpanAnnotation
25
25
  from phoenix.server.bearer_auth import PhoenixUser
26
26
  from phoenix.server.dml_event import SpanAnnotationDeleteEvent, SpanAnnotationInsertEvent
27
27
 
@@ -34,7 +34,7 @@ class SpanAnnotationMutationPayload:
34
34
 
35
35
  @strawberry.type
36
36
  class SpanAnnotationMutationMixin:
37
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
37
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
38
38
  async def create_span_annotations(
39
39
  self, info: Info[Context, None], input: list[CreateSpanAnnotationInput]
40
40
  ) -> SpanAnnotationMutationPayload:
@@ -138,7 +138,7 @@ class SpanAnnotationMutationMixin:
138
138
 
139
139
  # Convert the fully loaded annotations to GQL types
140
140
  returned_annotations = [
141
- to_gql_span_annotation(anno) for anno in ordered_final_annotations
141
+ SpanAnnotation(id=anno.id, db_record=anno) for anno in ordered_final_annotations
142
142
  ]
143
143
 
144
144
  await session.commit()
@@ -148,7 +148,7 @@ class SpanAnnotationMutationMixin:
148
148
  query=Query(),
149
149
  )
150
150
 
151
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
151
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
152
152
  async def create_span_note(
153
153
  self, info: Info[Context, None], annotation_input: CreateSpanNoteInput
154
154
  ) -> SpanAnnotationMutationPayload:
@@ -184,14 +184,16 @@ class SpanAnnotationMutationMixin:
184
184
  processed_annotation = result.one()
185
185
 
186
186
  info.context.event_queue.put(SpanAnnotationInsertEvent((processed_annotation.id,)))
187
- returned_annotation = to_gql_span_annotation(processed_annotation)
187
+ returned_annotation = SpanAnnotation(
188
+ id=processed_annotation.id, db_record=processed_annotation
189
+ )
188
190
  await session.commit()
189
191
  return SpanAnnotationMutationPayload(
190
192
  span_annotations=[returned_annotation],
191
193
  query=Query(),
192
194
  )
193
195
 
194
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
196
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
195
197
  async def patch_span_annotations(
196
198
  self, info: Info[Context, None], input: list[PatchAnnotationInput]
197
199
  ) -> SpanAnnotationMutationPayload:
@@ -256,7 +258,7 @@ class SpanAnnotationMutationMixin:
256
258
  session.add(span_annotation)
257
259
 
258
260
  patched_annotations = [
259
- to_gql_span_annotation(span_annotation)
261
+ SpanAnnotation(id=span_annotation.id, db_record=span_annotation)
260
262
  for span_annotation in span_annotations_by_id.values()
261
263
  ]
262
264
 
@@ -268,7 +270,7 @@ class SpanAnnotationMutationMixin:
268
270
  query=Query(),
269
271
  )
270
272
 
271
- @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
273
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
272
274
  async def delete_span_annotations(
273
275
  self, info: Info[Context, None], input: DeleteAnnotationsInput
274
276
  ) -> SpanAnnotationMutationPayload:
@@ -320,7 +322,10 @@ class SpanAnnotationMutationMixin:
320
322
  )
321
323
 
322
324
  deleted_annotations_gql = [
323
- to_gql_span_annotation(deleted_annotations_by_id[id]) for id in span_annotation_ids
325
+ SpanAnnotation(
326
+ id=deleted_annotations_by_id[id].id, db_record=deleted_annotations_by_id[id]
327
+ )
328
+ for id in span_annotation_ids
324
329
  ]
325
330
  info.context.event_queue.put(
326
331
  SpanAnnotationDeleteEvent(tuple(deleted_annotations_by_id.keys()))
@@ -6,7 +6,7 @@ from starlette.requests import Request
6
6
  from strawberry import UNSET, Info
7
7
 
8
8
  from phoenix.db import models
9
- from phoenix.server.api.auth import IsLocked, IsNotReadOnly
9
+ from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
10
10
  from phoenix.server.api.context import Context
11
11
  from phoenix.server.api.exceptions import BadRequest, NotFound, Unauthorized
12
12
  from phoenix.server.api.helpers.annotations import get_user_identifier
@@ -16,7 +16,7 @@ from phoenix.server.api.input_types.PatchAnnotationInput import PatchAnnotationI
16
16
  from phoenix.server.api.queries import Query
17
17
  from phoenix.server.api.types.AnnotationSource import AnnotationSource
18
18
  from phoenix.server.api.types.node import from_global_id_with_expected_type
19
- from phoenix.server.api.types.TraceAnnotation import TraceAnnotation, to_gql_trace_annotation
19
+ from phoenix.server.api.types.TraceAnnotation import TraceAnnotation
20
20
  from phoenix.server.bearer_auth import PhoenixUser
21
21
  from phoenix.server.dml_event import TraceAnnotationDeleteEvent, TraceAnnotationInsertEvent
22
22
 
@@ -29,7 +29,7 @@ class TraceAnnotationMutationPayload:
29
29
 
30
30
  @strawberry.type
31
31
  class TraceAnnotationMutationMixin:
32
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
32
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
33
33
  async def create_trace_annotations(
34
34
  self, info: Info[Context, None], input: list[CreateTraceAnnotationInput]
35
35
  ) -> TraceAnnotationMutationPayload:
@@ -111,7 +111,9 @@ class TraceAnnotationMutationMixin:
111
111
  info.context.event_queue.put(TraceAnnotationInsertEvent(inserted_annotation_ids))
112
112
 
113
113
  returned_annotations = [
114
- to_gql_trace_annotation(processed_annotations_map[i])
114
+ TraceAnnotation(
115
+ id=processed_annotations_map[i].id, db_record=processed_annotations_map[i]
116
+ )
115
117
  for i in sorted(processed_annotations_map.keys())
116
118
  ]
117
119
 
@@ -120,7 +122,7 @@ class TraceAnnotationMutationMixin:
120
122
  query=Query(),
121
123
  )
122
124
 
123
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
125
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
124
126
  async def patch_trace_annotations(
125
127
  self, info: Info[Context, None], input: list[PatchAnnotationInput]
126
128
  ) -> TraceAnnotationMutationPayload:
@@ -186,7 +188,7 @@ class TraceAnnotationMutationMixin:
186
188
  await session.commit()
187
189
 
188
190
  patched_annotations = [
189
- to_gql_trace_annotation(trace_annotation)
191
+ TraceAnnotation(id=trace_annotation.id, db_record=trace_annotation)
190
192
  for trace_annotation in trace_annotations_by_id.values()
191
193
  ]
192
194
  info.context.event_queue.put(TraceAnnotationInsertEvent(tuple(patch_by_id.keys())))
@@ -195,7 +197,7 @@ class TraceAnnotationMutationMixin:
195
197
  query=Query(),
196
198
  )
197
199
 
198
- @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
200
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
199
201
  async def delete_trace_annotations(
200
202
  self, info: Info[Context, None], input: DeleteAnnotationsInput
201
203
  ) -> TraceAnnotationMutationPayload:
@@ -245,7 +247,10 @@ class TraceAnnotationMutationMixin:
245
247
  )
246
248
 
247
249
  deleted_gql_annotations = [
248
- to_gql_trace_annotation(deleted_annotations_by_id[id]) for id in trace_annotation_ids
250
+ TraceAnnotation(
251
+ id=deleted_annotations_by_id[id].id, db_record=deleted_annotations_by_id[id]
252
+ )
253
+ for id in trace_annotation_ids
249
254
  ]
250
255
  info.context.event_queue.put(
251
256
  TraceAnnotationDeleteEvent(tuple(deleted_annotations_by_id.keys()))
@@ -6,7 +6,7 @@ from strawberry.relay import GlobalID
6
6
  from strawberry.types import Info
7
7
 
8
8
  from phoenix.db import models
9
- from phoenix.server.api.auth import IsNotReadOnly
9
+ from phoenix.server.api.auth import IsNotReadOnly, IsNotViewer
10
10
  from phoenix.server.api.context import Context
11
11
  from phoenix.server.api.exceptions import BadRequest
12
12
  from phoenix.server.api.queries import Query
@@ -16,7 +16,7 @@ from phoenix.server.dml_event import SpanDeleteEvent
16
16
 
17
17
  @strawberry.type
18
18
  class TraceMutationMixin:
19
- @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
19
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
20
20
  async def delete_traces(
21
21
  self,
22
22
  info: Info[Context, None],
@@ -73,7 +73,7 @@ class TraceMutationMixin:
73
73
  info.context.event_queue.put(SpanDeleteEvent(project_ids))
74
74
  return Query()
75
75
 
76
- @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
76
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
77
77
  async def transfer_traces_to_project(
78
78
  self,
79
79
  info: Info[Context, None],