arize-phoenix 3.16.1__py3-none-any.whl → 7.7.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.

Potentially problematic release.


This version of arize-phoenix might be problematic. Click here for more details.

Files changed (338) hide show
  1. arize_phoenix-7.7.1.dist-info/METADATA +261 -0
  2. arize_phoenix-7.7.1.dist-info/RECORD +345 -0
  3. {arize_phoenix-3.16.1.dist-info → arize_phoenix-7.7.1.dist-info}/WHEEL +1 -1
  4. arize_phoenix-7.7.1.dist-info/entry_points.txt +3 -0
  5. phoenix/__init__.py +86 -14
  6. phoenix/auth.py +309 -0
  7. phoenix/config.py +675 -45
  8. phoenix/core/model.py +32 -30
  9. phoenix/core/model_schema.py +102 -109
  10. phoenix/core/model_schema_adapter.py +48 -45
  11. phoenix/datetime_utils.py +24 -3
  12. phoenix/db/README.md +54 -0
  13. phoenix/db/__init__.py +4 -0
  14. phoenix/db/alembic.ini +85 -0
  15. phoenix/db/bulk_inserter.py +294 -0
  16. phoenix/db/engines.py +208 -0
  17. phoenix/db/enums.py +20 -0
  18. phoenix/db/facilitator.py +113 -0
  19. phoenix/db/helpers.py +159 -0
  20. phoenix/db/insertion/constants.py +2 -0
  21. phoenix/db/insertion/dataset.py +227 -0
  22. phoenix/db/insertion/document_annotation.py +171 -0
  23. phoenix/db/insertion/evaluation.py +191 -0
  24. phoenix/db/insertion/helpers.py +98 -0
  25. phoenix/db/insertion/span.py +193 -0
  26. phoenix/db/insertion/span_annotation.py +158 -0
  27. phoenix/db/insertion/trace_annotation.py +158 -0
  28. phoenix/db/insertion/types.py +256 -0
  29. phoenix/db/migrate.py +86 -0
  30. phoenix/db/migrations/data_migration_scripts/populate_project_sessions.py +199 -0
  31. phoenix/db/migrations/env.py +114 -0
  32. phoenix/db/migrations/script.py.mako +26 -0
  33. phoenix/db/migrations/versions/10460e46d750_datasets.py +317 -0
  34. phoenix/db/migrations/versions/3be8647b87d8_add_token_columns_to_spans_table.py +126 -0
  35. phoenix/db/migrations/versions/4ded9e43755f_create_project_sessions_table.py +66 -0
  36. phoenix/db/migrations/versions/cd164e83824f_users_and_tokens.py +157 -0
  37. phoenix/db/migrations/versions/cf03bd6bae1d_init.py +280 -0
  38. phoenix/db/models.py +807 -0
  39. phoenix/exceptions.py +5 -1
  40. phoenix/experiments/__init__.py +6 -0
  41. phoenix/experiments/evaluators/__init__.py +29 -0
  42. phoenix/experiments/evaluators/base.py +158 -0
  43. phoenix/experiments/evaluators/code_evaluators.py +184 -0
  44. phoenix/experiments/evaluators/llm_evaluators.py +473 -0
  45. phoenix/experiments/evaluators/utils.py +236 -0
  46. phoenix/experiments/functions.py +772 -0
  47. phoenix/experiments/tracing.py +86 -0
  48. phoenix/experiments/types.py +726 -0
  49. phoenix/experiments/utils.py +25 -0
  50. phoenix/inferences/__init__.py +0 -0
  51. phoenix/{datasets → inferences}/errors.py +6 -5
  52. phoenix/{datasets → inferences}/fixtures.py +49 -42
  53. phoenix/{datasets/dataset.py → inferences/inferences.py} +121 -105
  54. phoenix/{datasets → inferences}/schema.py +11 -11
  55. phoenix/{datasets → inferences}/validation.py +13 -14
  56. phoenix/logging/__init__.py +3 -0
  57. phoenix/logging/_config.py +90 -0
  58. phoenix/logging/_filter.py +6 -0
  59. phoenix/logging/_formatter.py +69 -0
  60. phoenix/metrics/__init__.py +5 -4
  61. phoenix/metrics/binning.py +4 -3
  62. phoenix/metrics/metrics.py +2 -1
  63. phoenix/metrics/mixins.py +7 -6
  64. phoenix/metrics/retrieval_metrics.py +2 -1
  65. phoenix/metrics/timeseries.py +5 -4
  66. phoenix/metrics/wrappers.py +9 -3
  67. phoenix/pointcloud/clustering.py +5 -5
  68. phoenix/pointcloud/pointcloud.py +7 -5
  69. phoenix/pointcloud/projectors.py +5 -6
  70. phoenix/pointcloud/umap_parameters.py +53 -52
  71. phoenix/server/api/README.md +28 -0
  72. phoenix/server/api/auth.py +44 -0
  73. phoenix/server/api/context.py +152 -9
  74. phoenix/server/api/dataloaders/__init__.py +91 -0
  75. phoenix/server/api/dataloaders/annotation_summaries.py +139 -0
  76. phoenix/server/api/dataloaders/average_experiment_run_latency.py +54 -0
  77. phoenix/server/api/dataloaders/cache/__init__.py +3 -0
  78. phoenix/server/api/dataloaders/cache/two_tier_cache.py +68 -0
  79. phoenix/server/api/dataloaders/dataset_example_revisions.py +131 -0
  80. phoenix/server/api/dataloaders/dataset_example_spans.py +38 -0
  81. phoenix/server/api/dataloaders/document_evaluation_summaries.py +144 -0
  82. phoenix/server/api/dataloaders/document_evaluations.py +31 -0
  83. phoenix/server/api/dataloaders/document_retrieval_metrics.py +89 -0
  84. phoenix/server/api/dataloaders/experiment_annotation_summaries.py +79 -0
  85. phoenix/server/api/dataloaders/experiment_error_rates.py +58 -0
  86. phoenix/server/api/dataloaders/experiment_run_annotations.py +36 -0
  87. phoenix/server/api/dataloaders/experiment_run_counts.py +49 -0
  88. phoenix/server/api/dataloaders/experiment_sequence_number.py +44 -0
  89. phoenix/server/api/dataloaders/latency_ms_quantile.py +188 -0
  90. phoenix/server/api/dataloaders/min_start_or_max_end_times.py +85 -0
  91. phoenix/server/api/dataloaders/project_by_name.py +31 -0
  92. phoenix/server/api/dataloaders/record_counts.py +116 -0
  93. phoenix/server/api/dataloaders/session_io.py +79 -0
  94. phoenix/server/api/dataloaders/session_num_traces.py +30 -0
  95. phoenix/server/api/dataloaders/session_num_traces_with_error.py +32 -0
  96. phoenix/server/api/dataloaders/session_token_usages.py +41 -0
  97. phoenix/server/api/dataloaders/session_trace_latency_ms_quantile.py +55 -0
  98. phoenix/server/api/dataloaders/span_annotations.py +26 -0
  99. phoenix/server/api/dataloaders/span_dataset_examples.py +31 -0
  100. phoenix/server/api/dataloaders/span_descendants.py +57 -0
  101. phoenix/server/api/dataloaders/span_projects.py +33 -0
  102. phoenix/server/api/dataloaders/token_counts.py +124 -0
  103. phoenix/server/api/dataloaders/trace_by_trace_ids.py +25 -0
  104. phoenix/server/api/dataloaders/trace_root_spans.py +32 -0
  105. phoenix/server/api/dataloaders/user_roles.py +30 -0
  106. phoenix/server/api/dataloaders/users.py +33 -0
  107. phoenix/server/api/exceptions.py +48 -0
  108. phoenix/server/api/helpers/__init__.py +12 -0
  109. phoenix/server/api/helpers/dataset_helpers.py +217 -0
  110. phoenix/server/api/helpers/experiment_run_filters.py +763 -0
  111. phoenix/server/api/helpers/playground_clients.py +948 -0
  112. phoenix/server/api/helpers/playground_registry.py +70 -0
  113. phoenix/server/api/helpers/playground_spans.py +455 -0
  114. phoenix/server/api/input_types/AddExamplesToDatasetInput.py +16 -0
  115. phoenix/server/api/input_types/AddSpansToDatasetInput.py +14 -0
  116. phoenix/server/api/input_types/ChatCompletionInput.py +38 -0
  117. phoenix/server/api/input_types/ChatCompletionMessageInput.py +24 -0
  118. phoenix/server/api/input_types/ClearProjectInput.py +15 -0
  119. phoenix/server/api/input_types/ClusterInput.py +2 -2
  120. phoenix/server/api/input_types/CreateDatasetInput.py +12 -0
  121. phoenix/server/api/input_types/CreateSpanAnnotationInput.py +18 -0
  122. phoenix/server/api/input_types/CreateTraceAnnotationInput.py +18 -0
  123. phoenix/server/api/input_types/DataQualityMetricInput.py +5 -2
  124. phoenix/server/api/input_types/DatasetExampleInput.py +14 -0
  125. phoenix/server/api/input_types/DatasetSort.py +17 -0
  126. phoenix/server/api/input_types/DatasetVersionSort.py +16 -0
  127. phoenix/server/api/input_types/DeleteAnnotationsInput.py +7 -0
  128. phoenix/server/api/input_types/DeleteDatasetExamplesInput.py +13 -0
  129. phoenix/server/api/input_types/DeleteDatasetInput.py +7 -0
  130. phoenix/server/api/input_types/DeleteExperimentsInput.py +7 -0
  131. phoenix/server/api/input_types/DimensionFilter.py +4 -4
  132. phoenix/server/api/input_types/GenerativeModelInput.py +17 -0
  133. phoenix/server/api/input_types/Granularity.py +1 -1
  134. phoenix/server/api/input_types/InvocationParameters.py +162 -0
  135. phoenix/server/api/input_types/PatchAnnotationInput.py +19 -0
  136. phoenix/server/api/input_types/PatchDatasetExamplesInput.py +35 -0
  137. phoenix/server/api/input_types/PatchDatasetInput.py +14 -0
  138. phoenix/server/api/input_types/PerformanceMetricInput.py +5 -2
  139. phoenix/server/api/input_types/ProjectSessionSort.py +29 -0
  140. phoenix/server/api/input_types/SpanAnnotationSort.py +17 -0
  141. phoenix/server/api/input_types/SpanSort.py +134 -69
  142. phoenix/server/api/input_types/TemplateOptions.py +10 -0
  143. phoenix/server/api/input_types/TraceAnnotationSort.py +17 -0
  144. phoenix/server/api/input_types/UserRoleInput.py +9 -0
  145. phoenix/server/api/mutations/__init__.py +28 -0
  146. phoenix/server/api/mutations/api_key_mutations.py +167 -0
  147. phoenix/server/api/mutations/chat_mutations.py +593 -0
  148. phoenix/server/api/mutations/dataset_mutations.py +591 -0
  149. phoenix/server/api/mutations/experiment_mutations.py +75 -0
  150. phoenix/server/api/{types/ExportEventsMutation.py → mutations/export_events_mutations.py} +21 -18
  151. phoenix/server/api/mutations/project_mutations.py +57 -0
  152. phoenix/server/api/mutations/span_annotations_mutations.py +128 -0
  153. phoenix/server/api/mutations/trace_annotations_mutations.py +127 -0
  154. phoenix/server/api/mutations/user_mutations.py +329 -0
  155. phoenix/server/api/openapi/__init__.py +0 -0
  156. phoenix/server/api/openapi/main.py +17 -0
  157. phoenix/server/api/openapi/schema.py +16 -0
  158. phoenix/server/api/queries.py +738 -0
  159. phoenix/server/api/routers/__init__.py +11 -0
  160. phoenix/server/api/routers/auth.py +284 -0
  161. phoenix/server/api/routers/embeddings.py +26 -0
  162. phoenix/server/api/routers/oauth2.py +488 -0
  163. phoenix/server/api/routers/v1/__init__.py +64 -0
  164. phoenix/server/api/routers/v1/datasets.py +1017 -0
  165. phoenix/server/api/routers/v1/evaluations.py +362 -0
  166. phoenix/server/api/routers/v1/experiment_evaluations.py +115 -0
  167. phoenix/server/api/routers/v1/experiment_runs.py +167 -0
  168. phoenix/server/api/routers/v1/experiments.py +308 -0
  169. phoenix/server/api/routers/v1/pydantic_compat.py +78 -0
  170. phoenix/server/api/routers/v1/spans.py +267 -0
  171. phoenix/server/api/routers/v1/traces.py +208 -0
  172. phoenix/server/api/routers/v1/utils.py +95 -0
  173. phoenix/server/api/schema.py +44 -241
  174. phoenix/server/api/subscriptions.py +597 -0
  175. phoenix/server/api/types/Annotation.py +21 -0
  176. phoenix/server/api/types/AnnotationSummary.py +55 -0
  177. phoenix/server/api/types/AnnotatorKind.py +16 -0
  178. phoenix/server/api/types/ApiKey.py +27 -0
  179. phoenix/server/api/types/AuthMethod.py +9 -0
  180. phoenix/server/api/types/ChatCompletionMessageRole.py +11 -0
  181. phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +46 -0
  182. phoenix/server/api/types/Cluster.py +25 -24
  183. phoenix/server/api/types/CreateDatasetPayload.py +8 -0
  184. phoenix/server/api/types/DataQualityMetric.py +31 -13
  185. phoenix/server/api/types/Dataset.py +288 -63
  186. phoenix/server/api/types/DatasetExample.py +85 -0
  187. phoenix/server/api/types/DatasetExampleRevision.py +34 -0
  188. phoenix/server/api/types/DatasetVersion.py +14 -0
  189. phoenix/server/api/types/Dimension.py +32 -31
  190. phoenix/server/api/types/DocumentEvaluationSummary.py +9 -8
  191. phoenix/server/api/types/EmbeddingDimension.py +56 -49
  192. phoenix/server/api/types/Evaluation.py +25 -31
  193. phoenix/server/api/types/EvaluationSummary.py +30 -50
  194. phoenix/server/api/types/Event.py +20 -20
  195. phoenix/server/api/types/ExampleRevisionInterface.py +14 -0
  196. phoenix/server/api/types/Experiment.py +152 -0
  197. phoenix/server/api/types/ExperimentAnnotationSummary.py +13 -0
  198. phoenix/server/api/types/ExperimentComparison.py +17 -0
  199. phoenix/server/api/types/ExperimentRun.py +119 -0
  200. phoenix/server/api/types/ExperimentRunAnnotation.py +56 -0
  201. phoenix/server/api/types/GenerativeModel.py +9 -0
  202. phoenix/server/api/types/GenerativeProvider.py +85 -0
  203. phoenix/server/api/types/Inferences.py +80 -0
  204. phoenix/server/api/types/InferencesRole.py +23 -0
  205. phoenix/server/api/types/LabelFraction.py +7 -0
  206. phoenix/server/api/types/MimeType.py +2 -2
  207. phoenix/server/api/types/Model.py +54 -54
  208. phoenix/server/api/types/PerformanceMetric.py +8 -5
  209. phoenix/server/api/types/Project.py +407 -142
  210. phoenix/server/api/types/ProjectSession.py +139 -0
  211. phoenix/server/api/types/Segments.py +4 -4
  212. phoenix/server/api/types/Span.py +221 -176
  213. phoenix/server/api/types/SpanAnnotation.py +43 -0
  214. phoenix/server/api/types/SpanIOValue.py +15 -0
  215. phoenix/server/api/types/SystemApiKey.py +9 -0
  216. phoenix/server/api/types/TemplateLanguage.py +10 -0
  217. phoenix/server/api/types/TimeSeries.py +19 -15
  218. phoenix/server/api/types/TokenUsage.py +11 -0
  219. phoenix/server/api/types/Trace.py +154 -0
  220. phoenix/server/api/types/TraceAnnotation.py +45 -0
  221. phoenix/server/api/types/UMAPPoints.py +7 -7
  222. phoenix/server/api/types/User.py +60 -0
  223. phoenix/server/api/types/UserApiKey.py +45 -0
  224. phoenix/server/api/types/UserRole.py +15 -0
  225. phoenix/server/api/types/node.py +4 -112
  226. phoenix/server/api/types/pagination.py +156 -57
  227. phoenix/server/api/utils.py +34 -0
  228. phoenix/server/app.py +864 -115
  229. phoenix/server/bearer_auth.py +163 -0
  230. phoenix/server/dml_event.py +136 -0
  231. phoenix/server/dml_event_handler.py +256 -0
  232. phoenix/server/email/__init__.py +0 -0
  233. phoenix/server/email/sender.py +97 -0
  234. phoenix/server/email/templates/__init__.py +0 -0
  235. phoenix/server/email/templates/password_reset.html +19 -0
  236. phoenix/server/email/types.py +11 -0
  237. phoenix/server/grpc_server.py +102 -0
  238. phoenix/server/jwt_store.py +505 -0
  239. phoenix/server/main.py +305 -116
  240. phoenix/server/oauth2.py +52 -0
  241. phoenix/server/openapi/__init__.py +0 -0
  242. phoenix/server/prometheus.py +111 -0
  243. phoenix/server/rate_limiters.py +188 -0
  244. phoenix/server/static/.vite/manifest.json +87 -0
  245. phoenix/server/static/assets/components-Cy9nwIvF.js +2125 -0
  246. phoenix/server/static/assets/index-BKvHIxkk.js +113 -0
  247. phoenix/server/static/assets/pages-CUi2xCVQ.js +4449 -0
  248. phoenix/server/static/assets/vendor-DvC8cT4X.js +894 -0
  249. phoenix/server/static/assets/vendor-DxkFTwjz.css +1 -0
  250. phoenix/server/static/assets/vendor-arizeai-Do1793cv.js +662 -0
  251. phoenix/server/static/assets/vendor-codemirror-BzwZPyJM.js +24 -0
  252. phoenix/server/static/assets/vendor-recharts-_Jb7JjhG.js +59 -0
  253. phoenix/server/static/assets/vendor-shiki-Cl9QBraO.js +5 -0
  254. phoenix/server/static/assets/vendor-three-DwGkEfCM.js +2998 -0
  255. phoenix/server/telemetry.py +68 -0
  256. phoenix/server/templates/index.html +82 -23
  257. phoenix/server/thread_server.py +3 -3
  258. phoenix/server/types.py +275 -0
  259. phoenix/services.py +27 -18
  260. phoenix/session/client.py +743 -68
  261. phoenix/session/data_extractor.py +31 -7
  262. phoenix/session/evaluation.py +3 -9
  263. phoenix/session/session.py +263 -219
  264. phoenix/settings.py +22 -0
  265. phoenix/trace/__init__.py +2 -22
  266. phoenix/trace/attributes.py +338 -0
  267. phoenix/trace/dsl/README.md +116 -0
  268. phoenix/trace/dsl/filter.py +663 -213
  269. phoenix/trace/dsl/helpers.py +73 -21
  270. phoenix/trace/dsl/query.py +574 -201
  271. phoenix/trace/exporter.py +24 -19
  272. phoenix/trace/fixtures.py +368 -32
  273. phoenix/trace/otel.py +71 -219
  274. phoenix/trace/projects.py +3 -2
  275. phoenix/trace/schemas.py +33 -11
  276. phoenix/trace/span_evaluations.py +21 -16
  277. phoenix/trace/span_json_decoder.py +6 -4
  278. phoenix/trace/span_json_encoder.py +2 -2
  279. phoenix/trace/trace_dataset.py +47 -32
  280. phoenix/trace/utils.py +21 -4
  281. phoenix/utilities/__init__.py +0 -26
  282. phoenix/utilities/client.py +132 -0
  283. phoenix/utilities/deprecation.py +31 -0
  284. phoenix/utilities/error_handling.py +3 -2
  285. phoenix/utilities/json.py +109 -0
  286. phoenix/utilities/logging.py +8 -0
  287. phoenix/utilities/project.py +2 -2
  288. phoenix/utilities/re.py +49 -0
  289. phoenix/utilities/span_store.py +0 -23
  290. phoenix/utilities/template_formatters.py +99 -0
  291. phoenix/version.py +1 -1
  292. arize_phoenix-3.16.1.dist-info/METADATA +0 -495
  293. arize_phoenix-3.16.1.dist-info/RECORD +0 -178
  294. phoenix/core/project.py +0 -619
  295. phoenix/core/traces.py +0 -96
  296. phoenix/experimental/evals/__init__.py +0 -73
  297. phoenix/experimental/evals/evaluators.py +0 -413
  298. phoenix/experimental/evals/functions/__init__.py +0 -4
  299. phoenix/experimental/evals/functions/classify.py +0 -453
  300. phoenix/experimental/evals/functions/executor.py +0 -353
  301. phoenix/experimental/evals/functions/generate.py +0 -138
  302. phoenix/experimental/evals/functions/processing.py +0 -76
  303. phoenix/experimental/evals/models/__init__.py +0 -14
  304. phoenix/experimental/evals/models/anthropic.py +0 -175
  305. phoenix/experimental/evals/models/base.py +0 -170
  306. phoenix/experimental/evals/models/bedrock.py +0 -221
  307. phoenix/experimental/evals/models/litellm.py +0 -134
  308. phoenix/experimental/evals/models/openai.py +0 -448
  309. phoenix/experimental/evals/models/rate_limiters.py +0 -246
  310. phoenix/experimental/evals/models/vertex.py +0 -173
  311. phoenix/experimental/evals/models/vertexai.py +0 -186
  312. phoenix/experimental/evals/retrievals.py +0 -96
  313. phoenix/experimental/evals/templates/__init__.py +0 -50
  314. phoenix/experimental/evals/templates/default_templates.py +0 -472
  315. phoenix/experimental/evals/templates/template.py +0 -195
  316. phoenix/experimental/evals/utils/__init__.py +0 -172
  317. phoenix/experimental/evals/utils/threads.py +0 -27
  318. phoenix/server/api/helpers.py +0 -11
  319. phoenix/server/api/routers/evaluation_handler.py +0 -109
  320. phoenix/server/api/routers/span_handler.py +0 -70
  321. phoenix/server/api/routers/trace_handler.py +0 -60
  322. phoenix/server/api/types/DatasetRole.py +0 -23
  323. phoenix/server/static/index.css +0 -6
  324. phoenix/server/static/index.js +0 -7447
  325. phoenix/storage/span_store/__init__.py +0 -23
  326. phoenix/storage/span_store/text_file.py +0 -85
  327. phoenix/trace/dsl/missing.py +0 -60
  328. phoenix/trace/langchain/__init__.py +0 -3
  329. phoenix/trace/langchain/instrumentor.py +0 -35
  330. phoenix/trace/llama_index/__init__.py +0 -3
  331. phoenix/trace/llama_index/callback.py +0 -102
  332. phoenix/trace/openai/__init__.py +0 -3
  333. phoenix/trace/openai/instrumentor.py +0 -30
  334. {arize_phoenix-3.16.1.dist-info → arize_phoenix-7.7.1.dist-info}/licenses/IP_NOTICE +0 -0
  335. {arize_phoenix-3.16.1.dist-info → arize_phoenix-7.7.1.dist-info}/licenses/LICENSE +0 -0
  336. /phoenix/{datasets → db/insertion}/__init__.py +0 -0
  337. /phoenix/{experimental → db/migrations}/__init__.py +0 -0
  338. /phoenix/{storage → db/migrations/data_migration_scripts}/__init__.py +0 -0
@@ -1,234 +1,483 @@
1
+ import operator
1
2
  from datetime import datetime
2
- from itertools import chain
3
- from typing import List, Optional
3
+ from typing import Any, ClassVar, Optional
4
4
 
5
5
  import strawberry
6
+ from aioitertools.itertools import islice
7
+ from openinference.semconv.trace import SpanAttributes
8
+ from sqlalchemy import and_, desc, distinct, func, or_, select
9
+ from sqlalchemy.orm import contains_eager
10
+ from sqlalchemy.sql.elements import ColumnElement
11
+ from sqlalchemy.sql.expression import tuple_
6
12
  from strawberry import ID, UNSET
13
+ from strawberry.relay import Connection, Node, NodeID
14
+ from strawberry.types import Info
15
+ from typing_extensions import assert_never
7
16
 
8
- from phoenix.core.project import Project as CoreProject
9
- from phoenix.metrics.retrieval_metrics import RetrievalMetrics
10
- from phoenix.server.api.input_types.SpanSort import SpanSort
17
+ from phoenix.datetime_utils import right_open_time_range
18
+ from phoenix.db import models
19
+ from phoenix.server.api.context import Context
20
+ from phoenix.server.api.input_types.ProjectSessionSort import (
21
+ ProjectSessionColumn,
22
+ ProjectSessionSort,
23
+ )
24
+ from phoenix.server.api.input_types.SpanSort import SpanSort, SpanSortConfig
11
25
  from phoenix.server.api.input_types.TimeRange import TimeRange
26
+ from phoenix.server.api.types.AnnotationSummary import AnnotationSummary
12
27
  from phoenix.server.api.types.DocumentEvaluationSummary import DocumentEvaluationSummary
13
- from phoenix.server.api.types.EvaluationSummary import EvaluationSummary
14
- from phoenix.server.api.types.node import Node
15
28
  from phoenix.server.api.types.pagination import (
16
- Connection,
17
- ConnectionArgs,
18
29
  Cursor,
19
- connection_from_list,
30
+ CursorSortColumn,
31
+ CursorString,
32
+ connection_from_cursors_and_nodes,
20
33
  )
34
+ from phoenix.server.api.types.ProjectSession import ProjectSession, to_gql_project_session
35
+ from phoenix.server.api.types.SortDir import SortDir
21
36
  from phoenix.server.api.types.Span import Span, to_gql_span
37
+ from phoenix.server.api.types.Trace import Trace, to_gql_trace
22
38
  from phoenix.server.api.types.ValidationResult import ValidationResult
23
39
  from phoenix.trace.dsl import SpanFilter
24
- from phoenix.trace.schemas import SpanID, TraceID
25
40
 
26
41
 
27
42
  @strawberry.type
28
43
  class Project(Node):
44
+ _table: ClassVar[type[models.Base]] = models.Project
45
+ id_attr: NodeID[int]
29
46
  name: str
30
- project: strawberry.Private[CoreProject]
47
+ gradient_start_color: str
48
+ gradient_end_color: str
31
49
 
32
50
  @strawberry.field
33
- def start_time(self) -> Optional[datetime]:
34
- start_time, _ = self.project.right_open_time_range
51
+ async def start_time(
52
+ self,
53
+ info: Info[Context, None],
54
+ ) -> Optional[datetime]:
55
+ start_time = await info.context.data_loaders.min_start_or_max_end_times.load(
56
+ (self.id_attr, "start"),
57
+ )
58
+ start_time, _ = right_open_time_range(start_time, None)
35
59
  return start_time
36
60
 
37
61
  @strawberry.field
38
- def end_time(self) -> Optional[datetime]:
39
- _, end_time = self.project.right_open_time_range
62
+ async def end_time(
63
+ self,
64
+ info: Info[Context, None],
65
+ ) -> Optional[datetime]:
66
+ end_time = await info.context.data_loaders.min_start_or_max_end_times.load(
67
+ (self.id_attr, "end"),
68
+ )
69
+ _, end_time = right_open_time_range(None, end_time)
40
70
  return end_time
41
71
 
42
72
  @strawberry.field
43
- def record_count(
73
+ async def record_count(
74
+ self,
75
+ info: Info[Context, None],
76
+ time_range: Optional[TimeRange] = UNSET,
77
+ filter_condition: Optional[str] = UNSET,
78
+ ) -> int:
79
+ return await info.context.data_loaders.record_counts.load(
80
+ ("span", self.id_attr, time_range, filter_condition),
81
+ )
82
+
83
+ @strawberry.field
84
+ async def trace_count(
85
+ self,
86
+ info: Info[Context, None],
87
+ time_range: Optional[TimeRange] = UNSET,
88
+ ) -> int:
89
+ return await info.context.data_loaders.record_counts.load(
90
+ ("trace", self.id_attr, time_range, None),
91
+ )
92
+
93
+ @strawberry.field
94
+ async def token_count_total(
44
95
  self,
96
+ info: Info[Context, None],
45
97
  time_range: Optional[TimeRange] = UNSET,
98
+ filter_condition: Optional[str] = UNSET,
99
+ ) -> int:
100
+ return await info.context.data_loaders.token_counts.load(
101
+ ("total", self.id_attr, time_range, filter_condition),
102
+ )
103
+
104
+ @strawberry.field
105
+ async def token_count_prompt(
106
+ self,
107
+ info: Info[Context, None],
108
+ time_range: Optional[TimeRange] = UNSET,
109
+ filter_condition: Optional[str] = UNSET,
46
110
  ) -> int:
47
- if not time_range:
48
- return self.project.span_count()
49
- return self.project.span_count(time_range.start, time_range.end)
111
+ return await info.context.data_loaders.token_counts.load(
112
+ ("prompt", self.id_attr, time_range, filter_condition),
113
+ )
50
114
 
51
115
  @strawberry.field
52
- def trace_count(
116
+ async def token_count_completion(
53
117
  self,
118
+ info: Info[Context, None],
54
119
  time_range: Optional[TimeRange] = UNSET,
120
+ filter_condition: Optional[str] = UNSET,
55
121
  ) -> int:
56
- if not time_range:
57
- return self.project.trace_count()
58
- return self.project.trace_count(time_range.start, time_range.end)
122
+ return await info.context.data_loaders.token_counts.load(
123
+ ("completion", self.id_attr, time_range, filter_condition),
124
+ )
59
125
 
60
126
  @strawberry.field
61
- def token_count_total(self) -> int:
62
- return self.project.token_count_total
127
+ async def latency_ms_quantile(
128
+ self,
129
+ info: Info[Context, None],
130
+ probability: float,
131
+ time_range: Optional[TimeRange] = UNSET,
132
+ ) -> Optional[float]:
133
+ return await info.context.data_loaders.latency_ms_quantile.load(
134
+ (
135
+ "trace",
136
+ self.id_attr,
137
+ time_range,
138
+ None,
139
+ probability,
140
+ ),
141
+ )
63
142
 
64
143
  @strawberry.field
65
- def latency_ms_p50(self) -> Optional[float]:
66
- return self.project.root_span_latency_ms_quantiles(0.50)
144
+ async def span_latency_ms_quantile(
145
+ self,
146
+ info: Info[Context, None],
147
+ probability: float,
148
+ time_range: Optional[TimeRange] = UNSET,
149
+ filter_condition: Optional[str] = UNSET,
150
+ ) -> Optional[float]:
151
+ return await info.context.data_loaders.latency_ms_quantile.load(
152
+ (
153
+ "span",
154
+ self.id_attr,
155
+ time_range,
156
+ filter_condition,
157
+ probability,
158
+ ),
159
+ )
67
160
 
68
161
  @strawberry.field
69
- def latency_ms_p99(self) -> Optional[float]:
70
- return self.project.root_span_latency_ms_quantiles(0.99)
162
+ async def trace(self, trace_id: ID, info: Info[Context, None]) -> Optional[Trace]:
163
+ stmt = (
164
+ select(models.Trace)
165
+ .where(models.Trace.trace_id == str(trace_id))
166
+ .where(models.Trace.project_rowid == self.id_attr)
167
+ )
168
+ async with info.context.db() as session:
169
+ if (trace := await session.scalar(stmt)) is None:
170
+ return None
171
+ return to_gql_trace(trace)
71
172
 
72
173
  @strawberry.field
73
- def spans(
174
+ async def spans(
74
175
  self,
176
+ info: Info[Context, None],
75
177
  time_range: Optional[TimeRange] = UNSET,
76
- trace_ids: Optional[List[ID]] = UNSET,
77
178
  first: Optional[int] = 50,
78
179
  last: Optional[int] = UNSET,
79
- after: Optional[Cursor] = UNSET,
80
- before: Optional[Cursor] = UNSET,
180
+ after: Optional[CursorString] = UNSET,
181
+ before: Optional[CursorString] = UNSET,
81
182
  sort: Optional[SpanSort] = UNSET,
82
183
  root_spans_only: Optional[bool] = UNSET,
83
184
  filter_condition: Optional[str] = UNSET,
84
185
  ) -> Connection[Span]:
85
- args = ConnectionArgs(
86
- first=first,
87
- after=after if isinstance(after, Cursor) else None,
88
- last=last,
89
- before=before if isinstance(before, Cursor) else None,
90
- )
91
- if not (project := self.project).span_count():
92
- return connection_from_list(data=[], args=args)
93
- predicate = (
94
- SpanFilter(
95
- condition=filter_condition,
96
- evals=project,
186
+ stmt = (
187
+ select(models.Span)
188
+ .join(models.Trace)
189
+ .where(models.Trace.project_rowid == self.id_attr)
190
+ .options(contains_eager(models.Span.trace).load_only(models.Trace.trace_id))
191
+ )
192
+ if time_range:
193
+ stmt = stmt.where(
194
+ and_(
195
+ time_range.start <= models.Span.start_time,
196
+ models.Span.start_time < time_range.end,
197
+ )
97
198
  )
98
- if filter_condition
99
- else None
100
- )
101
- if not trace_ids:
102
- spans = project.get_spans(
103
- start_time=time_range.start if time_range else None,
104
- stop_time=time_range.end if time_range else None,
105
- root_spans_only=root_spans_only,
199
+ if root_spans_only:
200
+ # A root span is any span whose parent span is missing in the
201
+ # database, even if its `parent_span_id` may not be NULL.
202
+ parent = select(models.Span.span_id).alias()
203
+ stmt = stmt.outerjoin(
204
+ parent,
205
+ models.Span.parent_id == parent.c.span_id,
206
+ ).where(parent.c.span_id.is_(None))
207
+ if filter_condition:
208
+ span_filter = SpanFilter(condition=filter_condition)
209
+ stmt = span_filter(stmt)
210
+ sort_config: Optional[SpanSortConfig] = None
211
+ cursor_rowid_column: Any = models.Span.id
212
+ if sort:
213
+ sort_config = sort.update_orm_expr(stmt)
214
+ stmt = sort_config.stmt
215
+ if sort_config.dir is SortDir.desc:
216
+ cursor_rowid_column = desc(cursor_rowid_column)
217
+ if after:
218
+ cursor = Cursor.from_string(after)
219
+ if sort_config and cursor.sort_column:
220
+ sort_column = cursor.sort_column
221
+ compare = operator.lt if sort_config.dir is SortDir.desc else operator.gt
222
+ stmt = stmt.where(
223
+ compare(
224
+ tuple_(sort_config.orm_expression, models.Span.id),
225
+ (sort_column.value, cursor.rowid),
226
+ )
227
+ )
228
+ else:
229
+ stmt = stmt.where(models.Span.id > cursor.rowid)
230
+ if first:
231
+ stmt = stmt.limit(
232
+ first + 1 # overfetch by one to determine whether there's a next page
106
233
  )
234
+ stmt = stmt.order_by(cursor_rowid_column)
235
+ cursors_and_nodes = []
236
+ async with info.context.db() as session:
237
+ span_records = await session.execute(stmt)
238
+ async for span_record in islice(span_records, first):
239
+ span = span_record[0]
240
+ cursor = Cursor(rowid=span.id)
241
+ if sort_config:
242
+ assert len(span_record) > 1
243
+ cursor.sort_column = CursorSortColumn(
244
+ type=sort_config.column_data_type,
245
+ value=span_record[1],
246
+ )
247
+ cursors_and_nodes.append((cursor, to_gql_span(span)))
248
+ has_next_page = True
249
+ try:
250
+ next(span_records)
251
+ except StopIteration:
252
+ has_next_page = False
253
+
254
+ return connection_from_cursors_and_nodes(
255
+ cursors_and_nodes,
256
+ has_previous_page=False,
257
+ has_next_page=has_next_page,
258
+ )
259
+
260
+ @strawberry.field
261
+ async def sessions(
262
+ self,
263
+ info: Info[Context, None],
264
+ time_range: Optional[TimeRange] = UNSET,
265
+ first: Optional[int] = 50,
266
+ after: Optional[CursorString] = UNSET,
267
+ sort: Optional[ProjectSessionSort] = UNSET,
268
+ filter_io_substring: Optional[str] = UNSET,
269
+ ) -> Connection[ProjectSession]:
270
+ table = models.ProjectSession
271
+ stmt = select(table).filter_by(project_id=self.id_attr)
272
+ if time_range:
273
+ if time_range.start:
274
+ stmt = stmt.where(time_range.start <= table.start_time)
275
+ if time_range.end:
276
+ stmt = stmt.where(table.start_time < time_range.end)
277
+ if filter_io_substring:
278
+ filter_subq = (
279
+ stmt.with_only_columns(distinct(table.id).label("id"))
280
+ .join_from(table, models.Trace)
281
+ .join_from(models.Trace, models.Span)
282
+ .where(models.Span.parent_id.is_(None))
283
+ .where(
284
+ or_(
285
+ models.TextContains(
286
+ models.Span.attributes[INPUT_VALUE].as_string(),
287
+ filter_io_substring,
288
+ ),
289
+ models.TextContains(
290
+ models.Span.attributes[OUTPUT_VALUE].as_string(),
291
+ filter_io_substring,
292
+ ),
293
+ )
294
+ )
295
+ ).subquery()
296
+ stmt = stmt.join(filter_subq, table.id == filter_subq.c.id)
297
+ if sort:
298
+ key: ColumnElement[Any]
299
+ if sort.col is ProjectSessionColumn.startTime:
300
+ key = table.start_time.label("key")
301
+ elif sort.col is ProjectSessionColumn.endTime:
302
+ key = table.end_time.label("key")
303
+ elif (
304
+ sort.col is ProjectSessionColumn.tokenCountTotal
305
+ or sort.col is ProjectSessionColumn.numTraces
306
+ ):
307
+ if sort.col is ProjectSessionColumn.tokenCountTotal:
308
+ sort_subq = (
309
+ select(
310
+ models.Trace.project_session_rowid.label("id"),
311
+ func.sum(models.Span.cumulative_llm_token_count_total).label("key"),
312
+ )
313
+ .join_from(models.Trace, models.Span)
314
+ .where(models.Span.parent_id.is_(None))
315
+ .group_by(models.Trace.project_session_rowid)
316
+ ).subquery()
317
+ elif sort.col is ProjectSessionColumn.numTraces:
318
+ sort_subq = (
319
+ select(
320
+ models.Trace.project_session_rowid.label("id"),
321
+ func.count(models.Trace.id).label("key"),
322
+ ).group_by(models.Trace.project_session_rowid)
323
+ ).subquery()
324
+ else:
325
+ assert_never(sort.col)
326
+ key = sort_subq.c.key
327
+ stmt = stmt.join(sort_subq, table.id == sort_subq.c.id)
328
+ else:
329
+ assert_never(sort.col)
330
+ stmt = stmt.add_columns(key)
331
+ if sort.dir is SortDir.asc:
332
+ stmt = stmt.order_by(key.asc(), table.id.asc())
333
+ else:
334
+ stmt = stmt.order_by(key.desc(), table.id.desc())
335
+ if after:
336
+ cursor = Cursor.from_string(after)
337
+ assert cursor.sort_column is not None
338
+ compare = operator.lt if sort.dir is SortDir.desc else operator.gt
339
+ stmt = stmt.where(
340
+ compare(
341
+ tuple_(key, table.id),
342
+ (cursor.sort_column.value, cursor.rowid),
343
+ )
344
+ )
107
345
  else:
108
- spans = chain.from_iterable(
109
- project.get_trace(trace_id) for trace_id in map(TraceID, trace_ids)
346
+ stmt = stmt.order_by(table.id.desc())
347
+ if after:
348
+ cursor = Cursor.from_string(after)
349
+ stmt = stmt.where(table.id < cursor.rowid)
350
+ if first:
351
+ stmt = stmt.limit(
352
+ first + 1 # over-fetch by one to determine whether there's a next page
110
353
  )
111
- if predicate:
112
- spans = filter(predicate, spans)
113
- if sort:
114
- spans = sort(spans, evals=project)
115
- data = [to_gql_span(span, project) for span in spans]
116
- return connection_from_list(data=data, args=args)
354
+ cursors_and_nodes = []
355
+ async with info.context.db() as session:
356
+ records = await session.stream(stmt)
357
+ async for record in islice(records, first):
358
+ project_session = record[0]
359
+ cursor = Cursor(rowid=project_session.id)
360
+ if sort:
361
+ assert len(record) > 1
362
+ cursor.sort_column = CursorSortColumn(
363
+ type=sort.col.data_type,
364
+ value=record[1],
365
+ )
366
+ cursors_and_nodes.append((cursor, to_gql_project_session(project_session)))
367
+ has_next_page = True
368
+ try:
369
+ await records.__anext__()
370
+ except StopAsyncIteration:
371
+ has_next_page = False
372
+ return connection_from_cursors_and_nodes(
373
+ cursors_and_nodes,
374
+ has_previous_page=False,
375
+ has_next_page=has_next_page,
376
+ )
117
377
 
118
378
  @strawberry.field(
119
- description="Names of all available evaluations for spans. "
379
+ description="Names of all available annotations for traces. "
120
380
  "(The list contains no duplicates.)"
121
381
  ) # type: ignore
122
- def span_evaluation_names(self) -> List[str]:
123
- return self.project.get_span_evaluation_names()
382
+ async def trace_annotations_names(
383
+ self,
384
+ info: Info[Context, None],
385
+ ) -> list[str]:
386
+ stmt = (
387
+ select(distinct(models.TraceAnnotation.name))
388
+ .join(models.Trace)
389
+ .where(models.Trace.project_rowid == self.id_attr)
390
+ )
391
+ async with info.context.db() as session:
392
+ return list(await session.scalars(stmt))
393
+
394
+ @strawberry.field(
395
+ description="Names of all available annotations for spans. "
396
+ "(The list contains no duplicates.)"
397
+ ) # type: ignore
398
+ async def span_annotation_names(
399
+ self,
400
+ info: Info[Context, None],
401
+ ) -> list[str]:
402
+ stmt = (
403
+ select(distinct(models.SpanAnnotation.name))
404
+ .join(models.Span)
405
+ .join(models.Trace, models.Span.trace_rowid == models.Trace.id)
406
+ .where(models.Trace.project_rowid == self.id_attr)
407
+ )
408
+ async with info.context.db() as session:
409
+ return list(await session.scalars(stmt))
124
410
 
125
411
  @strawberry.field(
126
412
  description="Names of available document evaluations.",
127
413
  ) # type: ignore
128
- def document_evaluation_names(
414
+ async def document_evaluation_names(
129
415
  self,
416
+ info: Info[Context, None],
130
417
  span_id: Optional[ID] = UNSET,
131
- ) -> List[str]:
132
- return self.project.get_document_evaluation_names(
133
- None if span_id is UNSET else SpanID(span_id),
418
+ ) -> list[str]:
419
+ stmt = (
420
+ select(distinct(models.DocumentAnnotation.name))
421
+ .join(models.Span)
422
+ .join(models.Trace, models.Span.trace_rowid == models.Trace.id)
423
+ .where(models.Trace.project_rowid == self.id_attr)
424
+ .where(models.DocumentAnnotation.annotator_kind == "LLM")
134
425
  )
426
+ if span_id:
427
+ stmt = stmt.where(models.Span.span_id == str(span_id))
428
+ async with info.context.db() as session:
429
+ return list(await session.scalars(stmt))
135
430
 
136
431
  @strawberry.field
137
- def span_evaluation_summary(
432
+ async def trace_annotation_summary(
138
433
  self,
139
- evaluation_name: str,
434
+ info: Info[Context, None],
435
+ annotation_name: str,
436
+ time_range: Optional[TimeRange] = UNSET,
437
+ ) -> Optional[AnnotationSummary]:
438
+ return await info.context.data_loaders.annotation_summaries.load(
439
+ ("trace", self.id_attr, time_range, None, annotation_name),
440
+ )
441
+
442
+ @strawberry.field
443
+ async def span_annotation_summary(
444
+ self,
445
+ info: Info[Context, None],
446
+ annotation_name: str,
140
447
  time_range: Optional[TimeRange] = UNSET,
141
448
  filter_condition: Optional[str] = UNSET,
142
- ) -> Optional[EvaluationSummary]:
143
- project = self.project
144
- predicate = (
145
- SpanFilter(
146
- condition=filter_condition,
147
- evals=project,
148
- )
149
- if filter_condition
150
- else None
151
- )
152
- span_ids = project.get_span_evaluation_span_ids(evaluation_name)
153
- if not span_ids:
154
- return None
155
- spans = project.get_spans(
156
- start_time=time_range.start if time_range else None,
157
- stop_time=time_range.end if time_range else None,
158
- span_ids=span_ids,
159
- )
160
- if predicate:
161
- spans = filter(predicate, spans)
162
- evaluations = tuple(
163
- evaluation
164
- for span in spans
165
- if (
166
- evaluation := project.get_span_evaluation(
167
- span.context.span_id,
168
- evaluation_name,
169
- )
170
- )
171
- is not None
449
+ ) -> Optional[AnnotationSummary]:
450
+ return await info.context.data_loaders.annotation_summaries.load(
451
+ ("span", self.id_attr, time_range, filter_condition, annotation_name),
172
452
  )
173
- if not evaluations:
174
- return None
175
- labels = project.get_span_evaluation_labels(evaluation_name)
176
- return EvaluationSummary(evaluations, labels)
177
453
 
178
454
  @strawberry.field
179
- def document_evaluation_summary(
455
+ async def document_evaluation_summary(
180
456
  self,
457
+ info: Info[Context, None],
181
458
  evaluation_name: str,
182
459
  time_range: Optional[TimeRange] = UNSET,
183
460
  filter_condition: Optional[str] = UNSET,
184
461
  ) -> Optional[DocumentEvaluationSummary]:
185
- project = self.project
186
- predicate = (
187
- SpanFilter(condition=filter_condition, evals=project) if filter_condition else None
188
- )
189
- span_ids = project.get_document_evaluation_span_ids(evaluation_name)
190
- if not span_ids:
191
- return None
192
- spans = project.get_spans(
193
- start_time=time_range.start if time_range else None,
194
- stop_time=time_range.end if time_range else None,
195
- span_ids=span_ids,
196
- )
197
- if predicate:
198
- spans = filter(predicate, spans)
199
- metrics_collection = []
200
- for span in spans:
201
- span_id = span.context.span_id
202
- num_documents = project.get_num_documents(span_id)
203
- if not num_documents:
204
- continue
205
- evaluation_scores = project.get_document_evaluation_scores(
206
- span_id=span_id,
207
- evaluation_name=evaluation_name,
208
- num_documents=num_documents,
209
- )
210
- metrics_collection.append(RetrievalMetrics(evaluation_scores))
211
- if not metrics_collection:
212
- return None
213
- return DocumentEvaluationSummary(
214
- evaluation_name=evaluation_name,
215
- metrics_collection=metrics_collection,
462
+ return await info.context.data_loaders.document_evaluation_summaries.load(
463
+ (self.id_attr, time_range, filter_condition, evaluation_name),
216
464
  )
217
465
 
218
466
  @strawberry.field
219
467
  def streaming_last_updated_at(
220
468
  self,
469
+ info: Info[Context, None],
221
470
  ) -> Optional[datetime]:
222
- return self.project.last_updated_at
471
+ return info.context.last_updated_at.get(self._table, self.id_attr)
223
472
 
224
473
  @strawberry.field
225
- def validate_span_filter_condition(self, condition: str) -> ValidationResult:
226
- valid_eval_names = self.project.get_span_evaluation_names()
474
+ async def validate_span_filter_condition(self, condition: str) -> ValidationResult:
475
+ # This query is too expensive to run on every validation
476
+ # valid_eval_names = await self.span_annotation_names()
227
477
  try:
228
478
  SpanFilter(
229
479
  condition=condition,
230
- evals=self.project,
231
- valid_eval_names=valid_eval_names,
480
+ # valid_eval_names=valid_eval_names,
232
481
  )
233
482
  return ValidationResult(is_valid=True, error_message=None)
234
483
  except SyntaxError as e:
@@ -236,3 +485,19 @@ class Project(Node):
236
485
  is_valid=False,
237
486
  error_message=e.msg,
238
487
  )
488
+
489
+
490
+ def to_gql_project(project: models.Project) -> Project:
491
+ """
492
+ Converts an ORM project to a GraphQL Project.
493
+ """
494
+ return Project(
495
+ id_attr=project.id,
496
+ name=project.name,
497
+ gradient_start_color=project.gradient_start_color,
498
+ gradient_end_color=project.gradient_end_color,
499
+ )
500
+
501
+
502
+ INPUT_VALUE = SpanAttributes.INPUT_VALUE.split(".")
503
+ OUTPUT_VALUE = SpanAttributes.OUTPUT_VALUE.split(".")