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,17 +1,17 @@
1
1
  import asyncio
2
2
  import logging
3
3
  from asyncio import Queue, as_completed
4
- from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
4
+ from collections import deque
5
5
  from dataclasses import dataclass, field
6
6
  from functools import singledispatchmethod
7
- from itertools import islice
8
- from time import perf_counter
9
- from typing import Any, Optional, cast
7
+ from time import perf_counter, time
8
+ from typing import Any, AsyncIterator, Awaitable, Callable, Iterable, Optional, cast
10
9
 
11
10
  from openinference.semconv.trace import SpanAttributes
12
11
  from typing_extensions import TypeAlias
13
12
 
14
13
  import phoenix.trace.v1 as pb
14
+ from phoenix.db import models
15
15
  from phoenix.db.insertion.constants import DEFAULT_RETRY_ALLOWANCE, DEFAULT_RETRY_DELAY_SEC
16
16
  from phoenix.db.insertion.document_annotation import DocumentAnnotationQueueInserter
17
17
  from phoenix.db.insertion.evaluation import (
@@ -23,21 +23,30 @@ from phoenix.db.insertion.helpers import (
23
23
  DataManipulationEvent,
24
24
  should_calculate_span_cost,
25
25
  )
26
+ from phoenix.db.insertion.session_annotation import SessionAnnotationQueueInserter
26
27
  from phoenix.db.insertion.span import SpanInsertionEvent, insert_span
27
28
  from phoenix.db.insertion.span_annotation import SpanAnnotationQueueInserter
28
29
  from phoenix.db.insertion.trace_annotation import TraceAnnotationQueueInserter
29
30
  from phoenix.db.insertion.types import Insertables, Precursors
30
31
  from phoenix.server.daemons.span_cost_calculator import (
31
32
  SpanCostCalculator,
32
- SpanCostCalculatorQueueItem,
33
33
  )
34
34
  from phoenix.server.dml_event import DmlEvent, SpanInsertEvent
35
+ from phoenix.server.prometheus import (
36
+ BULK_LOADER_EVALUATION_INSERTIONS,
37
+ BULK_LOADER_EXCEPTIONS,
38
+ BULK_LOADER_LAST_ACTIVITY,
39
+ BULK_LOADER_SPAN_EXCEPTIONS,
40
+ BULK_LOADER_SPAN_INSERTION_TIME,
41
+ SPAN_QUEUE_SIZE,
42
+ )
35
43
  from phoenix.server.types import CanPutItem, DbSessionFactory
36
44
  from phoenix.trace.schemas import Span
37
45
 
38
46
  logger = logging.getLogger(__name__)
39
47
 
40
48
  ProjectRowId: TypeAlias = int
49
+ ProjectName: TypeAlias = str
41
50
 
42
51
 
43
52
  @dataclass(frozen=True)
@@ -52,12 +61,12 @@ class BulkInserter:
52
61
  *,
53
62
  event_queue: CanPutItem[DmlEvent],
54
63
  span_cost_calculator: SpanCostCalculator,
55
- initial_batch_of_spans: Optional[Iterable[tuple[Span, str]]] = None,
56
- initial_batch_of_evaluations: Optional[Iterable[pb.Evaluation]] = None,
64
+ initial_batch_of_spans: Iterable[tuple[Span, ProjectName]] = (),
65
+ initial_batch_of_evaluations: Iterable[pb.Evaluation] = (),
57
66
  sleep: float = 0.1,
58
67
  max_ops_per_transaction: int = 1000,
59
68
  max_queue_size: int = 1000,
60
- enable_prometheus: bool = False,
69
+ max_spans_queue_size: Optional[int] = None,
61
70
  retry_delay_sec: float = DEFAULT_RETRY_DELAY_SEC,
62
71
  retry_allowance: int = DEFAULT_RETRY_ALLOWANCE,
63
72
  ) -> None:
@@ -68,7 +77,6 @@ class BulkInserter:
68
77
  :param max_ops_per_transaction: The maximum number of operations to dequeue from
69
78
  the operations queue for each transaction.
70
79
  :param max_queue_size: The maximum length of the operations queue.
71
- :param enable_prometheus: Whether Prometheus is enabled.
72
80
  """
73
81
  self._db = db
74
82
  self._running = False
@@ -76,20 +84,20 @@ class BulkInserter:
76
84
  self._max_ops_per_transaction = max_ops_per_transaction
77
85
  self._operations: Optional[Queue[DataManipulation]] = None
78
86
  self._max_queue_size = max_queue_size
79
- self._spans: list[tuple[Span, str]] = (
80
- [] if initial_batch_of_spans is None else list(initial_batch_of_spans)
81
- )
82
- self._evaluations: list[pb.Evaluation] = (
83
- [] if initial_batch_of_evaluations is None else list(initial_batch_of_evaluations)
84
- )
87
+ self._max_spans_queue_size = max_spans_queue_size
88
+ self._spans: deque[tuple[Span, ProjectName]] = deque(initial_batch_of_spans)
89
+ self._evaluations: deque[pb.Evaluation] = deque(initial_batch_of_evaluations)
85
90
  self._task: Optional[asyncio.Task[None]] = None
86
91
  self._event_queue = event_queue
87
- self._enable_prometheus = enable_prometheus
88
92
  self._retry_delay_sec = retry_delay_sec
89
93
  self._retry_allowance = retry_allowance
90
94
  self._queue_inserters = _QueueInserters(db, self._retry_delay_sec, self._retry_allowance)
91
95
  self._span_cost_calculator = span_cost_calculator
92
96
 
97
+ @property
98
+ def is_full(self) -> bool:
99
+ return bool(self._max_spans_queue_size and self._max_spans_queue_size <= len(self._spans))
100
+
93
101
  async def __aenter__(
94
102
  self,
95
103
  ) -> tuple[
@@ -102,9 +110,9 @@ class BulkInserter:
102
110
  self._operations = Queue(maxsize=self._max_queue_size)
103
111
  self._task = asyncio.create_task(self._bulk_insert())
104
112
  return (
105
- self._enqueue,
106
- self._queue_span,
107
- self._queue_evaluation,
113
+ self._enqueue_annotations,
114
+ self._enqueue_span,
115
+ self._enqueue_evaluation,
108
116
  self._enqueue_operation,
109
117
  )
110
118
 
@@ -114,23 +122,22 @@ class BulkInserter:
114
122
  self._task.cancel()
115
123
  self._task = None
116
124
 
117
- async def _enqueue(self, *items: Any) -> None:
125
+ async def _enqueue_annotations(self, *items: Any) -> None:
118
126
  await self._queue_inserters.enqueue(*items)
119
127
 
120
128
  def _enqueue_operation(self, operation: DataManipulation) -> None:
121
129
  cast("Queue[DataManipulation]", self._operations).put_nowait(operation)
122
130
 
123
- async def _queue_span(self, span: Span, project_name: str) -> None:
131
+ async def _enqueue_span(self, span: Span, project_name: str) -> None:
124
132
  self._spans.append((span, project_name))
125
133
 
126
- async def _queue_evaluation(self, evaluation: pb.Evaluation) -> None:
134
+ async def _enqueue_evaluation(self, evaluation: pb.Evaluation) -> None:
127
135
  self._evaluations.append(evaluation)
128
136
 
129
137
  async def _process_events(self, events: Iterable[Optional[DataManipulationEvent]]) -> None: ...
130
138
 
131
139
  async def _bulk_insert(self) -> None:
132
140
  assert isinstance(self._operations, Queue)
133
- spans_buffer, evaluations_buffer = None, None
134
141
  # start first insert immediately if the inserter has not run recently
135
142
  while (
136
143
  self._running
@@ -139,6 +146,8 @@ class BulkInserter:
139
146
  or self._spans
140
147
  or self._evaluations
141
148
  ):
149
+ BULK_LOADER_LAST_ACTIVITY.set(time())
150
+ SPAN_QUEUE_SIZE.set(len(self._spans))
142
151
  if (
143
152
  self._queue_inserters.empty
144
153
  and self._operations.empty()
@@ -156,113 +165,100 @@ class BulkInserter:
156
165
  async with session.begin_nested():
157
166
  await op(session)
158
167
  except Exception as e:
159
- if self._enable_prometheus:
160
- from phoenix.server.prometheus import BULK_LOADER_EXCEPTIONS
161
-
162
- BULK_LOADER_EXCEPTIONS.inc()
168
+ BULK_LOADER_EXCEPTIONS.inc()
163
169
  logger.exception(str(e))
164
170
  # It's important to grab the buffers at the same time so there's
165
171
  # no race condition, since an eval insertion will fail if the span
166
172
  # it references doesn't exist. Grabbing the eval buffer later may
167
173
  # include an eval whose span is in the queue but missed being
168
174
  # included in the span buffer that was grabbed previously.
169
- if self._spans:
170
- spans_buffer = self._spans
171
- self._spans = []
172
- if self._evaluations:
173
- evaluations_buffer = self._evaluations
174
- self._evaluations = []
175
+ num_spans_to_insert = min(self._max_ops_per_transaction, len(self._spans))
176
+ num_evals_to_insert = min(self._max_ops_per_transaction, len(self._evaluations))
175
177
  # Spans should be inserted before the evaluations, since an evaluation
176
178
  # insertion will fail if the span it references doesn't exist.
177
- if spans_buffer:
178
- await self._insert_spans(spans_buffer)
179
- spans_buffer = None
180
- if evaluations_buffer:
181
- await self._insert_evaluations(evaluations_buffer)
182
- evaluations_buffer = None
179
+ await self._insert_spans(num_spans_to_insert)
180
+ await self._insert_evaluations(num_evals_to_insert)
183
181
  async for event in self._queue_inserters.insert():
184
182
  self._event_queue.put(event)
185
183
  await asyncio.sleep(self._sleep)
186
184
 
187
- async def _insert_spans(self, spans: list[tuple[Span, str]]) -> None:
185
+ async def _insert_spans(self, num_spans_to_insert: int) -> None:
186
+ if not num_spans_to_insert or not self._spans:
187
+ return
188
188
  project_ids = set()
189
- span_cost_calculator_queue: list[SpanCostCalculatorQueueItem] = []
190
- for i in range(0, len(spans), self._max_ops_per_transaction):
191
- try:
192
- start = perf_counter()
193
- async with self._db() as session:
194
- for span, project_name in islice(spans, i, i + self._max_ops_per_transaction):
195
- if self._enable_prometheus:
196
- from phoenix.server.prometheus import BULK_LOADER_SPAN_INSERTIONS
197
-
198
- BULK_LOADER_SPAN_INSERTIONS.inc()
199
- result: Optional[SpanInsertionEvent] = None
200
- try:
201
- async with session.begin_nested():
202
- result = await insert_span(session, span, project_name)
203
- except Exception:
204
- if self._enable_prometheus:
205
- from phoenix.server.prometheus import BULK_LOADER_EXCEPTIONS
206
-
207
- BULK_LOADER_EXCEPTIONS.inc()
208
- logger.exception(
209
- f"Failed to insert span with span_id={span.context.span_id}"
210
- )
211
- if result is not None:
212
- project_ids.add(result.project_rowid)
213
- if should_calculate_span_cost(span.attributes):
214
- span_cost_calculator_queue.append(
215
- SpanCostCalculatorQueueItem(
216
- span_rowid=result.span_rowid,
217
- trace_rowid=result.trace_rowid,
218
- attributes=span.attributes,
219
- span_start_time=span.start_time,
220
- )
221
- )
222
-
223
- if self._enable_prometheus:
224
- from phoenix.server.prometheus import BULK_LOADER_INSERTION_TIME
225
-
226
- BULK_LOADER_INSERTION_TIME.observe(perf_counter() - start)
227
- except Exception:
228
- if self._enable_prometheus:
229
- from phoenix.server.prometheus import BULK_LOADER_EXCEPTIONS
230
-
231
- BULK_LOADER_EXCEPTIONS.inc()
232
- logger.exception("Failed to insert spans")
233
- self._event_queue.put(SpanInsertEvent(tuple(project_ids)))
234
- for item in span_cost_calculator_queue:
235
- self._span_cost_calculator.put_nowait(item)
236
-
237
- async def _insert_evaluations(self, evaluations: list[pb.Evaluation]) -> None:
238
- for i in range(0, len(evaluations), self._max_ops_per_transaction):
239
- try:
240
- start = perf_counter()
241
- async with self._db() as session:
242
- for evaluation in islice(evaluations, i, i + self._max_ops_per_transaction):
243
- if self._enable_prometheus:
244
- from phoenix.server.prometheus import BULK_LOADER_EVALUATION_INSERTIONS
245
-
246
- BULK_LOADER_EVALUATION_INSERTIONS.inc()
247
- try:
248
- async with session.begin_nested():
249
- await insert_evaluation(session, evaluation)
250
- except InsertEvaluationError as error:
251
- if self._enable_prometheus:
252
- from phoenix.server.prometheus import BULK_LOADER_EXCEPTIONS
253
-
254
- BULK_LOADER_EXCEPTIONS.inc()
255
- logger.exception(f"Failed to insert evaluation: {str(error)}")
256
- if self._enable_prometheus:
257
- from phoenix.server.prometheus import BULK_LOADER_INSERTION_TIME
258
-
259
- BULK_LOADER_INSERTION_TIME.observe(perf_counter() - start)
260
- except Exception:
261
- if self._enable_prometheus:
262
- from phoenix.server.prometheus import BULK_LOADER_EXCEPTIONS
189
+ span_costs: list[models.SpanCost] = []
190
+ try:
191
+ start = perf_counter()
192
+ async with self._db() as session:
193
+ while num_spans_to_insert > 0:
194
+ num_spans_to_insert -= 1
195
+ if not self._spans:
196
+ break
197
+ span, project_name = self._spans.popleft()
198
+ result: Optional[SpanInsertionEvent] = None
199
+ try:
200
+ async with session.begin_nested():
201
+ result = await insert_span(session, span, project_name)
202
+ except Exception:
203
+ BULK_LOADER_SPAN_EXCEPTIONS.inc()
204
+ logger.exception(
205
+ f"Failed to insert span with span_id={span.context.span_id}"
206
+ )
207
+ if result is None:
208
+ continue
209
+ project_ids.add(result.project_rowid)
210
+ try:
211
+ if not should_calculate_span_cost(span.attributes):
212
+ continue
213
+ span_cost = self._span_cost_calculator.calculate_cost(
214
+ span.start_time,
215
+ span.attributes,
216
+ )
217
+ except Exception:
218
+ logger.exception(
219
+ f"Failed to calculate span cost for span with "
220
+ f"span_id={span.context.span_id}"
221
+ )
222
+ else:
223
+ if span_cost is None:
224
+ continue
225
+ span_cost.span_rowid = result.span_rowid
226
+ span_cost.trace_rowid = result.trace_rowid
227
+ span_costs.append(span_cost)
228
+ BULK_LOADER_SPAN_INSERTION_TIME.observe(perf_counter() - start)
229
+ except Exception:
230
+ BULK_LOADER_SPAN_EXCEPTIONS.inc()
231
+ logger.exception("Failed to insert spans")
232
+ if project_ids:
233
+ self._event_queue.put(SpanInsertEvent(tuple(project_ids)))
234
+ if not span_costs:
235
+ return
236
+ try:
237
+ async with self._db() as session:
238
+ session.add_all(span_costs)
239
+ except Exception:
240
+ logger.exception("Failed to insert span costs")
263
241
 
264
- BULK_LOADER_EXCEPTIONS.inc()
265
- logger.exception("Failed to insert evaluations")
242
+ async def _insert_evaluations(self, num_evals_to_insert: int) -> None:
243
+ if not num_evals_to_insert or not self._evaluations:
244
+ return
245
+ try:
246
+ async with self._db() as session:
247
+ while num_evals_to_insert > 0:
248
+ num_evals_to_insert -= 1
249
+ if not self._evaluations:
250
+ break
251
+ evaluation = self._evaluations.popleft()
252
+ BULK_LOADER_EVALUATION_INSERTIONS.inc()
253
+ try:
254
+ async with session.begin_nested():
255
+ await insert_evaluation(session, evaluation)
256
+ except InsertEvaluationError as error:
257
+ BULK_LOADER_EXCEPTIONS.inc()
258
+ logger.exception(f"Failed to insert evaluation: {str(error)}")
259
+ except Exception:
260
+ BULK_LOADER_EXCEPTIONS.inc()
261
+ logger.exception("Failed to insert evaluations")
266
262
 
267
263
 
268
264
  class _QueueInserters:
@@ -277,10 +273,12 @@ class _QueueInserters:
277
273
  self._span_annotations = SpanAnnotationQueueInserter(*args)
278
274
  self._trace_annotations = TraceAnnotationQueueInserter(*args)
279
275
  self._document_annotations = DocumentAnnotationQueueInserter(*args)
276
+ self._session_annotations = SessionAnnotationQueueInserter(*args)
280
277
  self._queues = (
281
278
  self._span_annotations,
282
279
  self._trace_annotations,
283
280
  self._document_annotations,
281
+ self._session_annotations,
284
282
  )
285
283
 
286
284
  async def insert(self) -> AsyncIterator[DmlEvent]:
@@ -317,6 +315,11 @@ class _QueueInserters:
317
315
  async def _(self, item: Precursors.DocumentAnnotation) -> None:
318
316
  await self._document_annotations.enqueue(item)
319
317
 
318
+ @_enqueue.register(Precursors.SessionAnnotation)
319
+ @_enqueue.register(Insertables.SessionAnnotation)
320
+ async def _(self, item: Precursors.SessionAnnotation) -> None:
321
+ await self._session_annotations.enqueue(item)
322
+
320
323
 
321
324
  LLM_MODEL_NAME = SpanAttributes.LLM_MODEL_NAME
322
325
  LLM_PROVIDER = SpanAttributes.LLM_PROVIDER
phoenix/db/engines.py CHANGED
@@ -1,16 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import json
5
4
  import logging
6
5
  from collections.abc import Callable
7
- from datetime import datetime
8
6
  from enum import Enum
9
7
  from sqlite3 import Connection
10
- from typing import Any
8
+ from typing import Any, Optional
11
9
 
12
10
  import aiosqlite
13
11
  import numpy as np
12
+ import orjson
14
13
  import sqlalchemy
15
14
  import sqlean
16
15
  from sqlalchemy import URL, StaticPool, event, make_url
@@ -123,7 +122,7 @@ def aio_sqlite_engine(
123
122
  lambda: sqlean.connect(f"file:{database}", uri=True),
124
123
  iter_chunk_size=64,
125
124
  )
126
- conn.daemon = True
125
+ conn.daemon = True # type: ignore[attr-defined]
127
126
  return conn
128
127
 
129
128
  engine = create_async_engine(
@@ -169,43 +168,151 @@ def aio_postgresql_engine(
169
168
  log_to_stdout: bool = False,
170
169
  log_migrations_to_stdout: bool = True,
171
170
  ) -> AsyncEngine:
172
- asyncpg_url, asyncpg_args = get_pg_config(url, "asyncpg")
173
- engine = create_async_engine(
174
- url=asyncpg_url,
175
- connect_args=asyncpg_args,
176
- echo=log_to_stdout,
177
- json_serializer=_dumps,
171
+ from phoenix.config import (
172
+ get_env_postgres_iam_token_lifetime,
173
+ get_env_postgres_use_iam_auth,
178
174
  )
175
+
176
+ use_iam_auth = get_env_postgres_use_iam_auth()
177
+
178
+ asyncpg_url, asyncpg_args = get_pg_config(url, "asyncpg", enforce_ssl=use_iam_auth)
179
+
180
+ iam_config: Optional[dict[str, Any]] = None
181
+ token_lifetime: int = 0
182
+ if use_iam_auth:
183
+ iam_config = _extract_iam_config_from_url(url)
184
+ token_lifetime = get_env_postgres_iam_token_lifetime()
185
+
186
+ async def iam_async_creator() -> Any:
187
+ import asyncpg # type: ignore
188
+
189
+ from phoenix.db.iam_auth import generate_aws_rds_token
190
+
191
+ assert iam_config is not None
192
+ token = generate_aws_rds_token(
193
+ host=iam_config["host"],
194
+ port=iam_config["port"],
195
+ user=iam_config["user"],
196
+ )
197
+
198
+ conn_kwargs = {
199
+ "host": iam_config["host"],
200
+ "port": iam_config["port"],
201
+ "user": iam_config["user"],
202
+ "password": token,
203
+ "database": iam_config["database"],
204
+ }
205
+
206
+ if asyncpg_args:
207
+ conn_kwargs.update(asyncpg_args)
208
+
209
+ return await asyncpg.connect(**conn_kwargs)
210
+
211
+ engine = create_async_engine(
212
+ url=asyncpg_url,
213
+ async_creator=iam_async_creator,
214
+ echo=log_to_stdout,
215
+ json_serializer=_dumps,
216
+ pool_recycle=token_lifetime,
217
+ )
218
+ else:
219
+ engine = create_async_engine(
220
+ url=asyncpg_url,
221
+ connect_args=asyncpg_args,
222
+ echo=log_to_stdout,
223
+ json_serializer=_dumps,
224
+ )
225
+
179
226
  if not migrate:
180
227
  return engine
181
228
 
182
- psycopg_url, psycopg_args = get_pg_config(url, "psycopg")
183
- sync_engine = sqlalchemy.create_engine(
184
- url=psycopg_url,
185
- connect_args=psycopg_args,
186
- echo=log_migrations_to_stdout,
187
- json_serializer=_dumps,
188
- )
229
+ psycopg_url, psycopg_args = get_pg_config(url, "psycopg", enforce_ssl=use_iam_auth)
230
+
231
+ if use_iam_auth:
232
+ assert iam_config is not None
233
+
234
+ def iam_sync_creator() -> Any:
235
+ import psycopg
236
+
237
+ from phoenix.db.iam_auth import generate_aws_rds_token
238
+
239
+ token = generate_aws_rds_token(
240
+ host=iam_config["host"],
241
+ port=iam_config["port"],
242
+ user=iam_config["user"],
243
+ )
244
+
245
+ conn_kwargs = {
246
+ "host": iam_config["host"],
247
+ "port": iam_config["port"],
248
+ "user": iam_config["user"],
249
+ "password": token,
250
+ "dbname": iam_config["database"],
251
+ }
252
+
253
+ if psycopg_args:
254
+ conn_kwargs.update(psycopg_args)
255
+
256
+ return psycopg.connect(**conn_kwargs)
257
+
258
+ sync_engine = sqlalchemy.create_engine(
259
+ url=psycopg_url,
260
+ creator=iam_sync_creator,
261
+ echo=log_migrations_to_stdout,
262
+ json_serializer=_dumps,
263
+ pool_recycle=token_lifetime,
264
+ )
265
+ else:
266
+ sync_engine = sqlalchemy.create_engine(
267
+ url=psycopg_url,
268
+ connect_args=psycopg_args,
269
+ echo=log_migrations_to_stdout,
270
+ json_serializer=_dumps,
271
+ )
272
+
189
273
  if schema := get_env_database_schema():
190
274
  event.listen(sync_engine, "connect", set_postgresql_search_path(schema))
191
275
  migrate_in_thread(sync_engine)
192
276
  return engine
193
277
 
194
278
 
279
+ def _extract_iam_config_from_url(url: URL) -> dict[str, Any]:
280
+ """Extract connection parameters needed for IAM authentication from a SQLAlchemy URL.
281
+
282
+ Args:
283
+ url: SQLAlchemy database URL
284
+
285
+ Returns:
286
+ Dictionary with host, port, user, and database
287
+ """
288
+ host = url.host
289
+ if not host:
290
+ raise ValueError("Database host is required for IAM authentication")
291
+
292
+ port = url.port or 5432
293
+ user = url.username
294
+ if not user:
295
+ raise ValueError("Database user is required for IAM authentication")
296
+
297
+ database = url.database or "postgres"
298
+
299
+ return {
300
+ "host": host,
301
+ "port": port,
302
+ "user": user,
303
+ "database": database,
304
+ }
305
+
306
+
195
307
  def _dumps(obj: Any) -> str:
196
- return json.dumps(obj, cls=_Encoder)
197
-
198
-
199
- class _Encoder(json.JSONEncoder):
200
- def default(self, obj: Any) -> Any:
201
- if isinstance(obj, datetime):
202
- return obj.isoformat()
203
- elif isinstance(obj, Enum):
204
- return obj.value
205
- elif isinstance(obj, np.ndarray):
206
- return list(obj)
207
- elif isinstance(obj, np.integer):
208
- return int(obj)
209
- elif isinstance(obj, np.floating):
210
- return float(obj)
211
- return super().default(obj)
308
+ return orjson.dumps(obj, default=_default).decode()
309
+
310
+
311
+ def _default(obj: Any) -> Any:
312
+ if isinstance(obj, np.ndarray):
313
+ return obj.tolist()
314
+ if isinstance(obj, (np.integer, np.floating, np.bool_)):
315
+ return obj.item()
316
+ if isinstance(obj, Enum):
317
+ return obj.value
318
+ raise TypeError(f"Object of type {type(obj).__name__} is not serializable")
phoenix/db/facilitator.py CHANGED
@@ -26,10 +26,12 @@ from phoenix.auth import (
26
26
  compute_password_hash,
27
27
  )
28
28
  from phoenix.config import (
29
+ LDAPConfig,
29
30
  get_env_admins,
30
31
  get_env_default_admin_initial_password,
31
32
  get_env_default_retention_policy_days,
32
33
  get_env_disable_basic_auth,
34
+ get_env_oauth2_settings,
33
35
  )
34
36
  from phoenix.db import models
35
37
  from phoenix.db.constants import DEFAULT_PROJECT_TRACE_RETENTION_POLICY_ID
@@ -198,6 +200,20 @@ async def _ensure_admins(
198
200
  return
199
201
  admin_role_id = await session.scalar(sa.select(models.UserRole.id).filter_by(name="ADMIN"))
200
202
  assert admin_role_id is not None, "Admin role not found in database"
203
+
204
+ # Determine which auth method to use for admin users
205
+ # Priority: LOCAL (if enabled) > LDAP (if configured and no OAuth2) > OAuth2
206
+ # Use try/except to handle invalid configurations gracefully
207
+ try:
208
+ ldap_config = LDAPConfig.from_env()
209
+ except Exception:
210
+ ldap_config = None
211
+ try:
212
+ oauth2_configs = get_env_oauth2_settings()
213
+ except Exception:
214
+ oauth2_configs = []
215
+ use_ldap = disable_basic_auth and ldap_config is not None and not oauth2_configs
216
+
201
217
  user: models.User
202
218
  for email, username in admins.items():
203
219
  if not disable_basic_auth:
@@ -207,6 +223,11 @@ async def _ensure_admins(
207
223
  password_salt=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
208
224
  password_hash=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
209
225
  )
226
+ elif use_ldap:
227
+ user = models.LDAPUser(
228
+ email=email,
229
+ username=username,
230
+ )
210
231
  else:
211
232
  user = models.OAuth2User(
212
233
  email=email,
@@ -229,7 +250,7 @@ _CHILDLESS_RECORD_DELETION_GRACE_PERIOD_DAYS = 1
229
250
 
230
251
 
231
252
  def _stmt_to_delete_expired_childless_records(
232
- table: type[models.Base],
253
+ table: type[models.HasId],
233
254
  foreign_key: Union[InstrumentedAttribute[int], InstrumentedAttribute[Optional[int]]],
234
255
  ) -> ReturningDelete[tuple[int]]:
235
256
  """