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
phoenix/core/project.py DELETED
@@ -1,619 +0,0 @@
1
- import logging
2
- from collections import defaultdict
3
- from datetime import datetime, timezone
4
- from threading import RLock
5
- from types import MappingProxyType
6
- from typing import (
7
- Any,
8
- DefaultDict,
9
- Dict,
10
- Iterable,
11
- Iterator,
12
- List,
13
- Mapping,
14
- Optional,
15
- Set,
16
- Sized,
17
- Tuple,
18
- Union,
19
- cast,
20
- )
21
-
22
- import numpy as np
23
- from ddsketch import DDSketch
24
- from google.protobuf.json_format import MessageToDict
25
- from openinference.semconv.trace import SpanAttributes
26
- from pandas import DataFrame, Index, MultiIndex
27
- from sortedcontainers import SortedKeyList
28
- from typing_extensions import TypeAlias, assert_never
29
- from wrapt import ObjectProxy
30
-
31
- import phoenix.trace.v1 as pb
32
- from phoenix.datetime_utils import right_open_time_range
33
- from phoenix.trace import DocumentEvaluations, Evaluations, SpanEvaluations
34
- from phoenix.trace.schemas import (
35
- ComputedAttributes,
36
- Span,
37
- SpanID,
38
- SpanStatusCode,
39
- TraceID,
40
- )
41
-
42
- logger = logging.getLogger(__name__)
43
- logger.addHandler(logging.NullHandler())
44
-
45
- END_OF_QUEUE = None # sentinel value for queue termination
46
-
47
-
48
- class WrappedSpan(ObjectProxy): # type: ignore
49
- """
50
- A wrapped Span object with __getitem__ and __setitem__ methods for accessing
51
- computed attributes.
52
- """
53
-
54
- def __init__(self, span: Span) -> None:
55
- super().__init__(span)
56
- self._self_computed_values: Dict[ComputedAttributes, Union[float, int]] = {}
57
-
58
- def get_computed_value(self, key: str) -> Optional[Union[float, int]]:
59
- try:
60
- attr = ComputedAttributes(key)
61
- except Exception:
62
- return None
63
- return self._self_computed_values.get(attr)
64
-
65
- def __getitem__(self, key: Union[str, ComputedAttributes]) -> Any:
66
- if isinstance(key, ComputedAttributes):
67
- return self._self_computed_values.get(key)
68
- return self.__wrapped__.attributes.get(key)
69
-
70
- def __setitem__(self, key: ComputedAttributes, value: Any) -> None:
71
- if not isinstance(key, ComputedAttributes):
72
- raise KeyError(f"{key} is not a computed value")
73
- self._self_computed_values[key] = value
74
-
75
- def __eq__(self, other: Any) -> bool:
76
- return self is other
77
-
78
- def __hash__(self) -> int:
79
- return id(self)
80
-
81
-
82
- _ParentSpanID: TypeAlias = SpanID
83
- _ChildSpanID: TypeAlias = SpanID
84
- _ProjectName: TypeAlias = str
85
-
86
-
87
- EvaluationName: TypeAlias = str
88
- DocumentPosition: TypeAlias = int
89
-
90
-
91
- class Project:
92
- def __init__(self) -> None:
93
- self._spans = _Spans()
94
- self._evals = _Evals()
95
- self._is_archived = False
96
-
97
- @property
98
- def last_updated_at(self) -> Optional[datetime]:
99
- spans_last_updated_at = self._spans.last_updated_at
100
- evals_last_updated_at = self._evals.last_updated_at
101
- if (
102
- not spans_last_updated_at
103
- or evals_last_updated_at
104
- and evals_last_updated_at > spans_last_updated_at
105
- ):
106
- return evals_last_updated_at
107
- return spans_last_updated_at
108
-
109
- def add_span(self, span: Span) -> None:
110
- self._spans.add(WrappedSpan(span))
111
-
112
- def add_eval(self, pb_eval: pb.Evaluation) -> None:
113
- self._evals.add(pb_eval)
114
-
115
- def get_trace(self, trace_id: TraceID) -> Iterator[WrappedSpan]:
116
- yield from self._spans.get_trace(trace_id)
117
-
118
- def get_spans(
119
- self,
120
- start_time: Optional[datetime] = None,
121
- stop_time: Optional[datetime] = None,
122
- root_spans_only: Optional[bool] = False,
123
- span_ids: Optional[Iterable[SpanID]] = None,
124
- ) -> Iterator[WrappedSpan]:
125
- yield from self._spans.get_spans(start_time, stop_time, root_spans_only, span_ids)
126
-
127
- def get_num_documents(self, span_id: SpanID) -> int:
128
- return self._spans.get_num_documents(span_id)
129
-
130
- def root_span_latency_ms_quantiles(self, probability: float) -> Optional[float]:
131
- """Root span latency quantiles in milliseconds"""
132
- return self._spans.root_span_latency_ms_quantiles(probability)
133
-
134
- def get_descendant_spans(self, span_id: SpanID) -> Iterator[WrappedSpan]:
135
- yield from self._spans.get_descendant_spans(span_id)
136
-
137
- def span_count(
138
- self,
139
- start_time: Optional[datetime] = None,
140
- stop_time: Optional[datetime] = None,
141
- ) -> int:
142
- return self._spans.span_count(start_time, stop_time)
143
-
144
- def trace_count(
145
- self,
146
- start_time: Optional[datetime] = None,
147
- stop_time: Optional[datetime] = None,
148
- ) -> int:
149
- return self._spans.trace_count(start_time, stop_time)
150
-
151
- @property
152
- def token_count_total(self) -> int:
153
- return self._spans.token_count_total
154
-
155
- @property
156
- def right_open_time_range(self) -> Tuple[Optional[datetime], Optional[datetime]]:
157
- return self._spans.right_open_time_range
158
-
159
- def get_span_evaluation(self, span_id: SpanID, name: str) -> Optional[pb.Evaluation]:
160
- return self._evals.get_span_evaluation(span_id, name)
161
-
162
- def get_span_evaluation_names(self) -> List[EvaluationName]:
163
- return self._evals.get_span_evaluation_names()
164
-
165
- def get_document_evaluation_names(
166
- self,
167
- span_id: Optional[SpanID] = None,
168
- ) -> List[EvaluationName]:
169
- return self._evals.get_document_evaluation_names(span_id)
170
-
171
- def get_span_evaluation_labels(self, name: EvaluationName) -> Tuple[str, ...]:
172
- return self._evals.get_span_evaluation_labels(name)
173
-
174
- def get_span_evaluation_span_ids(self, name: EvaluationName) -> Tuple[SpanID, ...]:
175
- return self._evals.get_span_evaluation_span_ids(name)
176
-
177
- def get_evaluations_by_span_id(self, span_id: SpanID) -> List[pb.Evaluation]:
178
- return self._evals.get_evaluations_by_span_id(span_id)
179
-
180
- def get_document_evaluation_span_ids(self, name: EvaluationName) -> Tuple[SpanID, ...]:
181
- return self._evals.get_document_evaluation_span_ids(name)
182
-
183
- def get_document_evaluations_by_span_id(self, span_id: SpanID) -> List[pb.Evaluation]:
184
- return self._evals.get_document_evaluations_by_span_id(span_id)
185
-
186
- def get_document_evaluation_scores(
187
- self,
188
- span_id: SpanID,
189
- evaluation_name: str,
190
- num_documents: int,
191
- ) -> List[float]:
192
- return self._evals.get_document_evaluation_scores(span_id, evaluation_name, num_documents)
193
-
194
- def export_evaluations(self) -> List[Evaluations]:
195
- return self._evals.export_evaluations()
196
-
197
- def archive(self) -> None:
198
- self._is_archived = True
199
-
200
- @property
201
- def is_archived(self) -> bool:
202
- return self._is_archived
203
-
204
-
205
- class _Spans:
206
- def __init__(self) -> None:
207
- self._lock = RLock()
208
- self._spans: Dict[SpanID, WrappedSpan] = {}
209
- self._parent_span_ids: Dict[SpanID, _ParentSpanID] = {}
210
- self._traces: DefaultDict[TraceID, Set[WrappedSpan]] = defaultdict(set)
211
- self._child_spans: DefaultDict[SpanID, Set[WrappedSpan]] = defaultdict(set)
212
- self._num_documents: DefaultDict[SpanID, int] = defaultdict(int)
213
- self._start_time_sorted_spans: SortedKeyList[WrappedSpan] = SortedKeyList(
214
- key=lambda span: span.start_time,
215
- )
216
- self._start_time_sorted_root_spans: SortedKeyList[WrappedSpan] = SortedKeyList(
217
- key=lambda span: span.start_time,
218
- )
219
- self._latency_sorted_root_spans: SortedKeyList[WrappedSpan] = SortedKeyList(
220
- key=lambda span: span[ComputedAttributes.LATENCY_MS],
221
- )
222
- self._root_span_latency_ms_sketch = DDSketch()
223
- self._token_count_total: int = 0
224
- self._last_updated_at: Optional[datetime] = None
225
-
226
- def get_trace(self, trace_id: TraceID) -> Iterator[WrappedSpan]:
227
- with self._lock:
228
- # make a copy because source data can mutate during iteration
229
- if not (trace := self._traces.get(trace_id)):
230
- return
231
- spans = tuple(trace)
232
- for span in spans:
233
- yield span
234
-
235
- def get_spans(
236
- self,
237
- start_time: Optional[datetime] = None,
238
- stop_time: Optional[datetime] = None,
239
- root_spans_only: Optional[bool] = False,
240
- span_ids: Optional[Iterable[SpanID]] = None,
241
- ) -> Iterator[WrappedSpan]:
242
- if not self._spans:
243
- return
244
- if start_time is None or stop_time is None:
245
- min_start_time, max_stop_time = cast(
246
- Tuple[datetime, datetime],
247
- self.right_open_time_range,
248
- )
249
- start_time = start_time or min_start_time
250
- stop_time = stop_time or max_stop_time
251
- if span_ids is not None:
252
- with self._lock:
253
- spans = tuple(
254
- span
255
- for span_id in span_ids
256
- if (
257
- (span := self._spans.get(span_id))
258
- and start_time <= span.start_time < stop_time
259
- and (not root_spans_only or span.parent_id is None)
260
- )
261
- )
262
- else:
263
- sorted_spans = (
264
- self._start_time_sorted_root_spans
265
- if root_spans_only
266
- else self._start_time_sorted_spans
267
- )
268
- # make a copy because source data can mutate during iteration
269
- with self._lock:
270
- spans = tuple(
271
- sorted_spans.irange_key(
272
- start_time.astimezone(timezone.utc),
273
- stop_time.astimezone(timezone.utc),
274
- inclusive=(True, False),
275
- reverse=True, # most recent spans first
276
- )
277
- )
278
- for span in spans:
279
- yield span
280
-
281
- def get_num_documents(self, span_id: SpanID) -> int:
282
- with self._lock:
283
- return self._num_documents.get(span_id) or 0
284
-
285
- def root_span_latency_ms_quantiles(self, probability: float) -> Optional[float]:
286
- """Root span latency quantiles in milliseconds"""
287
- with self._lock:
288
- return self._root_span_latency_ms_sketch.get_quantile_value(probability)
289
-
290
- def get_descendant_spans(self, span_id: SpanID) -> Iterator[WrappedSpan]:
291
- for span in self._get_descendant_spans(span_id):
292
- yield span
293
-
294
- def _get_descendant_spans(self, span_id: SpanID) -> Iterator[WrappedSpan]:
295
- with self._lock:
296
- # make a copy because source data can mutate during iteration
297
- if not (child_spans := self._child_spans.get(span_id)):
298
- return
299
- spans = tuple(child_spans)
300
- for child_span in spans:
301
- yield child_span
302
- yield from self._get_descendant_spans(child_span.context.span_id)
303
-
304
- @property
305
- def last_updated_at(self) -> Optional[datetime]:
306
- return self._last_updated_at
307
-
308
- def span_count(
309
- self,
310
- start_time: Optional[datetime] = None,
311
- stop_time: Optional[datetime] = None,
312
- ) -> int:
313
- _index = self._start_time_sorted_spans.bisect_key_left
314
- with self._lock:
315
- start: int = _index(start_time) if start_time else 0
316
- stop: int = _index(stop_time) if stop_time else len(self._spans)
317
- return stop - start
318
-
319
- def trace_count(
320
- self,
321
- start_time: Optional[datetime] = None,
322
- stop_time: Optional[datetime] = None,
323
- ) -> int:
324
- _index = self._start_time_sorted_root_spans.bisect_key_left
325
- with self._lock:
326
- start: int = _index(start_time) if start_time else 0
327
- stop: int = _index(stop_time) if stop_time else len(self._traces)
328
- return stop - start
329
-
330
- @property
331
- def token_count_total(self) -> int:
332
- return self._token_count_total
333
-
334
- @property
335
- def right_open_time_range(self) -> Tuple[Optional[datetime], Optional[datetime]]:
336
- with self._lock:
337
- if not self._start_time_sorted_spans:
338
- return None, None
339
- first_span = self._start_time_sorted_spans[0]
340
- last_span = self._start_time_sorted_spans[-1]
341
- min_start_time = first_span.start_time
342
- max_start_time = last_span.start_time
343
- return right_open_time_range(min_start_time, max_start_time)
344
-
345
- def add(self, span: WrappedSpan) -> None:
346
- with self._lock:
347
- self._add_span(span)
348
-
349
- def _add_span(self, span: WrappedSpan) -> None:
350
- span_id = span.context.span_id
351
- if span_id in self._spans:
352
- # Update is not allowed.
353
- return
354
-
355
- parent_span_id = span.parent_id
356
- is_root_span = parent_span_id is None
357
- if not is_root_span:
358
- self._child_spans[parent_span_id].add(span)
359
- self._parent_span_ids[span_id] = parent_span_id
360
-
361
- # Add computed attributes to span
362
- start_time = span.start_time
363
- end_time = span.end_time
364
- span[ComputedAttributes.LATENCY_MS] = latency = (
365
- end_time - start_time
366
- ).total_seconds() * 1000
367
- if is_root_span:
368
- self._root_span_latency_ms_sketch.add(latency)
369
- span[ComputedAttributes.ERROR_COUNT] = int(span.status_code is SpanStatusCode.ERROR)
370
-
371
- # Store the new span (after adding computed attributes)
372
- self._spans[span_id] = span
373
- self._traces[span.context.trace_id].add(span)
374
- self._start_time_sorted_spans.add(span)
375
- if is_root_span:
376
- self._start_time_sorted_root_spans.add(span)
377
- self._latency_sorted_root_spans.add(span)
378
- self._propagate_cumulative_values(span)
379
- self._update_cached_statistics(span)
380
-
381
- # Update last updated timestamp, letting users know
382
- # when they should refresh the page.
383
- self._last_updated_at = datetime.now(timezone.utc)
384
-
385
- def _update_cached_statistics(self, span: WrappedSpan) -> None:
386
- # Update statistics for quick access later
387
- span_id = span.context.span_id
388
- if token_count_update := span.attributes.get(SpanAttributes.LLM_TOKEN_COUNT_TOTAL):
389
- self._token_count_total += token_count_update
390
- if isinstance(
391
- (retrieval_documents := span.attributes.get(SpanAttributes.RETRIEVAL_DOCUMENTS)),
392
- Sized,
393
- ) and (num_documents_update := len(retrieval_documents)):
394
- self._num_documents[span_id] += num_documents_update
395
-
396
- def _propagate_cumulative_values(self, span: WrappedSpan) -> None:
397
- child_spans: Iterable[WrappedSpan] = self._child_spans.get(span.context.span_id) or ()
398
- for cumulative_attribute, attribute in _CUMULATIVE_ATTRIBUTES.items():
399
- span[cumulative_attribute] = span[attribute] or 0
400
- for child_span in child_spans:
401
- span[cumulative_attribute] += child_span[cumulative_attribute] or 0
402
- self._update_ancestors(span)
403
-
404
- def _update_ancestors(self, span: WrappedSpan) -> None:
405
- # Add cumulative values to each of the span's ancestors.
406
- span_id = span.context.span_id
407
- for attribute in _CUMULATIVE_ATTRIBUTES.keys():
408
- value = span[attribute] or 0
409
- self._add_value_to_span_ancestors(span_id, attribute, value)
410
-
411
- def _add_value_to_span_ancestors(
412
- self,
413
- span_id: SpanID,
414
- attribute: ComputedAttributes,
415
- value: float,
416
- ) -> None:
417
- while parent_span_id := self._parent_span_ids.get(span_id):
418
- if not (parent_span := self._spans.get(parent_span_id)):
419
- return
420
- cumulative_value = parent_span[attribute] or 0
421
- parent_span[attribute] = cumulative_value + value
422
- span_id = parent_span_id
423
-
424
-
425
- class _Evals:
426
- def __init__(self) -> None:
427
- self._lock = RLock()
428
- self._trace_evaluations_by_name: DefaultDict[
429
- EvaluationName, Dict[TraceID, pb.Evaluation]
430
- ] = defaultdict(dict)
431
- self._evaluations_by_trace_id: DefaultDict[TraceID, Dict[EvaluationName, pb.Evaluation]] = (
432
- defaultdict(dict)
433
- )
434
- self._span_evaluations_by_name: DefaultDict[EvaluationName, Dict[SpanID, pb.Evaluation]] = (
435
- defaultdict(dict)
436
- )
437
- self._evaluations_by_span_id: DefaultDict[SpanID, Dict[EvaluationName, pb.Evaluation]] = (
438
- defaultdict(dict)
439
- )
440
- self._span_evaluation_labels: DefaultDict[EvaluationName, Set[str]] = defaultdict(set)
441
- self._document_evaluations_by_span_id: DefaultDict[
442
- SpanID, DefaultDict[EvaluationName, Dict[DocumentPosition, pb.Evaluation]]
443
- ] = defaultdict(lambda: defaultdict(dict))
444
- self._document_evaluations_by_name: DefaultDict[
445
- EvaluationName, DefaultDict[SpanID, Dict[DocumentPosition, pb.Evaluation]]
446
- ] = defaultdict(lambda: defaultdict(dict))
447
- self._last_updated_at: Optional[datetime] = None
448
-
449
- def add(self, evaluation: pb.Evaluation) -> None:
450
- with self._lock:
451
- self._add(evaluation)
452
-
453
- def _add(self, evaluation: pb.Evaluation) -> None:
454
- subject_id = evaluation.subject_id
455
- name = evaluation.name
456
- subject_id_kind = subject_id.WhichOneof("kind")
457
- if subject_id_kind == "document_retrieval_id":
458
- document_retrieval_id = subject_id.document_retrieval_id
459
- span_id = SpanID(document_retrieval_id.span_id)
460
- document_position = document_retrieval_id.document_position
461
- self._document_evaluations_by_span_id[span_id][name][document_position] = evaluation
462
- self._document_evaluations_by_name[name][span_id][document_position] = evaluation
463
- elif subject_id_kind == "span_id":
464
- span_id = SpanID(subject_id.span_id)
465
- self._evaluations_by_span_id[span_id][name] = evaluation
466
- self._span_evaluations_by_name[name][span_id] = evaluation
467
- if evaluation.result.HasField("label"):
468
- label = evaluation.result.label.value
469
- self._span_evaluation_labels[name].add(label)
470
- elif subject_id_kind == "trace_id":
471
- trace_id = TraceID(subject_id.trace_id)
472
- self._evaluations_by_trace_id[trace_id][name] = evaluation
473
- self._trace_evaluations_by_name[name][trace_id] = evaluation
474
- elif subject_id_kind is None:
475
- logger.warning(
476
- f"discarding evaluation with missing subject_id: {MessageToDict(evaluation)}"
477
- )
478
- else:
479
- assert_never(subject_id_kind)
480
- self._last_updated_at = datetime.now(timezone.utc)
481
-
482
- @property
483
- def last_updated_at(self) -> Optional[datetime]:
484
- return self._last_updated_at
485
-
486
- def get_span_evaluation(self, span_id: SpanID, name: str) -> Optional[pb.Evaluation]:
487
- with self._lock:
488
- span_evaluations = self._evaluations_by_span_id.get(span_id)
489
- return span_evaluations.get(name) if span_evaluations else None
490
-
491
- def get_span_evaluation_names(self) -> List[EvaluationName]:
492
- with self._lock:
493
- return list(self._span_evaluations_by_name)
494
-
495
- def get_document_evaluation_names(
496
- self,
497
- span_id: Optional[SpanID] = None,
498
- ) -> List[EvaluationName]:
499
- with self._lock:
500
- if span_id is None:
501
- return list(self._document_evaluations_by_name)
502
- document_evaluations = self._document_evaluations_by_span_id.get(span_id)
503
- return list(document_evaluations) if document_evaluations else []
504
-
505
- def get_span_evaluation_labels(self, name: EvaluationName) -> Tuple[str, ...]:
506
- with self._lock:
507
- labels = self._span_evaluation_labels.get(name)
508
- return tuple(labels) if labels else ()
509
-
510
- def get_span_evaluation_span_ids(self, name: EvaluationName) -> Tuple[SpanID, ...]:
511
- with self._lock:
512
- span_evaluations = self._span_evaluations_by_name.get(name)
513
- return tuple(span_evaluations.keys()) if span_evaluations else ()
514
-
515
- def get_evaluations_by_span_id(self, span_id: SpanID) -> List[pb.Evaluation]:
516
- with self._lock:
517
- evaluations = self._evaluations_by_span_id.get(span_id)
518
- return list(evaluations.values()) if evaluations else []
519
-
520
- def get_document_evaluation_span_ids(self, name: EvaluationName) -> Tuple[SpanID, ...]:
521
- with self._lock:
522
- document_evaluations = self._document_evaluations_by_name.get(name)
523
- return tuple(document_evaluations.keys()) if document_evaluations else ()
524
-
525
- def get_document_evaluations_by_span_id(self, span_id: SpanID) -> List[pb.Evaluation]:
526
- all_evaluations: List[pb.Evaluation] = []
527
- with self._lock:
528
- document_evaluations = self._document_evaluations_by_span_id.get(span_id)
529
- if not document_evaluations:
530
- return all_evaluations
531
- for evaluations in document_evaluations.values():
532
- all_evaluations.extend(evaluations.values())
533
- return all_evaluations
534
-
535
- def get_document_evaluation_scores(
536
- self,
537
- span_id: SpanID,
538
- evaluation_name: str,
539
- num_documents: int,
540
- ) -> List[float]:
541
- # num_documents is needed as argument because the document position values
542
- # are not checked during ingestion: e.g. if there exists a position value
543
- # of one trillion, we would not want to create a result that large.
544
- scores: List[float] = [np.nan] * num_documents
545
- with self._lock:
546
- document_evaluations = self._document_evaluations_by_span_id.get(span_id)
547
- if not document_evaluations:
548
- return scores
549
- evaluations = document_evaluations.get(evaluation_name)
550
- if not evaluations:
551
- return scores
552
- for document_position, evaluation in evaluations.items():
553
- result = evaluation.result
554
- if result.HasField("score") and document_position < num_documents:
555
- scores[document_position] = result.score.value
556
- return scores
557
-
558
- def export_evaluations(self) -> List[Evaluations]:
559
- evaluations: List[Evaluations] = []
560
- evaluations.extend(self._export_span_evaluations())
561
- evaluations.extend(self._export_document_evaluations())
562
- return evaluations
563
-
564
- def _export_span_evaluations(self) -> List[SpanEvaluations]:
565
- span_evaluations = []
566
- with self._lock:
567
- span_evaluations_by_name = tuple(self._span_evaluations_by_name.items())
568
- for eval_name, _span_evaluations_by_id in span_evaluations_by_name:
569
- span_ids = []
570
- rows = []
571
- with self._lock:
572
- span_evaluations_by_id = tuple(_span_evaluations_by_id.items())
573
- for span_id, pb_eval in span_evaluations_by_id:
574
- span_ids.append(span_id)
575
- rows.append(MessageToDict(pb_eval.result))
576
- dataframe = DataFrame(rows, index=Index(span_ids, name="context.span_id"))
577
- span_evaluations.append(SpanEvaluations(eval_name, dataframe))
578
- return span_evaluations
579
-
580
- def _export_document_evaluations(self) -> List[DocumentEvaluations]:
581
- evaluations = []
582
- with self._lock:
583
- document_evaluations_by_name = tuple(self._document_evaluations_by_name.items())
584
- for eval_name, _document_evaluations_by_id in document_evaluations_by_name:
585
- span_ids = []
586
- document_positions = []
587
- rows = []
588
- with self._lock:
589
- document_evaluations_by_id = tuple(_document_evaluations_by_id.items())
590
- for span_id, _document_evaluations_by_position in document_evaluations_by_id:
591
- with self._lock:
592
- document_evaluations_by_position = sorted(
593
- _document_evaluations_by_position.items()
594
- ) # ensure the evals are sorted by document position
595
- for document_position, pb_eval in document_evaluations_by_position:
596
- span_ids.append(span_id)
597
- document_positions.append(document_position)
598
- rows.append(MessageToDict(pb_eval.result))
599
- dataframe = DataFrame(
600
- rows,
601
- index=MultiIndex.from_arrays(
602
- (span_ids, document_positions),
603
- names=("context.span_id", "document_position"),
604
- ),
605
- )
606
- evaluations.append(DocumentEvaluations(eval_name, dataframe))
607
- return evaluations
608
-
609
-
610
- _CUMULATIVE_ATTRIBUTES: Mapping[ComputedAttributes, Union[str, ComputedAttributes]] = (
611
- MappingProxyType(
612
- {
613
- ComputedAttributes.CUMULATIVE_LLM_TOKEN_COUNT_TOTAL: SpanAttributes.LLM_TOKEN_COUNT_TOTAL, # noqa: E501
614
- ComputedAttributes.CUMULATIVE_LLM_TOKEN_COUNT_PROMPT: SpanAttributes.LLM_TOKEN_COUNT_PROMPT, # noqa: E501
615
- ComputedAttributes.CUMULATIVE_LLM_TOKEN_COUNT_COMPLETION: SpanAttributes.LLM_TOKEN_COUNT_COMPLETION, # noqa: E501
616
- ComputedAttributes.CUMULATIVE_ERROR_COUNT: ComputedAttributes.ERROR_COUNT,
617
- }
618
- )
619
- )