agenta 0.12.2__py3-none-any.whl → 0.32.0a1__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 agenta might be problematic. Click here for more details.

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