agenta 0.57.0__py3-none-any.whl → 0.63.2__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 (267) hide show
  1. agenta/__init__.py +12 -3
  2. agenta/client/__init__.py +4 -4
  3. agenta/client/backend/__init__.py +4 -4
  4. agenta/client/backend/api_keys/client.py +2 -2
  5. agenta/client/backend/billing/client.py +2 -2
  6. agenta/client/backend/billing/raw_client.py +2 -2
  7. agenta/client/backend/client.py +56 -48
  8. agenta/client/backend/core/client_wrapper.py +2 -2
  9. agenta/client/backend/core/file.py +3 -1
  10. agenta/client/backend/core/http_client.py +3 -3
  11. agenta/client/backend/core/pydantic_utilities.py +13 -3
  12. agenta/client/backend/human_evaluations/client.py +2 -2
  13. agenta/client/backend/human_evaluations/raw_client.py +2 -2
  14. agenta/client/backend/organization/client.py +46 -34
  15. agenta/client/backend/organization/raw_client.py +32 -26
  16. agenta/client/backend/raw_client.py +26 -26
  17. agenta/client/backend/testsets/client.py +18 -18
  18. agenta/client/backend/testsets/raw_client.py +30 -30
  19. agenta/client/backend/types/__init__.py +4 -4
  20. agenta/client/backend/types/account_request.py +3 -1
  21. agenta/client/backend/types/account_response.py +3 -1
  22. agenta/client/backend/types/agenta_node_dto.py +3 -1
  23. agenta/client/backend/types/agenta_nodes_response.py +3 -1
  24. agenta/client/backend/types/agenta_root_dto.py +3 -1
  25. agenta/client/backend/types/agenta_roots_response.py +3 -1
  26. agenta/client/backend/types/agenta_tree_dto.py +3 -1
  27. agenta/client/backend/types/agenta_trees_response.py +3 -1
  28. agenta/client/backend/types/aggregated_result.py +3 -1
  29. agenta/client/backend/types/analytics_response.py +3 -1
  30. agenta/client/backend/types/annotation.py +6 -4
  31. agenta/client/backend/types/annotation_create.py +3 -1
  32. agenta/client/backend/types/annotation_edit.py +3 -1
  33. agenta/client/backend/types/annotation_link.py +3 -1
  34. agenta/client/backend/types/annotation_link_response.py +3 -1
  35. agenta/client/backend/types/annotation_query.py +3 -1
  36. agenta/client/backend/types/annotation_query_request.py +3 -1
  37. agenta/client/backend/types/annotation_reference.py +3 -1
  38. agenta/client/backend/types/annotation_references.py +3 -1
  39. agenta/client/backend/types/annotation_response.py +3 -1
  40. agenta/client/backend/types/annotations_response.py +3 -1
  41. agenta/client/backend/types/app.py +3 -1
  42. agenta/client/backend/types/app_variant_response.py +3 -1
  43. agenta/client/backend/types/app_variant_revision.py +3 -1
  44. agenta/client/backend/types/artifact.py +6 -4
  45. agenta/client/backend/types/base_output.py +3 -1
  46. agenta/client/backend/types/body_fetch_workflow_revision.py +3 -1
  47. agenta/client/backend/types/body_import_testset.py +3 -1
  48. agenta/client/backend/types/bucket_dto.py +3 -1
  49. agenta/client/backend/types/collect_status_response.py +3 -1
  50. agenta/client/backend/types/config_db.py +3 -1
  51. agenta/client/backend/types/config_dto.py +3 -1
  52. agenta/client/backend/types/config_response_model.py +3 -1
  53. agenta/client/backend/types/correct_answer.py +3 -1
  54. agenta/client/backend/types/create_app_output.py +3 -1
  55. agenta/client/backend/types/custom_model_settings_dto.py +3 -1
  56. agenta/client/backend/types/custom_provider_dto.py +3 -1
  57. agenta/client/backend/types/custom_provider_kind.py +1 -1
  58. agenta/client/backend/types/custom_provider_settings_dto.py +3 -1
  59. agenta/client/backend/types/delete_evaluation.py +3 -1
  60. agenta/client/backend/types/environment_output.py +3 -1
  61. agenta/client/backend/types/environment_output_extended.py +3 -1
  62. agenta/client/backend/types/environment_revision.py +3 -1
  63. agenta/client/backend/types/error.py +3 -1
  64. agenta/client/backend/types/evaluation.py +3 -1
  65. agenta/client/backend/types/evaluation_scenario.py +3 -1
  66. agenta/client/backend/types/evaluation_scenario_input.py +3 -1
  67. agenta/client/backend/types/evaluation_scenario_output.py +3 -1
  68. agenta/client/backend/types/evaluation_scenario_result.py +3 -1
  69. agenta/client/backend/types/evaluator.py +6 -4
  70. agenta/client/backend/types/evaluator_config.py +6 -4
  71. agenta/client/backend/types/evaluator_flags.py +3 -1
  72. agenta/client/backend/types/evaluator_mapping_output_interface.py +3 -1
  73. agenta/client/backend/types/evaluator_output_interface.py +3 -1
  74. agenta/client/backend/types/evaluator_query.py +3 -1
  75. agenta/client/backend/types/evaluator_query_request.py +3 -1
  76. agenta/client/backend/types/evaluator_request.py +3 -1
  77. agenta/client/backend/types/evaluator_response.py +3 -1
  78. agenta/client/backend/types/evaluators_response.py +3 -1
  79. agenta/client/backend/types/exception_dto.py +3 -1
  80. agenta/client/backend/types/extended_o_tel_tracing_response.py +3 -1
  81. agenta/client/backend/types/get_config_response.py +3 -1
  82. agenta/client/backend/types/header.py +3 -1
  83. agenta/client/backend/types/http_validation_error.py +3 -1
  84. agenta/client/backend/types/human_evaluation.py +3 -1
  85. agenta/client/backend/types/human_evaluation_scenario.py +3 -1
  86. agenta/client/backend/types/human_evaluation_scenario_input.py +3 -1
  87. agenta/client/backend/types/human_evaluation_scenario_output.py +3 -1
  88. agenta/client/backend/types/invite_request.py +3 -1
  89. agenta/client/backend/types/legacy_analytics_response.py +3 -1
  90. agenta/client/backend/types/legacy_data_point.py +3 -1
  91. agenta/client/backend/types/legacy_evaluator.py +3 -1
  92. agenta/client/backend/types/legacy_scope_request.py +3 -1
  93. agenta/client/backend/types/legacy_scopes_response.py +3 -1
  94. agenta/client/backend/types/legacy_subscription_request.py +3 -1
  95. agenta/client/backend/types/legacy_user_request.py +3 -1
  96. agenta/client/backend/types/legacy_user_response.py +3 -1
  97. agenta/client/backend/types/lifecycle_dto.py +3 -1
  98. agenta/client/backend/types/link_dto.py +3 -1
  99. agenta/client/backend/types/list_api_keys_response.py +3 -1
  100. agenta/client/backend/types/llm_run_rate_limit.py +3 -1
  101. agenta/client/backend/types/meta_request.py +3 -1
  102. agenta/client/backend/types/metrics_dto.py +3 -1
  103. agenta/client/backend/types/new_testset.py +3 -1
  104. agenta/client/backend/types/node_dto.py +3 -1
  105. agenta/client/backend/types/o_tel_context_dto.py +3 -1
  106. agenta/client/backend/types/o_tel_event.py +6 -4
  107. agenta/client/backend/types/o_tel_event_dto.py +3 -1
  108. agenta/client/backend/types/o_tel_extra_dto.py +3 -1
  109. agenta/client/backend/types/o_tel_flat_span.py +6 -4
  110. agenta/client/backend/types/o_tel_link.py +6 -4
  111. agenta/client/backend/types/o_tel_link_dto.py +3 -1
  112. agenta/client/backend/types/o_tel_links_response.py +3 -1
  113. agenta/client/backend/types/o_tel_span.py +1 -1
  114. agenta/client/backend/types/o_tel_span_dto.py +3 -1
  115. agenta/client/backend/types/o_tel_spans_tree.py +3 -1
  116. agenta/client/backend/types/o_tel_tracing_data_response.py +3 -1
  117. agenta/client/backend/types/o_tel_tracing_request.py +3 -1
  118. agenta/client/backend/types/o_tel_tracing_response.py +3 -1
  119. agenta/client/backend/types/organization.py +3 -1
  120. agenta/client/backend/types/organization_details.py +3 -1
  121. agenta/client/backend/types/organization_membership_request.py +3 -1
  122. agenta/client/backend/types/organization_output.py +3 -1
  123. agenta/client/backend/types/organization_request.py +3 -1
  124. agenta/client/backend/types/parent_dto.py +3 -1
  125. agenta/client/backend/types/project_membership_request.py +3 -1
  126. agenta/client/backend/types/project_request.py +3 -1
  127. agenta/client/backend/types/project_scope.py +3 -1
  128. agenta/client/backend/types/projects_response.py +3 -1
  129. agenta/client/backend/types/reference.py +6 -4
  130. agenta/client/backend/types/reference_dto.py +3 -1
  131. agenta/client/backend/types/reference_request_model.py +3 -1
  132. agenta/client/backend/types/result.py +3 -1
  133. agenta/client/backend/types/root_dto.py +3 -1
  134. agenta/client/backend/types/scopes_response_model.py +3 -1
  135. agenta/client/backend/types/secret_dto.py +3 -1
  136. agenta/client/backend/types/secret_response_dto.py +3 -1
  137. agenta/client/backend/types/simple_evaluation_output.py +3 -1
  138. agenta/client/backend/types/span_dto.py +6 -4
  139. agenta/client/backend/types/standard_provider_dto.py +3 -1
  140. agenta/client/backend/types/standard_provider_settings_dto.py +3 -1
  141. agenta/client/backend/types/status_dto.py +3 -1
  142. agenta/client/backend/types/tags_request.py +3 -1
  143. agenta/client/backend/types/testcase_response.py +6 -4
  144. agenta/client/backend/types/testset.py +6 -4
  145. agenta/client/backend/types/{test_set_output_response.py → testset_output_response.py} +4 -2
  146. agenta/client/backend/types/testset_request.py +3 -1
  147. agenta/client/backend/types/testset_response.py +3 -1
  148. agenta/client/backend/types/{test_set_simple_response.py → testset_simple_response.py} +4 -2
  149. agenta/client/backend/types/testsets_response.py +3 -1
  150. agenta/client/backend/types/time_dto.py +3 -1
  151. agenta/client/backend/types/tree_dto.py +3 -1
  152. agenta/client/backend/types/update_app_output.py +3 -1
  153. agenta/client/backend/types/user_request.py +3 -1
  154. agenta/client/backend/types/validation_error.py +3 -1
  155. agenta/client/backend/types/workflow_artifact.py +6 -4
  156. agenta/client/backend/types/workflow_data.py +3 -1
  157. agenta/client/backend/types/workflow_flags.py +3 -1
  158. agenta/client/backend/types/workflow_request.py +3 -1
  159. agenta/client/backend/types/workflow_response.py +3 -1
  160. agenta/client/backend/types/workflow_revision.py +6 -4
  161. agenta/client/backend/types/workflow_revision_request.py +3 -1
  162. agenta/client/backend/types/workflow_revision_response.py +3 -1
  163. agenta/client/backend/types/workflow_revisions_response.py +3 -1
  164. agenta/client/backend/types/workflow_variant.py +6 -4
  165. agenta/client/backend/types/workflow_variant_request.py +3 -1
  166. agenta/client/backend/types/workflow_variant_response.py +3 -1
  167. agenta/client/backend/types/workflow_variants_response.py +3 -1
  168. agenta/client/backend/types/workflows_response.py +3 -1
  169. agenta/client/backend/types/workspace.py +3 -1
  170. agenta/client/backend/types/workspace_member_response.py +3 -1
  171. agenta/client/backend/types/workspace_membership_request.py +3 -1
  172. agenta/client/backend/types/workspace_permission.py +3 -1
  173. agenta/client/backend/types/workspace_request.py +3 -1
  174. agenta/client/backend/types/workspace_response.py +3 -1
  175. agenta/client/backend/workspace/client.py +2 -2
  176. agenta/client/client.py +102 -88
  177. agenta/sdk/__init__.py +52 -3
  178. agenta/sdk/agenta_init.py +43 -16
  179. agenta/sdk/assets.py +22 -15
  180. agenta/sdk/context/serving.py +20 -8
  181. agenta/sdk/context/tracing.py +40 -22
  182. agenta/sdk/contexts/__init__.py +0 -0
  183. agenta/sdk/contexts/routing.py +38 -0
  184. agenta/sdk/contexts/running.py +57 -0
  185. agenta/sdk/contexts/tracing.py +86 -0
  186. agenta/sdk/decorators/__init__.py +1 -0
  187. agenta/sdk/decorators/routing.py +284 -0
  188. agenta/sdk/decorators/running.py +692 -98
  189. agenta/sdk/decorators/serving.py +20 -21
  190. agenta/sdk/decorators/tracing.py +176 -131
  191. agenta/sdk/engines/__init__.py +0 -0
  192. agenta/sdk/engines/running/__init__.py +0 -0
  193. agenta/sdk/engines/running/utils.py +17 -0
  194. agenta/sdk/engines/tracing/__init__.py +1 -0
  195. agenta/sdk/engines/tracing/attributes.py +185 -0
  196. agenta/sdk/engines/tracing/conventions.py +49 -0
  197. agenta/sdk/engines/tracing/exporters.py +130 -0
  198. agenta/sdk/engines/tracing/inline.py +1154 -0
  199. agenta/sdk/engines/tracing/processors.py +190 -0
  200. agenta/sdk/engines/tracing/propagation.py +102 -0
  201. agenta/sdk/engines/tracing/spans.py +136 -0
  202. agenta/sdk/engines/tracing/tracing.py +324 -0
  203. agenta/sdk/evaluations/__init__.py +2 -0
  204. agenta/sdk/evaluations/metrics.py +37 -0
  205. agenta/sdk/evaluations/preview/__init__.py +0 -0
  206. agenta/sdk/evaluations/preview/evaluate.py +765 -0
  207. agenta/sdk/evaluations/preview/utils.py +861 -0
  208. agenta/sdk/evaluations/results.py +66 -0
  209. agenta/sdk/evaluations/runs.py +153 -0
  210. agenta/sdk/evaluations/scenarios.py +48 -0
  211. agenta/sdk/litellm/litellm.py +12 -0
  212. agenta/sdk/litellm/mockllm.py +6 -8
  213. agenta/sdk/litellm/mocks/__init__.py +5 -5
  214. agenta/sdk/managers/applications.py +304 -0
  215. agenta/sdk/managers/config.py +2 -2
  216. agenta/sdk/managers/evaluations.py +0 -0
  217. agenta/sdk/managers/evaluators.py +303 -0
  218. agenta/sdk/managers/secrets.py +161 -24
  219. agenta/sdk/managers/shared.py +3 -1
  220. agenta/sdk/managers/testsets.py +441 -0
  221. agenta/sdk/managers/vault.py +3 -3
  222. agenta/sdk/middleware/auth.py +0 -176
  223. agenta/sdk/middleware/vault.py +203 -8
  224. agenta/sdk/middlewares/__init__.py +0 -0
  225. agenta/sdk/middlewares/routing/__init__.py +0 -0
  226. agenta/sdk/middlewares/routing/auth.py +263 -0
  227. agenta/sdk/middlewares/routing/cors.py +30 -0
  228. agenta/sdk/middlewares/routing/otel.py +29 -0
  229. agenta/sdk/middlewares/running/__init__.py +0 -0
  230. agenta/sdk/middlewares/running/normalizer.py +321 -0
  231. agenta/sdk/middlewares/running/resolver.py +161 -0
  232. agenta/sdk/middlewares/running/vault.py +140 -0
  233. agenta/sdk/models/__init__.py +0 -0
  234. agenta/sdk/models/blobs.py +33 -0
  235. agenta/sdk/models/evaluations.py +119 -0
  236. agenta/sdk/models/git.py +126 -0
  237. agenta/sdk/models/shared.py +167 -0
  238. agenta/sdk/models/testsets.py +163 -0
  239. agenta/sdk/models/tracing.py +202 -0
  240. agenta/sdk/models/workflows.py +753 -0
  241. agenta/sdk/tracing/exporters.py +67 -17
  242. agenta/sdk/tracing/processors.py +97 -0
  243. agenta/sdk/tracing/propagation.py +3 -1
  244. agenta/sdk/tracing/spans.py +4 -0
  245. agenta/sdk/tracing/tracing.py +13 -13
  246. agenta/sdk/types.py +211 -17
  247. agenta/sdk/utils/cache.py +1 -1
  248. agenta/sdk/utils/client.py +38 -0
  249. agenta/sdk/utils/helpers.py +13 -12
  250. agenta/sdk/utils/logging.py +18 -78
  251. agenta/sdk/utils/references.py +23 -0
  252. agenta/sdk/workflows/builtin.py +600 -0
  253. agenta/sdk/workflows/configurations.py +22 -0
  254. agenta/sdk/workflows/errors.py +292 -0
  255. agenta/sdk/workflows/handlers.py +1791 -0
  256. agenta/sdk/workflows/interfaces.py +948 -0
  257. agenta/sdk/workflows/sandbox.py +118 -0
  258. agenta/sdk/workflows/utils.py +303 -6
  259. {agenta-0.57.0.dist-info → agenta-0.63.2.dist-info}/METADATA +33 -30
  260. agenta-0.63.2.dist-info/RECORD +421 -0
  261. agenta/sdk/middleware/adapt.py +0 -253
  262. agenta/sdk/middleware/base.py +0 -40
  263. agenta/sdk/middleware/flags.py +0 -40
  264. agenta/sdk/workflows/types.py +0 -472
  265. agenta-0.57.0.dist-info/RECORD +0 -371
  266. /agenta/sdk/{workflows → engines/running}/registry.py +0 -0
  267. {agenta-0.57.0.dist-info → agenta-0.63.2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,1154 @@
1
+ ############################
2
+ ### services.shared.dtos ###
3
+ ### -------------------- ###
4
+
5
+ from typing import Optional
6
+
7
+ from pydantic import BaseModel
8
+ from uuid import UUID
9
+ from datetime import datetime
10
+ from enum import Enum
11
+ from collections import OrderedDict
12
+
13
+
14
+ class ProjectScopeDTO(BaseModel):
15
+ project_id: UUID
16
+
17
+
18
+ class LifecycleDTO(BaseModel):
19
+ created_at: datetime
20
+ updated_at: Optional[datetime] = None
21
+
22
+ updated_by_id: Optional[UUID] = None
23
+
24
+
25
+ ### -------------------- ###
26
+ ### services.shared.dtos ###
27
+ ############################
28
+
29
+
30
+ ###################################
31
+ ### services.observability.dtos ###
32
+ ### --------------------------- ###
33
+
34
+ from typing import List, Dict, Any, Union, Optional
35
+
36
+ from enum import Enum
37
+ from datetime import datetime
38
+ from uuid import UUID
39
+
40
+
41
+ class TimeDTO(BaseModel):
42
+ start: datetime
43
+ end: datetime
44
+
45
+
46
+ class StatusCode(Enum):
47
+ UNSET = "UNSET"
48
+ OK = "OK"
49
+ ERROR = "ERROR"
50
+
51
+
52
+ class StatusDTO(BaseModel):
53
+ code: StatusCode
54
+ message: Optional[str] = None
55
+ stacktrace: Optional[str] = None
56
+
57
+
58
+ AttributeValueType = Any
59
+ Attributes = Dict[str, AttributeValueType]
60
+
61
+
62
+ class TreeType(Enum):
63
+ # --- VARIANTS --- #
64
+ INVOCATION = "invocation"
65
+ ANNOTATION = "annotation"
66
+ # --- VARIANTS --- #
67
+
68
+
69
+ class NodeType(Enum):
70
+ # --- VARIANTS --- #
71
+ ## SPAN_KIND_SERVER
72
+ AGENT = "agent"
73
+ WORKFLOW = "workflow"
74
+ CHAIN = "chain"
75
+ ## SPAN_KIND_INTERNAL
76
+ TASK = "task"
77
+ ## SPAN_KIND_CLIENT
78
+ TOOL = "tool"
79
+ EMBEDDING = "embedding"
80
+ QUERY = "query"
81
+ COMPLETION = "completion"
82
+ CHAT = "chat"
83
+ RERANK = "rerank"
84
+ # --- VARIANTS --- #
85
+
86
+
87
+ class RootDTO(BaseModel):
88
+ id: UUID
89
+
90
+
91
+ class TreeDTO(BaseModel):
92
+ id: UUID
93
+ type: Optional[TreeType] = None
94
+
95
+
96
+ class NodeDTO(BaseModel):
97
+ id: UUID
98
+ type: Optional[NodeType] = None
99
+ name: str
100
+
101
+
102
+ Data = Dict[str, Any]
103
+ Metrics = Dict[str, Any]
104
+ Metadata = Dict[str, Any]
105
+ Tags = Dict[str, Any]
106
+ Refs = Dict[str, Any]
107
+
108
+
109
+ class LinkDTO(BaseModel):
110
+ type: str
111
+ id: UUID
112
+ tree_id: Optional[UUID] = None
113
+
114
+
115
+ class ParentDTO(BaseModel):
116
+ id: UUID
117
+
118
+
119
+ class OTelSpanKind(Enum):
120
+ SPAN_KIND_UNSPECIFIED = "SPAN_KIND_UNSPECIFIED"
121
+ # INTERNAL
122
+ SPAN_KIND_INTERNAL = "SPAN_KIND_INTERNAL"
123
+ # SYNCHRONOUS
124
+ SPAN_KIND_SERVER = "SPAN_KIND_SERVER"
125
+ SPAN_KIND_CLIENT = "SPAN_KIND_CLIENT"
126
+ # ASYNCHRONOUS
127
+ SPAN_KIND_PRODUCER = "SPAN_KIND_PRODUCER"
128
+ SPAN_KIND_CONSUMER = "SPAN_KIND_CONSUMER"
129
+
130
+
131
+ class OTelStatusCode(Enum):
132
+ STATUS_CODE_OK = "STATUS_CODE_OK"
133
+ STATUS_CODE_ERROR = "STATUS_CODE_ERROR"
134
+ STATUS_CODE_UNSET = "STATUS_CODE_UNSET"
135
+
136
+
137
+ class OTelContextDTO(BaseModel):
138
+ trace_id: str
139
+ span_id: str
140
+
141
+
142
+ class OTelEventDTO(BaseModel):
143
+ name: str
144
+ timestamp: datetime
145
+
146
+ attributes: Optional[Attributes] = None
147
+
148
+
149
+ class OTelLinkDTO(BaseModel):
150
+ context: OTelContextDTO
151
+
152
+ attributes: Optional[Attributes] = None
153
+
154
+
155
+ class OTelExtraDTO(BaseModel):
156
+ kind: Optional[str] = None
157
+
158
+ attributes: Optional[Attributes] = None
159
+ events: Optional[List[OTelEventDTO]] = None
160
+ links: Optional[List[OTelLinkDTO]] = None
161
+
162
+
163
+ class SpanDTO(BaseModel):
164
+ trace_id: str
165
+ span_id: str
166
+
167
+ scope: Optional[ProjectScopeDTO] = None
168
+
169
+ lifecycle: Optional[LifecycleDTO] = None
170
+
171
+ root: RootDTO
172
+ tree: TreeDTO
173
+ node: NodeDTO
174
+
175
+ parent: Optional[ParentDTO] = None
176
+
177
+ time: TimeDTO
178
+ status: StatusDTO
179
+
180
+ data: Optional[Data] = None
181
+ metrics: Optional[Metrics] = None
182
+ meta: Optional[Metadata] = None
183
+ tags: Optional[Tags] = None
184
+ refs: Optional[Refs] = None
185
+
186
+ links: Optional[List[LinkDTO]] = None
187
+
188
+ otel: Optional[OTelExtraDTO] = None
189
+
190
+ nodes: Optional[Dict[str, Union["SpanDTO", List["SpanDTO"]]]] = None
191
+
192
+
193
+ class OTelSpanDTO(BaseModel):
194
+ context: OTelContextDTO
195
+
196
+ name: str
197
+ kind: OTelSpanKind = OTelSpanKind.SPAN_KIND_UNSPECIFIED
198
+
199
+ start_time: datetime
200
+ end_time: datetime
201
+
202
+ status_code: OTelStatusCode = OTelStatusCode.STATUS_CODE_UNSET
203
+ status_message: Optional[str] = None
204
+
205
+ attributes: Optional[Attributes] = None
206
+ events: Optional[List[OTelEventDTO]] = None
207
+
208
+ parent: Optional[OTelContextDTO] = None
209
+ links: Optional[List[OTelLinkDTO]] = None
210
+
211
+
212
+ ### --------------------------- ###
213
+ ### services.observability.dtos ###
214
+ ###################################
215
+
216
+
217
+ ####################################
218
+ ### services.observability.utils ###
219
+ ### ---------------------------- ###
220
+
221
+ from typing import List, Dict, OrderedDict
222
+
223
+
224
+ def parse_span_dtos_to_span_idx(
225
+ span_dtos: List[SpanDTO],
226
+ ) -> Dict[str, SpanDTO]:
227
+ span_idx = {span_dto.node.id: span_dto for span_dto in span_dtos}
228
+
229
+ return span_idx
230
+
231
+
232
+ def parse_span_idx_to_span_id_tree(
233
+ span_idx: Dict[str, SpanDTO],
234
+ ) -> OrderedDict:
235
+ span_id_tree = OrderedDict()
236
+ index = {}
237
+
238
+ def push(span_dto: SpanDTO) -> None:
239
+ if span_dto.parent is None:
240
+ span_id_tree[span_dto.node.id] = OrderedDict()
241
+ index[span_dto.node.id] = span_id_tree[span_dto.node.id]
242
+ elif span_dto.parent.id in index:
243
+ index[span_dto.parent.id][span_dto.node.id] = OrderedDict()
244
+ index[span_dto.node.id] = index[span_dto.parent.id][span_dto.node.id]
245
+
246
+ for span_dto in sorted(span_idx.values(), key=lambda span_dto: span_dto.time.start):
247
+ push(span_dto)
248
+
249
+ return span_id_tree
250
+
251
+
252
+ def cumulate_costs(
253
+ spans_id_tree: OrderedDict,
254
+ spans_idx: Dict[str, SpanDTO],
255
+ ) -> None:
256
+ def _get_unit(span: SpanDTO):
257
+ if span.metrics is not None:
258
+ return span.metrics.get("unit.costs.total", 0.0)
259
+
260
+ return 0.0
261
+
262
+ def _get_acc(span: SpanDTO):
263
+ if span.metrics is not None:
264
+ return span.metrics.get("acc.costs.total", 0.0)
265
+
266
+ return 0.0
267
+
268
+ def _acc(a: float, b: float):
269
+ return a + b
270
+
271
+ def _set(span: SpanDTO, cost: float):
272
+ if span.metrics is None:
273
+ span.metrics = {}
274
+
275
+ if cost != 0.0:
276
+ span.metrics["acc.costs.total"] = cost
277
+
278
+ _cumulate_tree_dfs(spans_id_tree, spans_idx, _get_unit, _get_acc, _acc, _set)
279
+
280
+
281
+ def cumulate_tokens(
282
+ spans_id_tree: OrderedDict,
283
+ spans_idx: Dict[str, dict],
284
+ ) -> None:
285
+ def _get_unit(span: SpanDTO):
286
+ _tokens = {
287
+ "prompt": 0.0,
288
+ "completion": 0.0,
289
+ "total": 0.0,
290
+ }
291
+
292
+ if span.metrics is not None:
293
+ return {
294
+ "prompt": span.metrics.get("unit.tokens.prompt", 0.0),
295
+ "completion": span.metrics.get("unit.tokens.completion", 0.0),
296
+ "total": span.metrics.get("unit.tokens.total", 0.0),
297
+ }
298
+
299
+ return _tokens
300
+
301
+ def _get_acc(span: SpanDTO):
302
+ _tokens = {
303
+ "prompt": 0.0,
304
+ "completion": 0.0,
305
+ "total": 0.0,
306
+ }
307
+
308
+ if span.metrics is not None:
309
+ return {
310
+ "prompt": span.metrics.get("acc.tokens.prompt", 0.0),
311
+ "completion": span.metrics.get("acc.tokens.completion", 0.0),
312
+ "total": span.metrics.get("acc.tokens.total", 0.0),
313
+ }
314
+
315
+ return _tokens
316
+
317
+ def _acc(a: dict, b: dict):
318
+ return {
319
+ "prompt": a.get("prompt", 0.0) + b.get("prompt", 0.0),
320
+ "completion": a.get("completion", 0.0) + b.get("completion", 0.0),
321
+ "total": a.get("total", 0.0) + b.get("total", 0.0),
322
+ }
323
+
324
+ def _set(span: SpanDTO, tokens: dict):
325
+ if span.metrics is None:
326
+ span.metrics = {}
327
+
328
+ if tokens.get("prompt", 0.0) != 0.0:
329
+ span.metrics["acc.tokens.prompt"] = tokens.get("prompt", 0.0)
330
+ if tokens.get("completion", 0.0) != 0.0:
331
+ span.metrics["acc.tokens.completion"] = (
332
+ tokens.get("completion", 0.0)
333
+ if tokens.get("completion", 0.0) != 0.0
334
+ else None
335
+ )
336
+ if tokens.get("total", 0.0) != 0.0:
337
+ span.metrics["acc.tokens.total"] = (
338
+ tokens.get("total", 0.0) if tokens.get("total", 0.0) != 0.0 else None
339
+ )
340
+
341
+ _cumulate_tree_dfs(spans_id_tree, spans_idx, _get_unit, _get_acc, _acc, _set)
342
+
343
+
344
+ def _cumulate_tree_dfs(
345
+ spans_id_tree: OrderedDict,
346
+ spans_idx: Dict[str, SpanDTO],
347
+ get_unit_metric,
348
+ get_acc_metric,
349
+ accumulate_metric,
350
+ set_metric,
351
+ ):
352
+ for span_id, children_spans_id_tree in spans_id_tree.items():
353
+ children_spans_id_tree: OrderedDict
354
+
355
+ cumulated_metric = get_unit_metric(spans_idx[span_id])
356
+
357
+ _cumulate_tree_dfs(
358
+ children_spans_id_tree,
359
+ spans_idx,
360
+ get_unit_metric,
361
+ get_acc_metric,
362
+ accumulate_metric,
363
+ set_metric,
364
+ )
365
+
366
+ for child_span_id in children_spans_id_tree.keys():
367
+ marginal_metric = get_acc_metric(spans_idx[child_span_id])
368
+ cumulated_metric = accumulate_metric(cumulated_metric, marginal_metric)
369
+
370
+ set_metric(spans_idx[span_id], cumulated_metric)
371
+
372
+
373
+ def connect_children(
374
+ spans_id_tree: OrderedDict,
375
+ spans_idx: Dict[str, dict],
376
+ ) -> None:
377
+ _connect_tree_dfs(spans_id_tree, spans_idx)
378
+
379
+
380
+ def _connect_tree_dfs(
381
+ spans_id_tree: OrderedDict,
382
+ spans_idx: Dict[str, SpanDTO],
383
+ ):
384
+ for span_id, children_spans_id_tree in spans_id_tree.items():
385
+ children_spans_id_tree: OrderedDict
386
+
387
+ parent_span = spans_idx[span_id]
388
+
389
+ parent_span.nodes = dict()
390
+
391
+ _connect_tree_dfs(children_spans_id_tree, spans_idx)
392
+
393
+ for child_span_id in children_spans_id_tree.keys():
394
+ child_span_name = spans_idx[child_span_id].node.name
395
+ if child_span_name not in parent_span.nodes:
396
+ parent_span.nodes[child_span_name] = spans_idx[child_span_id]
397
+ else:
398
+ if not isinstance(parent_span.nodes[child_span_name], list):
399
+ parent_span.nodes[child_span_name] = [
400
+ parent_span.nodes[child_span_name]
401
+ ]
402
+
403
+ parent_span.nodes[child_span_name].append(spans_idx[child_span_id])
404
+
405
+ if len(parent_span.nodes) == 0:
406
+ parent_span.nodes = None
407
+
408
+
409
+ ### ---------------------------- ###
410
+ ### services.observability.utils ###
411
+ ####################################
412
+
413
+
414
+ ########################################################
415
+ ### apis.fastapi.observability.opentelemetry.semconv ###
416
+ ### ------------------------------------------------ ###
417
+
418
+ from json import loads
419
+
420
+ VERSION = "0.4.1"
421
+
422
+ V_0_4_1_ATTRIBUTES_EXACT = [
423
+ # OPENLLMETRY
424
+ ("gen_ai.system", "ag.meta.system"),
425
+ ("gen_ai.request.base_url", "ag.meta.request.base_url"),
426
+ ("gen_ai.request.endpoint", "ag.meta.request.endpoint"),
427
+ ("gen_ai.request.headers", "ag.meta.request.headers"),
428
+ ("gen_ai.request.type", "ag.type.node"),
429
+ ("gen_ai.request.streaming", "ag.meta.request.streaming"),
430
+ ("gen_ai.request.model", "ag.meta.request.model"),
431
+ ("gen_ai.request.max_tokens", "ag.meta.request.max_tokens"),
432
+ ("gen_ai.request.temperature", "ag.meta.request.temperature"),
433
+ ("gen_ai.request.top_p", "ag.meta.request.top_p"),
434
+ ("gen_ai.response.model", "ag.meta.response.model"),
435
+ ("gen_ai.usage.prompt_tokens", "ag.metrics.unit.tokens.prompt"),
436
+ ("gen_ai.usage.completion_tokens", "ag.metrics.unit.tokens.completion"),
437
+ ("gen_ai.usage.total_tokens", "ag.metrics.unit.tokens.total"),
438
+ ("llm.headers", "ag.meta.request.headers"),
439
+ ("llm.request.type", "ag.type.node"),
440
+ ("llm.top_k", "ag.meta.request.top_k"),
441
+ ("llm.is_streaming", "ag.meta.request.streaming"),
442
+ ("llm.usage.total_tokens", "ag.metrics.unit.tokens.total"),
443
+ ("gen_ai.openai.api_base", "ag.meta.request.base_url"),
444
+ ("db.system", "ag.meta.system"),
445
+ ("db.vector.query.top_k", "ag.meta.request.top_k"),
446
+ ("pinecone.query.top_k", "ag.meta.request.top_k"),
447
+ ("traceloop.span.kind", "ag.type.node"),
448
+ ("traceloop.entity.name", "ag.node.name"),
449
+ # OPENINFERENCE
450
+ ("output.value", "ag.data.outputs"),
451
+ ("input.value", "ag.data.inputs"),
452
+ ("embedding.model_name", "ag.meta.request.model"),
453
+ ("llm.invocation_parameters", "ag.meta.request"),
454
+ ("llm.model_name", "ag.meta.request.model"),
455
+ ("llm.provider", "ag.meta.provider"),
456
+ ("llm.system", "ag.meta.system"),
457
+ ]
458
+ V_0_4_1_ATTRIBUTES_PREFIX = [
459
+ # OPENLLMETRY
460
+ ("gen_ai.prompt", "ag.data.inputs.prompt"),
461
+ ("gen_ai.completion", "ag.data.outputs.completion"),
462
+ ("llm.request.functions", "ag.data.inputs.functions"),
463
+ ("llm.request.tools", "ag.data.inputs.tools"),
464
+ # OPENINFERENCE
465
+ ("llm.token_count", "ag.metrics.unit.tokens"),
466
+ ("llm.input_messages", "ag.data.inputs.prompt"),
467
+ ("llm.output_messages", "ag.data.outputs.completion"),
468
+ ]
469
+
470
+ V_0_4_1_ATTRIBUTES_DYNAMIC = [
471
+ # OPENLLMETRY
472
+ ("traceloop.entity.input", lambda x: ("ag.data.inputs", loads(x).get("inputs"))),
473
+ ("traceloop.entity.output", lambda x: ("ag.data.outputs", loads(x).get("outputs"))),
474
+ ]
475
+
476
+
477
+ V_0_4_1_MAPS = {
478
+ "attributes": {
479
+ "exact": {
480
+ "from": {otel: agenta for otel, agenta in V_0_4_1_ATTRIBUTES_EXACT[::-1]},
481
+ "to": {agenta: otel for otel, agenta in V_0_4_1_ATTRIBUTES_EXACT[::-1]},
482
+ },
483
+ "prefix": {
484
+ "from": {otel: agenta for otel, agenta in V_0_4_1_ATTRIBUTES_PREFIX[::-1]},
485
+ "to": {agenta: otel for otel, agenta in V_0_4_1_ATTRIBUTES_PREFIX[::-1]},
486
+ },
487
+ "dynamic": {
488
+ "from": {otel: agenta for otel, agenta in V_0_4_1_ATTRIBUTES_DYNAMIC[::-1]}
489
+ },
490
+ },
491
+ }
492
+ V_0_4_1_KEYS = {
493
+ "attributes": {
494
+ "exact": {
495
+ "from": list(V_0_4_1_MAPS["attributes"]["exact"]["from"].keys()),
496
+ "to": list(V_0_4_1_MAPS["attributes"]["exact"]["to"].keys()),
497
+ },
498
+ "prefix": {
499
+ "from": list(V_0_4_1_MAPS["attributes"]["prefix"]["from"].keys()),
500
+ "to": list(V_0_4_1_MAPS["attributes"]["prefix"]["to"].keys()),
501
+ },
502
+ "dynamic": {
503
+ "from": list(V_0_4_1_MAPS["attributes"]["dynamic"]["from"].keys()),
504
+ },
505
+ },
506
+ }
507
+
508
+
509
+ MAPS = {
510
+ "0.4.1": V_0_4_1_MAPS, # LATEST
511
+ }
512
+ KEYS = {
513
+ "0.4.1": V_0_4_1_KEYS, # LATEST
514
+ }
515
+
516
+ CODEX = {"maps": MAPS[VERSION], "keys": KEYS[VERSION]}
517
+
518
+
519
+ ### ------------------------------------------------ ###
520
+ ### apis.fastapi.observability.opentelemetry.semconv ###
521
+ ########################################################
522
+
523
+
524
+ ########################################
525
+ ### apis.fastapi.observability.utils ###
526
+ ### -------------------------------- ###
527
+
528
+ from typing import Optional, Union, Tuple, Any, List, Dict
529
+ from uuid import UUID
530
+ from collections import OrderedDict
531
+ from json import loads, JSONDecodeError, dumps
532
+ from copy import copy
533
+
534
+
535
+ def _unmarshal_attributes(
536
+ marshalled: Dict[str, Any],
537
+ ) -> Dict[str, Any]:
538
+ """
539
+ Unmarshals a dictionary of marshalled attributes into a nested dictionary
540
+
541
+ Example:
542
+ marshalled = {
543
+ "ag.type": "tree",
544
+ "ag.node.name": "root",
545
+ "ag.node.children.0.name": "child1",
546
+ "ag.node.children.1.name": "child2"
547
+ }
548
+ unmarshalled = {
549
+ "ag": {
550
+ "type": "tree",
551
+ "node": {
552
+ "name": "root",
553
+ "children": [
554
+ {
555
+ "name": "child1",
556
+ },
557
+ {
558
+ "name": "child2",
559
+ }
560
+ ]
561
+ }
562
+ }
563
+ }
564
+ """
565
+ unmarshalled = {}
566
+
567
+ for key, value in marshalled.items():
568
+ keys = key.split(".")
569
+
570
+ level = unmarshalled
571
+
572
+ for i, part in enumerate(keys[:-1]):
573
+ if part.isdigit():
574
+ part = int(part)
575
+
576
+ if not isinstance(level, list):
577
+ level = []
578
+
579
+ while len(level) <= part:
580
+ level.append({})
581
+
582
+ level = level[part]
583
+
584
+ else:
585
+ if part not in level:
586
+ level[part] = {} if not keys[i + 1].isdigit() else []
587
+
588
+ level = level[part]
589
+
590
+ last_key = keys[-1]
591
+
592
+ if last_key.isdigit():
593
+ last_key = int(last_key)
594
+
595
+ if not isinstance(level, list):
596
+ level = []
597
+
598
+ while len(level) <= last_key:
599
+ level.append(None)
600
+
601
+ level[last_key] = value
602
+
603
+ else:
604
+ level[last_key] = value
605
+
606
+ return unmarshalled
607
+
608
+
609
+ def _encode_key(
610
+ namespace,
611
+ key: str,
612
+ ) -> str:
613
+ return f"ag.{namespace}.{key}"
614
+
615
+
616
+ def _decode_key(
617
+ namespace,
618
+ key: str,
619
+ ) -> str:
620
+ return key.replace(f"ag.{namespace}.", "")
621
+
622
+
623
+ def _decode_value(
624
+ value: Any,
625
+ ) -> Any:
626
+ if isinstance(value, (int, float, bool, bytes)):
627
+ return value
628
+
629
+ if isinstance(value, str):
630
+ if value == "@ag.type=none:":
631
+ return None
632
+
633
+ if value.startswith("@ag.type=json:"):
634
+ encoded = value[len("@ag.type=json:") :]
635
+ value = loads(encoded)
636
+ return value
637
+
638
+ return value
639
+
640
+ return value
641
+
642
+
643
+ def _get_attributes(
644
+ attributes: Attributes,
645
+ namespace: str,
646
+ ):
647
+ return {
648
+ _decode_key(namespace, key): _decode_value(value)
649
+ for key, value in attributes.items()
650
+ if key != _decode_key(namespace, key)
651
+ }
652
+
653
+
654
+ def _parse_from_types(
655
+ otel_span_dto: OTelSpanDTO,
656
+ ) -> dict:
657
+ types = _get_attributes(otel_span_dto.attributes, "type")
658
+
659
+ if types.get("tree"):
660
+ del otel_span_dto.attributes[_encode_key("type", "tree")]
661
+
662
+ if types.get("node"):
663
+ del otel_span_dto.attributes[_encode_key("type", "node")]
664
+
665
+ return types
666
+
667
+
668
+ def _parse_from_semconv(
669
+ attributes: Attributes,
670
+ ) -> None:
671
+ _attributes = copy(attributes)
672
+
673
+ for old_key, value in _attributes.items():
674
+ if old_key in CODEX["keys"]["attributes"]["exact"]["from"]:
675
+ new_key = CODEX["maps"]["attributes"]["exact"]["from"][old_key]
676
+
677
+ attributes[new_key] = value
678
+
679
+ del attributes[old_key]
680
+
681
+ else:
682
+ for prefix_key in CODEX["keys"]["attributes"]["prefix"]["from"]:
683
+ if old_key.startswith(prefix_key):
684
+ prefix = CODEX["maps"]["attributes"]["prefix"]["from"][prefix_key]
685
+
686
+ new_key = old_key.replace(prefix_key, prefix)
687
+
688
+ attributes[new_key] = value
689
+
690
+ del attributes[old_key]
691
+
692
+ for dynamic_key in CODEX["keys"]["attributes"]["dynamic"]["from"]:
693
+ if old_key == dynamic_key:
694
+ try:
695
+ new_key, new_value = CODEX["maps"]["attributes"]["dynamic"][
696
+ "from"
697
+ ][dynamic_key](value)
698
+
699
+ attributes[new_key] = new_value
700
+
701
+ except: # pylint: disable=bare-except
702
+ pass
703
+
704
+
705
+ def _parse_from_links(
706
+ otel_span_dto: OTelSpanDTO,
707
+ ) -> dict:
708
+ # LINKS
709
+ links = None
710
+ otel_links = None
711
+
712
+ if otel_span_dto.links:
713
+ links = list()
714
+ otel_links = list()
715
+
716
+ for link in otel_span_dto.links:
717
+ _links = _get_attributes(link.attributes, "type")
718
+
719
+ if _links:
720
+ link_type = _links.get("link")
721
+ link_tree_id = str(UUID(link.context.trace_id[2:]))
722
+ link_node_id = str(
723
+ UUID(link.context.trace_id[2 + 16 :] + link.context.span_id[2:])
724
+ )
725
+
726
+ links.append(
727
+ LinkDTO(
728
+ type=link_type,
729
+ tree_id=link_tree_id,
730
+ id=link_node_id,
731
+ )
732
+ )
733
+ else:
734
+ otel_links.append(link)
735
+
736
+ links = links if links else None
737
+ otel_links = otel_links if otel_links else None
738
+
739
+ otel_span_dto.links = otel_links
740
+
741
+ return links
742
+
743
+
744
+ def _parse_from_attributes(
745
+ otel_span_dto: OTelSpanDTO,
746
+ ) -> Tuple[dict, dict, dict, dict, dict]:
747
+ # DATA
748
+ _data = _get_attributes(otel_span_dto.attributes, "data")
749
+
750
+ for key in _data.keys():
751
+ del otel_span_dto.attributes[_encode_key("data", key)]
752
+
753
+ # _data = _unmarshal_attributes(_data)
754
+ _data = _data if _data else None
755
+
756
+ # METRICS
757
+ _metrics = _get_attributes(otel_span_dto.attributes, "metrics")
758
+
759
+ for key in _metrics.keys():
760
+ del otel_span_dto.attributes[_encode_key("metrics", key)]
761
+
762
+ # _metrics = _unmarshal_attributes(_metrics)
763
+ _metrics = _metrics if _metrics else None
764
+
765
+ # META
766
+ _meta = _get_attributes(otel_span_dto.attributes, "meta")
767
+
768
+ for key in _meta.keys():
769
+ del otel_span_dto.attributes[_encode_key("meta", key)]
770
+
771
+ # _meta = _unmarshal_attributes(_meta)
772
+ _meta = _meta if _meta else None
773
+
774
+ # TAGS
775
+ _tags = _get_attributes(otel_span_dto.attributes, "tags")
776
+
777
+ for key in _tags.keys():
778
+ del otel_span_dto.attributes[_encode_key("tags", key)]
779
+
780
+ _tags = _tags if _tags else None
781
+
782
+ # REFS
783
+ _refs = _get_attributes(otel_span_dto.attributes, "refs")
784
+
785
+ for key in _refs.keys():
786
+ del otel_span_dto.attributes[_encode_key("refs", key)]
787
+
788
+ _refs = _refs if _refs else None
789
+
790
+ if len(otel_span_dto.attributes.keys()) < 1:
791
+ otel_span_dto.attributes = None
792
+
793
+ return _data, _metrics, _meta, _tags, _refs
794
+
795
+
796
+ def parse_from_otel_span_dto(
797
+ otel_span_dto: OTelSpanDTO,
798
+ ) -> SpanDTO:
799
+ trace_id = str(otel_span_dto.context.trace_id[2:])
800
+ span_id = str(otel_span_dto.context.span_id[2:])
801
+
802
+ lifecyle = LifecycleDTO(
803
+ created_at=datetime.now(),
804
+ )
805
+
806
+ _parse_from_semconv(otel_span_dto.attributes)
807
+
808
+ types = _parse_from_types(otel_span_dto)
809
+
810
+ tree_id = UUID(trace_id)
811
+
812
+ tree_type: str = types.get("tree")
813
+
814
+ tree = TreeDTO(
815
+ id=tree_id,
816
+ type=tree_type.lower() if tree_type else None,
817
+ )
818
+
819
+ node_id = UUID(trace_id[16:] + span_id)
820
+
821
+ node_type = NodeType.TASK
822
+ try:
823
+ node_type = NodeType(types.get("node", "").lower())
824
+ except: # pylint: disable=bare-except
825
+ pass
826
+
827
+ node = NodeDTO(
828
+ id=node_id,
829
+ type=node_type,
830
+ name=otel_span_dto.name,
831
+ )
832
+
833
+ parent = (
834
+ ParentDTO(
835
+ id=(
836
+ UUID(
837
+ otel_span_dto.parent.trace_id[2 + 16 :]
838
+ + otel_span_dto.parent.span_id[2:]
839
+ )
840
+ )
841
+ )
842
+ if otel_span_dto.parent
843
+ else None
844
+ )
845
+
846
+ time = TimeDTO(
847
+ start=otel_span_dto.start_time,
848
+ end=otel_span_dto.end_time,
849
+ )
850
+
851
+ status = StatusDTO(
852
+ code=otel_span_dto.status_code.value.replace("STATUS_CODE_", ""),
853
+ message=otel_span_dto.status_message,
854
+ )
855
+
856
+ links = _parse_from_links(otel_span_dto)
857
+
858
+ data, metrics, meta, tags, refs = _parse_from_attributes(otel_span_dto)
859
+
860
+ duration = (otel_span_dto.end_time - otel_span_dto.start_time).total_seconds()
861
+
862
+ if metrics is None:
863
+ metrics = dict()
864
+
865
+ metrics["acc.duration.total"] = round(duration * 1_000, 3) # milliseconds
866
+
867
+ root_id = str(tree_id)
868
+ if refs is not None:
869
+ root_id = refs.get("scenario.id", root_id)
870
+
871
+ root = RootDTO(id=UUID(root_id))
872
+
873
+ otel = OTelExtraDTO(
874
+ kind=otel_span_dto.kind.value,
875
+ attributes=otel_span_dto.attributes,
876
+ events=otel_span_dto.events,
877
+ links=otel_span_dto.links,
878
+ )
879
+
880
+ span_dto = SpanDTO(
881
+ trace_id=trace_id,
882
+ span_id=span_id,
883
+ lifecycle=lifecyle,
884
+ root=root,
885
+ tree=tree,
886
+ node=node,
887
+ parent=parent,
888
+ time=time,
889
+ status=status,
890
+ data=data,
891
+ metrics=metrics,
892
+ meta=meta,
893
+ tags=tags,
894
+ refs=refs,
895
+ links=links,
896
+ otel=otel,
897
+ )
898
+
899
+ return span_dto
900
+
901
+
902
+ def parse_to_agenta_span_dto(
903
+ span_dto: SpanDTO,
904
+ ) -> SpanDTO:
905
+ # DATA
906
+ if span_dto.data:
907
+ span_dto.data = _unmarshal_attributes(span_dto.data)
908
+
909
+ if "outputs" in span_dto.data:
910
+ if "__default__" in span_dto.data["outputs"]:
911
+ span_dto.data["outputs"] = span_dto.data["outputs"]["__default__"]
912
+
913
+ # METRICS
914
+ if span_dto.metrics:
915
+ span_dto.metrics = _unmarshal_attributes(span_dto.metrics)
916
+
917
+ # META
918
+ if span_dto.meta:
919
+ span_dto.meta = _unmarshal_attributes(span_dto.meta)
920
+
921
+ # TAGS
922
+ if span_dto.tags:
923
+ span_dto.tags = _unmarshal_attributes(span_dto.tags)
924
+
925
+ # REFS
926
+ if span_dto.refs:
927
+ span_dto.refs = _unmarshal_attributes(span_dto.refs)
928
+
929
+ if isinstance(span_dto.links, list):
930
+ for link in span_dto.links:
931
+ link.tree_id = None
932
+
933
+ if span_dto.nodes:
934
+ for v in span_dto.nodes.values():
935
+ if isinstance(v, list):
936
+ for n in v:
937
+ parse_to_agenta_span_dto(n)
938
+ else:
939
+ parse_to_agenta_span_dto(v)
940
+
941
+ # MASK LINKS FOR NOW
942
+ span_dto.links = None
943
+ # ------------------
944
+
945
+ # MASK LIFECYCLE FOR NOW
946
+ # span_dto.lifecycle = None
947
+ if span_dto.lifecycle:
948
+ span_dto.lifecycle.updated_at = None
949
+ span_dto.lifecycle.updated_by_id = None
950
+ # ----------------------
951
+
952
+ return span_dto
953
+
954
+
955
+ ### -------------------------------- ###
956
+ ### apis.fastapi.observability.utils ###
957
+ ########################################
958
+
959
+
960
+ from litellm import cost_calculator
961
+ from opentelemetry.sdk.trace import ReadableSpan
962
+
963
+ from agenta.sdk.types import AgentaNodeDto, AgentaNodesResponse
964
+
965
+
966
+ def parse_inline_trace(
967
+ spans: Dict[str, ReadableSpan],
968
+ ):
969
+ otel_span_dtos = _parse_readable_spans(spans)
970
+
971
+ ############################################################
972
+ ### apis.fastapi.observability.api.otlp_collect_traces() ###
973
+ ### ---------------------------------------------------- ###
974
+ span_dtos = [
975
+ parse_from_otel_span_dto(otel_span_dto) for otel_span_dto in otel_span_dtos
976
+ ]
977
+ ### ---------------------------------------------------- ###
978
+ ### apis.fastapi.observability.api.otlp_collect_traces() ###
979
+ ############################################################
980
+
981
+ #####################################################
982
+ ### services.observability.service.ingest/query() ###
983
+ ### --------------------------------------------- ###
984
+ span_idx = parse_span_dtos_to_span_idx(span_dtos)
985
+ span_id_tree = parse_span_idx_to_span_id_tree(span_idx)
986
+ ### --------------------------------------------- ###
987
+ ### services.observability.service.ingest/query() ###
988
+ #####################################################
989
+
990
+ ###############################################
991
+ ### services.observability.service.ingest() ###
992
+ ### --------------------------------------- ###
993
+ calculate_costs(span_idx)
994
+ cumulate_costs(span_id_tree, span_idx)
995
+ cumulate_tokens(span_id_tree, span_idx)
996
+ ### --------------------------------------- ###
997
+ ### services.observability.service.ingest() ###
998
+ ###############################################
999
+
1000
+ ##############################################
1001
+ ### services.observability.service.query() ###
1002
+ ### -------------------------------------- ###
1003
+ connect_children(span_id_tree, span_idx)
1004
+ root_span_dtos = [span_idx[span_id] for span_id in span_id_tree.keys()]
1005
+ agenta_span_dtos = [
1006
+ parse_to_agenta_span_dto(span_dto) for span_dto in root_span_dtos
1007
+ ]
1008
+ ### -------------------------------------- ###
1009
+ ### services.observability.service.query() ###
1010
+ ##############################################
1011
+
1012
+ spans = [
1013
+ span_dto.model_dump(
1014
+ mode="json",
1015
+ exclude_none=True,
1016
+ exclude_defaults=True,
1017
+ )
1018
+ for span_dto in agenta_span_dtos
1019
+ ]
1020
+ inline_trace = AgentaNodesResponse(
1021
+ version="1.0.0",
1022
+ nodes=[AgentaNodeDto(**span) for span in spans],
1023
+ ).model_dump(
1024
+ mode="json",
1025
+ exclude_none=True,
1026
+ exclude_unset=True,
1027
+ )
1028
+ return inline_trace
1029
+
1030
+
1031
+ def _parse_readable_spans(
1032
+ spans: List[ReadableSpan],
1033
+ ) -> List[OTelSpanDTO]:
1034
+ otel_span_dtos = list()
1035
+
1036
+ for span in spans:
1037
+ otel_events = [
1038
+ OTelEventDTO(
1039
+ name=event.name,
1040
+ timestamp=_timestamp_ns_to_datetime(event.timestamp),
1041
+ attributes=event.attributes,
1042
+ )
1043
+ for event in span.events
1044
+ ]
1045
+ otel_links = [
1046
+ OTelLinkDTO(
1047
+ context=OTelContextDTO(
1048
+ trace_id=_int_to_hex(link.context.trace_id, 128),
1049
+ span_id=_int_to_hex(link.context.span_id, 64),
1050
+ ),
1051
+ attributes=link.attributes,
1052
+ )
1053
+ for link in span.links
1054
+ ]
1055
+ otel_span_dto = OTelSpanDTO(
1056
+ context=OTelContextDTO(
1057
+ trace_id=_int_to_hex(span.get_span_context().trace_id, 128),
1058
+ span_id=_int_to_hex(span.get_span_context().span_id, 64),
1059
+ ),
1060
+ name=span.name,
1061
+ kind=OTelSpanKind(
1062
+ "SPAN_KIND_"
1063
+ + (span.kind if isinstance(span.kind, str) else span.kind.name)
1064
+ ),
1065
+ start_time=_timestamp_ns_to_datetime(span.start_time),
1066
+ end_time=_timestamp_ns_to_datetime(span.end_time),
1067
+ status_code=OTelStatusCode("STATUS_CODE_" + span.status.status_code.name),
1068
+ status_message=span.status.description,
1069
+ attributes=span.attributes,
1070
+ events=otel_events if len(otel_events) > 0 else None,
1071
+ parent=(
1072
+ OTelContextDTO(
1073
+ trace_id=_int_to_hex(span.parent.trace_id, 128),
1074
+ span_id=_int_to_hex(span.parent.span_id, 64),
1075
+ )
1076
+ if span.parent and not span.parent.is_remote
1077
+ else None
1078
+ ),
1079
+ links=otel_links if len(otel_links) > 0 else None,
1080
+ )
1081
+
1082
+ otel_span_dtos.append(otel_span_dto)
1083
+
1084
+ return otel_span_dtos
1085
+
1086
+
1087
+ def _int_to_hex(integer, bits):
1088
+ _hex = hex(integer)[2:]
1089
+
1090
+ _hex = _hex.zfill(bits // 4)
1091
+
1092
+ _hex = "0x" + _hex
1093
+
1094
+ return _hex
1095
+
1096
+
1097
+ def _timestamp_ns_to_datetime(timestamp_ns):
1098
+ _datetime = datetime.fromtimestamp(
1099
+ timestamp_ns / 1_000_000_000,
1100
+ ).isoformat(
1101
+ timespec="microseconds",
1102
+ )
1103
+
1104
+ return _datetime
1105
+
1106
+
1107
+ class LlmTokens(BaseModel):
1108
+ prompt_tokens: Optional[int] = 0
1109
+ completion_tokens: Optional[int] = 0
1110
+ total_tokens: Optional[int] = 0
1111
+
1112
+
1113
+ TYPES_WITH_COSTS = [
1114
+ "embedding",
1115
+ "query",
1116
+ "completion",
1117
+ "chat",
1118
+ "rerank",
1119
+ ]
1120
+
1121
+
1122
+ def calculate_costs(span_idx: Dict[str, SpanDTO]):
1123
+ for span in span_idx.values():
1124
+ if (
1125
+ span.node.type
1126
+ and span.node.type.name.lower() in TYPES_WITH_COSTS
1127
+ and span.meta
1128
+ and span.metrics
1129
+ ):
1130
+ model = span.meta.get("response.model") or span.meta.get(
1131
+ "configuration.model"
1132
+ )
1133
+ prompt_tokens = span.metrics.get("unit.tokens.prompt", 0.0)
1134
+ completion_tokens = span.metrics.get("unit.tokens.completion", 0.0)
1135
+
1136
+ try:
1137
+ costs = cost_calculator.cost_per_token(
1138
+ model=model,
1139
+ prompt_tokens=prompt_tokens,
1140
+ completion_tokens=completion_tokens,
1141
+ )
1142
+
1143
+ if not costs:
1144
+ continue
1145
+
1146
+ prompt_cost, completion_cost = costs
1147
+ total_cost = prompt_cost + completion_cost
1148
+
1149
+ span.metrics["unit.costs.prompt"] = prompt_cost
1150
+ span.metrics["unit.costs.completion"] = completion_cost
1151
+ span.metrics["unit.costs.total"] = total_cost
1152
+
1153
+ except: # pylint: disable=bare-except
1154
+ pass