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
@@ -1,58 +1,116 @@
1
+ from collections import defaultdict
2
+ from dataclasses import asdict, dataclass
1
3
  from datetime import datetime
2
- from typing import TYPE_CHECKING, Annotated, ClassVar, Optional, Type
4
+ from typing import TYPE_CHECKING, Annotated, Optional
3
5
 
6
+ import pandas as pd
4
7
  import strawberry
5
8
  from openinference.semconv.trace import SpanAttributes
6
9
  from sqlalchemy import select
7
- from strawberry import UNSET, Info, Private, lazy
8
- from strawberry.relay import Connection, GlobalID, Node, NodeID
10
+ from strawberry import UNSET, Info, lazy
11
+ from strawberry.relay import Connection, Node, NodeID
9
12
 
10
13
  from phoenix.db import models
11
14
  from phoenix.server.api.context import Context
15
+ from phoenix.server.api.input_types.AnnotationFilter import AnnotationFilter, satisfies_filter
16
+ from phoenix.server.api.types.AnnotationSummary import AnnotationSummary
17
+ from phoenix.server.api.types.CostBreakdown import CostBreakdown
12
18
  from phoenix.server.api.types.MimeType import MimeType
13
19
  from phoenix.server.api.types.pagination import ConnectionArgs, CursorString, connection_from_list
20
+ from phoenix.server.api.types.SpanCostDetailSummaryEntry import SpanCostDetailSummaryEntry
21
+ from phoenix.server.api.types.SpanCostSummary import SpanCostSummary
14
22
  from phoenix.server.api.types.SpanIOValue import SpanIOValue
15
23
  from phoenix.server.api.types.TokenUsage import TokenUsage
16
24
 
17
25
  if TYPE_CHECKING:
26
+ from phoenix.server.api.types.Project import Project
27
+ from phoenix.server.api.types.ProjectSessionAnnotation import ProjectSessionAnnotation
18
28
  from phoenix.server.api.types.Trace import Trace
19
29
 
20
30
 
21
31
  @strawberry.type
22
32
  class ProjectSession(Node):
23
- _table: ClassVar[Type[models.ProjectSession]] = models.ProjectSession
24
- id_attr: NodeID[int]
25
- project_rowid: Private[int]
26
- session_id: str
27
- start_time: datetime
28
- end_time: datetime
33
+ id: NodeID[int]
34
+ db_record: strawberry.Private[Optional[models.ProjectSession]] = None
35
+
36
+ def __post_init__(self) -> None:
37
+ if self.db_record and self.id != self.db_record.id:
38
+ raise ValueError("ProjectSession ID mismatch")
39
+
40
+ @strawberry.field
41
+ async def session_id(
42
+ self,
43
+ info: Info[Context, None],
44
+ ) -> str:
45
+ if self.db_record:
46
+ val = self.db_record.session_id
47
+ else:
48
+ val = await info.context.data_loaders.project_session_fields.load(
49
+ (self.id, models.ProjectSession.session_id),
50
+ )
51
+ return val
52
+
53
+ @strawberry.field
54
+ async def start_time(
55
+ self,
56
+ info: Info[Context, None],
57
+ ) -> datetime:
58
+ if self.db_record:
59
+ val = self.db_record.start_time
60
+ else:
61
+ val = await info.context.data_loaders.project_session_fields.load(
62
+ (self.id, models.ProjectSession.start_time),
63
+ )
64
+ return val
65
+
66
+ @strawberry.field
67
+ async def end_time(
68
+ self,
69
+ info: Info[Context, None],
70
+ ) -> datetime:
71
+ if self.db_record:
72
+ val = self.db_record.end_time
73
+ else:
74
+ val = await info.context.data_loaders.project_session_fields.load(
75
+ (self.id, models.ProjectSession.end_time),
76
+ )
77
+ return val
29
78
 
30
79
  @strawberry.field
31
- async def project_id(self) -> GlobalID:
80
+ async def project(
81
+ self,
82
+ info: Info[Context, None],
83
+ ) -> Annotated["Project", lazy(".Project")]:
32
84
  from phoenix.server.api.types.Project import Project
33
85
 
34
- return GlobalID(type_name=Project.__name__, node_id=str(self.project_rowid))
86
+ if self.db_record:
87
+ project_rowid = self.db_record.project_id
88
+ else:
89
+ project_rowid = await info.context.data_loaders.project_session_fields.load(
90
+ (self.id, models.ProjectSession.project_id),
91
+ )
92
+ return Project(id=project_rowid)
35
93
 
36
94
  @strawberry.field
37
95
  async def num_traces(
38
96
  self,
39
97
  info: Info[Context, None],
40
98
  ) -> int:
41
- return await info.context.data_loaders.session_num_traces.load(self.id_attr)
99
+ return await info.context.data_loaders.session_num_traces.load(self.id)
42
100
 
43
101
  @strawberry.field
44
102
  async def num_traces_with_error(
45
103
  self,
46
104
  info: Info[Context, None],
47
105
  ) -> int:
48
- return await info.context.data_loaders.session_num_traces_with_error.load(self.id_attr)
106
+ return await info.context.data_loaders.session_num_traces_with_error.load(self.id)
49
107
 
50
108
  @strawberry.field
51
109
  async def first_input(
52
110
  self,
53
111
  info: Info[Context, None],
54
112
  ) -> Optional[SpanIOValue]:
55
- record = await info.context.data_loaders.session_first_inputs.load(self.id_attr)
113
+ record = await info.context.data_loaders.session_first_inputs.load(self.id)
56
114
  if record is None:
57
115
  return None
58
116
  return SpanIOValue(
@@ -65,7 +123,7 @@ class ProjectSession(Node):
65
123
  self,
66
124
  info: Info[Context, None],
67
125
  ) -> Optional[SpanIOValue]:
68
- record = await info.context.data_loaders.session_last_outputs.load(self.id_attr)
126
+ record = await info.context.data_loaders.session_last_outputs.load(self.id)
69
127
  if record is None:
70
128
  return None
71
129
  return SpanIOValue(
@@ -78,7 +136,7 @@ class ProjectSession(Node):
78
136
  self,
79
137
  info: Info[Context, None],
80
138
  ) -> TokenUsage:
81
- usage = await info.context.data_loaders.session_token_usages.load(self.id_attr)
139
+ usage = await info.context.data_loaders.session_token_usages.load(self.id)
82
140
  return TokenUsage(
83
141
  prompt=usage.prompt,
84
142
  completion=usage.completion,
@@ -103,13 +161,12 @@ class ProjectSession(Node):
103
161
  )
104
162
  stmt = (
105
163
  select(models.Trace)
106
- .filter_by(project_session_rowid=self.id_attr)
164
+ .filter_by(project_session_rowid=self.id)
107
165
  .order_by(models.Trace.start_time)
108
- .limit(first)
109
166
  )
110
167
  async with info.context.db() as session:
111
168
  traces = await session.stream_scalars(stmt)
112
- data = [Trace(trace_rowid=trace.id, db_trace=trace) async for trace in traces]
169
+ data = [Trace(id=trace.id, db_record=trace) async for trace in traces]
113
170
  return connection_from_list(data=data, args=args)
114
171
 
115
172
  @strawberry.field
@@ -119,18 +176,121 @@ class ProjectSession(Node):
119
176
  probability: float,
120
177
  ) -> Optional[float]:
121
178
  return await info.context.data_loaders.session_trace_latency_ms_quantile.load(
122
- (self.id_attr, probability)
179
+ (self.id, probability)
180
+ )
181
+
182
+ @strawberry.field
183
+ async def cost_summary(
184
+ self,
185
+ info: Info[Context, None],
186
+ ) -> SpanCostSummary:
187
+ loader = info.context.data_loaders.span_cost_summary_by_project_session
188
+ summary = await loader.load(self.id)
189
+ return SpanCostSummary(
190
+ prompt=CostBreakdown(
191
+ tokens=summary.prompt.tokens,
192
+ cost=summary.prompt.cost,
193
+ ),
194
+ completion=CostBreakdown(
195
+ tokens=summary.completion.tokens,
196
+ cost=summary.completion.cost,
197
+ ),
198
+ total=CostBreakdown(
199
+ tokens=summary.total.tokens,
200
+ cost=summary.total.cost,
201
+ ),
123
202
  )
124
203
 
204
+ @strawberry.field
205
+ async def cost_detail_summary_entries(
206
+ self,
207
+ info: Info[Context, None],
208
+ ) -> list[SpanCostDetailSummaryEntry]:
209
+ loader = info.context.data_loaders.span_cost_detail_summary_entries_by_project_session
210
+ summary = await loader.load(self.id)
211
+ return [
212
+ SpanCostDetailSummaryEntry(
213
+ token_type=entry.token_type,
214
+ is_prompt=entry.is_prompt,
215
+ value=CostBreakdown(
216
+ tokens=entry.value.tokens,
217
+ cost=entry.value.cost,
218
+ ),
219
+ )
220
+ for entry in summary
221
+ ]
222
+
223
+ @strawberry.field
224
+ async def session_annotations(
225
+ self,
226
+ info: Info[Context, None],
227
+ ) -> list[Annotated["ProjectSessionAnnotation", lazy(".ProjectSessionAnnotation")]]:
228
+ """Get all annotations for this session."""
229
+ from .ProjectSessionAnnotation import ProjectSessionAnnotation
230
+
231
+ stmt = select(models.ProjectSessionAnnotation).filter_by(project_session_id=self.id)
232
+ async with info.context.db() as session:
233
+ annotations = await session.stream_scalars(stmt)
234
+ return [
235
+ ProjectSessionAnnotation(id=annotation.id, db_record=annotation)
236
+ async for annotation in annotations
237
+ ]
238
+
239
+ @strawberry.field(
240
+ description="Summarizes each annotation (by name) associated with the session"
241
+ ) # type: ignore
242
+ async def session_annotation_summaries(
243
+ self,
244
+ info: Info[Context, None],
245
+ filter: Optional[AnnotationFilter] = None,
246
+ ) -> list[AnnotationSummary]:
247
+ """
248
+ Retrieves and summarizes annotations associated with this span.
249
+
250
+ This method aggregates annotation data by name and label, calculating metrics
251
+ such as count of occurrences and sum of scores. The results are organized
252
+ into a structured format that can be easily converted to a DataFrame.
253
+
254
+ Args:
255
+ info: GraphQL context information
256
+ filter: Optional filter to apply to annotations before processing
257
+
258
+ Returns:
259
+ A list of AnnotationSummary objects, each containing:
260
+ - name: The name of the annotation
261
+ - data: A list of dictionaries with label statistics
262
+ """
263
+ # Load all annotations for this span from the data loader
264
+ annotations = await info.context.data_loaders.session_annotations_by_session.load(self.id)
265
+
266
+ # Apply filter if provided to narrow down the annotations
267
+ if filter:
268
+ annotations = [
269
+ annotation for annotation in annotations if satisfies_filter(annotation, filter)
270
+ ]
271
+
272
+ @dataclass
273
+ class Metrics:
274
+ record_count: int = 0
275
+ label_count: int = 0
276
+ score_sum: float = 0
277
+ score_count: int = 0
278
+
279
+ summaries: defaultdict[str, defaultdict[Optional[str], Metrics]] = defaultdict(
280
+ lambda: defaultdict(Metrics)
281
+ )
282
+ for annotation in annotations:
283
+ metrics = summaries[annotation.name][annotation.label]
284
+ metrics.record_count += 1
285
+ metrics.label_count += int(annotation.label is not None)
286
+ metrics.score_sum += annotation.score or 0
287
+ metrics.score_count += int(annotation.score is not None)
125
288
 
126
- def to_gql_project_session(project_session: models.ProjectSession) -> ProjectSession:
127
- return ProjectSession(
128
- id_attr=project_session.id,
129
- session_id=project_session.session_id,
130
- start_time=project_session.start_time,
131
- project_rowid=project_session.project_id,
132
- end_time=project_session.end_time,
133
- )
289
+ result: list[AnnotationSummary] = []
290
+ for name, label_metrics in summaries.items():
291
+ rows = [{"label": label, **asdict(metrics)} for label, metrics in label_metrics.items()]
292
+ result.append(AnnotationSummary(name=name, df=pd.DataFrame(rows), simple_avg=True))
293
+ return result
134
294
 
135
295
 
136
296
  INPUT_VALUE = SpanAttributes.INPUT_VALUE.split(".")
@@ -0,0 +1,187 @@
1
+ from math import isfinite
2
+ from typing import TYPE_CHECKING, Annotated, Optional
3
+
4
+ import strawberry
5
+ from strawberry.relay import GlobalID, Node, NodeID
6
+ from strawberry.scalars import JSON
7
+ from strawberry.types import Info
8
+
9
+ from phoenix.db import models
10
+ from phoenix.server.api.context import Context
11
+ from phoenix.server.api.types.AnnotatorKind import AnnotatorKind
12
+
13
+ from .Annotation import Annotation
14
+ from .AnnotationSource import AnnotationSource
15
+
16
+ if TYPE_CHECKING:
17
+ from .ProjectSession import ProjectSession
18
+ from .User import User
19
+
20
+
21
+ @strawberry.type
22
+ class ProjectSessionAnnotation(Node, Annotation):
23
+ id: NodeID[int]
24
+ db_record: strawberry.Private[Optional[models.ProjectSessionAnnotation]] = None
25
+
26
+ def __post_init__(self) -> None:
27
+ if self.db_record and self.id != self.db_record.id:
28
+ raise ValueError("ProjectSessionAnnotation ID mismatch")
29
+
30
+ @strawberry.field(description="Name of the annotation, e.g. 'helpfulness' or 'relevance'.") # type: ignore
31
+ async def name(
32
+ self,
33
+ info: Info[Context, None],
34
+ ) -> str:
35
+ if self.db_record:
36
+ val = self.db_record.name
37
+ else:
38
+ val = await info.context.data_loaders.project_session_annotation_fields.load(
39
+ (self.id, models.ProjectSessionAnnotation.name),
40
+ )
41
+ return val
42
+
43
+ @strawberry.field(description="The kind of annotator that produced the annotation.") # type: ignore
44
+ async def annotator_kind(
45
+ self,
46
+ info: Info[Context, None],
47
+ ) -> AnnotatorKind:
48
+ if self.db_record:
49
+ val = self.db_record.annotator_kind
50
+ else:
51
+ val = await info.context.data_loaders.project_session_annotation_fields.load(
52
+ (self.id, models.ProjectSessionAnnotation.annotator_kind),
53
+ )
54
+ return AnnotatorKind(val)
55
+
56
+ @strawberry.field(
57
+ description="Value of the annotation in the form of a string, e.g. 'helpful' or 'not helpful'. Note that the label is not necessarily binary." # noqa: E501
58
+ ) # type: ignore
59
+ async def label(
60
+ self,
61
+ info: Info[Context, None],
62
+ ) -> Optional[str]:
63
+ if self.db_record:
64
+ val = self.db_record.label
65
+ else:
66
+ val = await info.context.data_loaders.project_session_annotation_fields.load(
67
+ (self.id, models.ProjectSessionAnnotation.label),
68
+ )
69
+ return val
70
+
71
+ @strawberry.field(description="Value of the annotation in the form of a numeric score.") # type: ignore
72
+ async def score(
73
+ self,
74
+ info: Info[Context, None],
75
+ ) -> Optional[float]:
76
+ if self.db_record:
77
+ val = self.db_record.score
78
+ else:
79
+ val = await info.context.data_loaders.project_session_annotation_fields.load(
80
+ (self.id, models.ProjectSessionAnnotation.score),
81
+ )
82
+ return val if val is not None and isfinite(val) else None
83
+
84
+ @strawberry.field(
85
+ description="The annotator's explanation for the annotation result (i.e. score or label, or both) given to the subject." # noqa: E501
86
+ ) # type: ignore
87
+ async def explanation(
88
+ self,
89
+ info: Info[Context, None],
90
+ ) -> Optional[str]:
91
+ if self.db_record:
92
+ val = self.db_record.explanation
93
+ else:
94
+ val = await info.context.data_loaders.project_session_annotation_fields.load(
95
+ (self.id, models.ProjectSessionAnnotation.explanation),
96
+ )
97
+ return val
98
+
99
+ @strawberry.field(description="Metadata about the annotation.") # type: ignore
100
+ async def metadata(
101
+ self,
102
+ info: Info[Context, None],
103
+ ) -> JSON:
104
+ if self.db_record:
105
+ val = self.db_record.metadata_
106
+ else:
107
+ val = await info.context.data_loaders.project_session_annotation_fields.load(
108
+ (self.id, models.ProjectSessionAnnotation.metadata_),
109
+ )
110
+ return val
111
+
112
+ @strawberry.field(description="The identifier of the annotation.") # type: ignore
113
+ async def identifier(
114
+ self,
115
+ info: Info[Context, None],
116
+ ) -> str:
117
+ if self.db_record:
118
+ val = self.db_record.identifier
119
+ else:
120
+ val = await info.context.data_loaders.project_session_annotation_fields.load(
121
+ (self.id, models.ProjectSessionAnnotation.identifier),
122
+ )
123
+ return val
124
+
125
+ @strawberry.field(description="The source of the annotation.") # type: ignore
126
+ async def source(
127
+ self,
128
+ info: Info[Context, None],
129
+ ) -> AnnotationSource:
130
+ if self.db_record:
131
+ val = self.db_record.source
132
+ else:
133
+ val = await info.context.data_loaders.project_session_annotation_fields.load(
134
+ (self.id, models.ProjectSessionAnnotation.source),
135
+ )
136
+ return AnnotationSource(val)
137
+
138
+ @strawberry.field(description="The project session associated with the annotation.") # type: ignore
139
+ async def project_session_id(
140
+ self,
141
+ info: Info[Context, None],
142
+ ) -> GlobalID:
143
+ from phoenix.server.api.types.ProjectSession import ProjectSession
144
+
145
+ if self.db_record:
146
+ project_session_id = self.db_record.project_session_id
147
+ else:
148
+ project_session_id = (
149
+ await info.context.data_loaders.project_session_annotation_fields.load(
150
+ (self.id, models.ProjectSessionAnnotation.project_session_id),
151
+ )
152
+ )
153
+ return GlobalID(type_name=ProjectSession.__name__, node_id=str(project_session_id))
154
+
155
+ @strawberry.field(description="The project session associated with the annotation.") # type: ignore
156
+ async def project_session(
157
+ self,
158
+ info: Info[Context, None],
159
+ ) -> Annotated["ProjectSession", strawberry.lazy(".ProjectSession")]:
160
+ if self.db_record:
161
+ project_session_id = self.db_record.project_session_id
162
+ else:
163
+ project_session_id = (
164
+ await info.context.data_loaders.project_session_annotation_fields.load(
165
+ (self.id, models.ProjectSessionAnnotation.project_session_id),
166
+ )
167
+ )
168
+ from .ProjectSession import ProjectSession
169
+
170
+ return ProjectSession(id=project_session_id)
171
+
172
+ @strawberry.field(description="The user that produced the annotation.") # type: ignore
173
+ async def user(
174
+ self,
175
+ info: Info[Context, None],
176
+ ) -> Optional[Annotated["User", strawberry.lazy(".User")]]:
177
+ if self.db_record:
178
+ user_id = self.db_record.user_id
179
+ else:
180
+ user_id = await info.context.data_loaders.project_session_annotation_fields.load(
181
+ (self.id, models.ProjectSessionAnnotation.user_id),
182
+ )
183
+ if user_id is None:
184
+ return None
185
+ from .User import User
186
+
187
+ return User(id=user_id)
@@ -106,5 +106,5 @@ class ProjectTraceRetentionPolicy(Node):
106
106
  project_rowids = await info.context.data_loaders.projects_by_trace_retention_policy_id.load(
107
107
  self.id
108
108
  )
109
- data = [Project(project_rowid=project_rowid) for project_rowid in project_rowids]
109
+ data = [Project(id=project_rowid) for project_rowid in project_rowids]
110
110
  return connection_from_list(data=data, args=args)