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
@@ -3,58 +3,163 @@ from typing import Optional
3
3
 
4
4
  import strawberry
5
5
  from sqlalchemy import select
6
- from strawberry import Private
7
6
  from strawberry.relay import Node, NodeID
8
7
  from strawberry.types import Info
9
8
 
9
+ from phoenix.config import get_env_admins
10
10
  from phoenix.db import models
11
11
  from phoenix.server.api.context import Context
12
12
  from phoenix.server.api.exceptions import NotFound
13
13
  from phoenix.server.api.types.AuthMethod import AuthMethod
14
- from phoenix.server.api.types.UserApiKey import UserApiKey, to_gql_api_key
14
+ from phoenix.server.api.types.UserApiKey import UserApiKey
15
+ from phoenix.server.ldap import is_null_email_marker
15
16
 
16
17
  from .UserRole import UserRole, to_gql_user_role
17
18
 
18
19
 
19
20
  @strawberry.type
20
21
  class User(Node):
21
- id_attr: NodeID[int]
22
- password_needs_reset: bool
23
- email: str
24
- username: str
25
- profile_picture_url: Optional[str]
26
- created_at: datetime
27
- user_role_id: Private[int]
28
- auth_method: AuthMethod
22
+ id: NodeID[int]
23
+ db_record: strawberry.Private[Optional[models.User]] = None
24
+
25
+ def __post_init__(self) -> None:
26
+ if self.db_record and self.id != self.db_record.id:
27
+ raise ValueError("User ID mismatch")
28
+
29
+ @strawberry.field
30
+ async def password_needs_reset(
31
+ self,
32
+ info: Info[Context, None],
33
+ ) -> bool:
34
+ if self.db_record:
35
+ val = self.db_record.reset_password
36
+ else:
37
+ val = await info.context.data_loaders.user_fields.load(
38
+ (self.id, models.User.reset_password),
39
+ )
40
+ return val
41
+
42
+ @strawberry.field
43
+ async def email(
44
+ self,
45
+ info: Info[Context, None],
46
+ ) -> str | None:
47
+ if self.db_record:
48
+ val = self.db_record.email
49
+ else:
50
+ val = await info.context.data_loaders.user_fields.load(
51
+ (self.id, models.User.email),
52
+ )
53
+ if is_null_email_marker(val):
54
+ return None
55
+ return val
56
+
57
+ @strawberry.field
58
+ async def username(
59
+ self,
60
+ info: Info[Context, None],
61
+ ) -> str:
62
+ if self.db_record:
63
+ val = self.db_record.username
64
+ else:
65
+ val = await info.context.data_loaders.user_fields.load(
66
+ (self.id, models.User.username),
67
+ )
68
+ return val
69
+
70
+ @strawberry.field
71
+ async def profile_picture_url(
72
+ self,
73
+ info: Info[Context, None],
74
+ ) -> Optional[str]:
75
+ if self.db_record:
76
+ val = self.db_record.profile_picture_url
77
+ else:
78
+ val = await info.context.data_loaders.user_fields.load(
79
+ (self.id, models.User.profile_picture_url),
80
+ )
81
+ return val
82
+
83
+ @strawberry.field
84
+ async def created_at(
85
+ self,
86
+ info: Info[Context, None],
87
+ ) -> datetime:
88
+ if self.db_record:
89
+ val = self.db_record.created_at
90
+ else:
91
+ val = await info.context.data_loaders.user_fields.load(
92
+ (self.id, models.User.created_at),
93
+ )
94
+ return val
95
+
96
+ @strawberry.field
97
+ async def auth_method(
98
+ self,
99
+ info: Info[Context, None],
100
+ ) -> AuthMethod:
101
+ """Return semantic auth method (translated from database for LDAP users).
102
+
103
+ For Approach 1 (zero-migration), LDAP users are stored with auth_method='OAUTH2'
104
+ but have a special Unicode marker in oauth2_client_id. This resolver translates
105
+ that storage convention to the correct semantic AuthMethod.LDAP for the frontend.
106
+ """
107
+ if self.db_record:
108
+ auth_method_val = self.db_record.auth_method
109
+ oauth2_client_id = self.db_record.oauth2_client_id
110
+ else:
111
+ (
112
+ auth_method_val,
113
+ oauth2_client_id,
114
+ ) = await info.context.data_loaders.user_fields.load_many(
115
+ (
116
+ (self.id, models.User.auth_method),
117
+ (self.id, models.User.oauth2_client_id),
118
+ )
119
+ )
120
+
121
+ # Translate LDAP users from database storage to semantic type
122
+ if auth_method_val == "OAUTH2":
123
+ # Import here to avoid circular dependency
124
+ from phoenix.server.ldap import is_ldap_user
125
+
126
+ if is_ldap_user(oauth2_client_id):
127
+ return AuthMethod.LDAP
128
+
129
+ return AuthMethod(auth_method_val)
29
130
 
30
131
  @strawberry.field
31
132
  async def role(self, info: Info[Context, None]) -> UserRole:
32
- role = await info.context.data_loaders.user_roles.load(self.user_role_id)
133
+ if self.db_record:
134
+ user_role_id = self.db_record.user_role_id
135
+ else:
136
+ user_role_id = await info.context.data_loaders.user_fields.load(
137
+ (self.id, models.User.user_role_id),
138
+ )
139
+ role = await info.context.data_loaders.user_roles.load(user_role_id)
33
140
  if role is None:
34
- raise NotFound(f"User role with id {self.user_role_id} not found")
141
+ raise NotFound(f"User role with id {user_role_id} not found")
35
142
  return to_gql_user_role(role)
36
143
 
37
144
  @strawberry.field
38
145
  async def api_keys(self, info: Info[Context, None]) -> list[UserApiKey]:
39
146
  async with info.context.db() as session:
40
147
  api_keys = await session.scalars(
41
- select(models.ApiKey).where(models.ApiKey.user_id == self.id_attr)
42
- )
43
- return [to_gql_api_key(api_key) for api_key in api_keys]
44
-
45
-
46
- def to_gql_user(user: models.User, api_keys: Optional[list[models.ApiKey]] = None) -> User:
47
- """
48
- Converts an ORM user to a GraphQL user.
49
- """
50
- assert user.auth_method is not None
51
- return User(
52
- id_attr=user.id,
53
- password_needs_reset=user.reset_password,
54
- username=user.username,
55
- email=user.email,
56
- profile_picture_url=user.profile_picture_url,
57
- created_at=user.created_at,
58
- user_role_id=user.user_role_id,
59
- auth_method=AuthMethod(user.auth_method),
60
- )
148
+ select(models.ApiKey).where(models.ApiKey.user_id == self.id)
149
+ )
150
+ return [UserApiKey(id=api_key.id, db_record=api_key) for api_key in api_keys]
151
+
152
+ @strawberry.field
153
+ async def is_management_user(self, info: Info[Context, None]) -> bool:
154
+ initial_admins = get_env_admins()
155
+ # this field is only visible to initial admins as they are the ones likely to have access to
156
+ # a management interface / the phoenix environment.
157
+ if self.db_record:
158
+ email = self.db_record.email
159
+ else:
160
+ email = await info.context.data_loaders.user_fields.load(
161
+ (self.id, models.User.email),
162
+ )
163
+ if email in initial_admins or email == "admin@localhost":
164
+ return True
165
+ return False
@@ -1,14 +1,13 @@
1
- from typing import TYPE_CHECKING
1
+ from datetime import datetime
2
+ from typing import TYPE_CHECKING, Optional
2
3
 
3
4
  import strawberry
4
- from strawberry import Private
5
5
  from strawberry.relay import Node, NodeID
6
6
  from strawberry.types import Info
7
7
  from typing_extensions import Annotated
8
8
 
9
9
  from phoenix.db.models import ApiKey as OrmApiKey
10
10
  from phoenix.server.api.context import Context
11
- from phoenix.server.api.exceptions import NotFound
12
11
 
13
12
  from .ApiKey import ApiKey
14
13
 
@@ -18,28 +17,76 @@ if TYPE_CHECKING:
18
17
 
19
18
  @strawberry.type
20
19
  class UserApiKey(ApiKey, Node):
21
- id_attr: NodeID[int]
22
- user_id: Private[int]
20
+ id: NodeID[int]
21
+ db_record: strawberry.Private[Optional[OrmApiKey]] = None
22
+
23
+ def __post_init__(self) -> None:
24
+ if self.db_record and self.id != self.db_record.id:
25
+ raise ValueError("UserApiKey ID mismatch")
26
+
27
+ @strawberry.field(description="Name of the API key.") # type: ignore
28
+ async def name(
29
+ self,
30
+ info: Info[Context, None],
31
+ ) -> str:
32
+ if self.db_record:
33
+ val = self.db_record.name
34
+ else:
35
+ val = await info.context.data_loaders.user_api_key_fields.load(
36
+ (self.id, OrmApiKey.name),
37
+ )
38
+ return val
39
+
40
+ @strawberry.field(description="Description of the API key.") # type: ignore
41
+ async def description(
42
+ self,
43
+ info: Info[Context, None],
44
+ ) -> Optional[str]:
45
+ if self.db_record:
46
+ val = self.db_record.description
47
+ else:
48
+ val = await info.context.data_loaders.user_api_key_fields.load(
49
+ (self.id, OrmApiKey.description),
50
+ )
51
+ return val
52
+
53
+ @strawberry.field(description="The date and time the API key was created.") # type: ignore
54
+ async def created_at(
55
+ self,
56
+ info: Info[Context, None],
57
+ ) -> datetime:
58
+ if self.db_record:
59
+ val = self.db_record.created_at
60
+ else:
61
+ val = await info.context.data_loaders.user_api_key_fields.load(
62
+ (self.id, OrmApiKey.created_at),
63
+ )
64
+ return val
65
+
66
+ @strawberry.field(description="The date and time the API key will expire.") # type: ignore
67
+ async def expires_at(
68
+ self,
69
+ info: Info[Context, None],
70
+ ) -> Optional[datetime]:
71
+ if self.db_record:
72
+ val = self.db_record.expires_at
73
+ else:
74
+ val = await info.context.data_loaders.user_api_key_fields.load(
75
+ (self.id, OrmApiKey.expires_at),
76
+ )
77
+ return val
23
78
 
24
79
  @strawberry.field
25
- async def user(self, info: Info[Context, None]) -> Annotated["User", strawberry.lazy(".User")]:
26
- user = await info.context.data_loaders.users.load(self.user_id)
27
- if user is None:
28
- raise NotFound(f"User with id {self.user_id} not found")
29
- from .User import to_gql_user
30
-
31
- return to_gql_user(user)
32
-
33
-
34
- def to_gql_api_key(api_key: OrmApiKey) -> UserApiKey:
35
- """
36
- Converts an ORM API key to a GraphQL UserApiKey type.
37
- """
38
- return UserApiKey(
39
- id_attr=api_key.id,
40
- user_id=api_key.user_id,
41
- name=api_key.name,
42
- description=api_key.description,
43
- created_at=api_key.created_at,
44
- expires_at=api_key.expires_at,
45
- )
80
+ async def user(
81
+ self,
82
+ info: Info[Context, None],
83
+ ) -> Annotated["User", strawberry.lazy(".User")]:
84
+ if self.db_record:
85
+ user_id = self.db_record.user_id
86
+ else:
87
+ user_id = await info.context.data_loaders.user_api_key_fields.load(
88
+ (self.id, OrmApiKey.user_id),
89
+ )
90
+ from .User import User
91
+
92
+ return User(id=user_id)
@@ -1,5 +1,15 @@
1
+ import re
2
+ from base64 import b64decode
3
+
1
4
  from strawberry.relay import GlobalID
2
5
 
6
+ _GLOBAL_ID_PATTERN = re.compile(r"[a-zA-Z]+:[0-9]+")
7
+
8
+
9
+ def is_global_id(node_id: str) -> bool:
10
+ decoded_node_id = b64decode(node_id).decode()
11
+ return _GLOBAL_ID_PATTERN.match(decoded_node_id) is not None
12
+
3
13
 
4
14
  def from_global_id(global_id: GlobalID) -> tuple[str, int]:
5
15
  """
@@ -9,7 +9,7 @@ from strawberry.relay.types import Connection, Edge, NodeType, PageInfo
9
9
  from typing_extensions import TypeAlias, assert_never
10
10
 
11
11
  ID: TypeAlias = int
12
- CursorSortColumnValue: TypeAlias = Union[str, int, float, datetime]
12
+ CursorSortColumnValue: TypeAlias = Union[str, int, float, datetime, None]
13
13
 
14
14
  # A type alias for the connection cursor implementation
15
15
  CursorString = str
@@ -19,6 +19,7 @@ CURSOR_PREFIX = "connection:"
19
19
 
20
20
 
21
21
  class CursorSortColumnDataType(Enum):
22
+ NULL = auto()
22
23
  STRING = auto()
23
24
  INT = auto()
24
25
  FLOAT = auto()
@@ -30,6 +31,10 @@ class CursorSortColumn:
30
31
  type: CursorSortColumnDataType
31
32
  value: CursorSortColumnValue
32
33
 
34
+ def __post_init__(self) -> None:
35
+ if self.value is None:
36
+ self.type = CursorSortColumnDataType.NULL
37
+
33
38
  def __str__(self) -> str:
34
39
  if isinstance(self.value, str):
35
40
  return self.value
@@ -37,7 +42,9 @@ class CursorSortColumn:
37
42
  return str(self.value)
38
43
  if isinstance(self.value, datetime):
39
44
  return self.value.isoformat()
40
- assert_never(self.type)
45
+ if self.value is None:
46
+ return ""
47
+ assert_never(self.value)
41
48
 
42
49
  @classmethod
43
50
  def from_string(cls, type: CursorSortColumnDataType, cursor_string: str) -> "CursorSortColumn":
@@ -50,6 +57,8 @@ class CursorSortColumn:
50
57
  value = float(cursor_string)
51
58
  elif type is CursorSortColumnDataType.DATETIME:
52
59
  value = datetime.fromisoformat(cursor_string)
60
+ elif type is CursorSortColumnDataType.NULL:
61
+ value = None
53
62
  else:
54
63
  assert_never(type)
55
64
  return cls(type=type, value=value)