agenta 0.26.0a0__py3-none-any.whl → 0.27.0a0__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 (41) hide show
  1. agenta/__init__.py +6 -7
  2. agenta/client/backend/client.py +22 -14
  3. agenta/client/backend/core/http_client.py +23 -15
  4. agenta/sdk/__init__.py +27 -6
  5. agenta/sdk/agenta_init.py +73 -26
  6. agenta/sdk/config_manager.py +2 -2
  7. agenta/sdk/context/__init__.py +0 -0
  8. agenta/sdk/context/routing.py +25 -0
  9. agenta/sdk/context/tracing.py +3 -0
  10. agenta/sdk/decorators/__init__.py +0 -0
  11. agenta/sdk/decorators/{llm_entrypoint.py → routing.py} +137 -124
  12. agenta/sdk/decorators/tracing.py +228 -76
  13. agenta/sdk/litellm/__init__.py +1 -0
  14. agenta/sdk/litellm/litellm.py +277 -0
  15. agenta/sdk/router.py +0 -7
  16. agenta/sdk/tracing/__init__.py +1 -0
  17. agenta/sdk/tracing/attributes.py +181 -0
  18. agenta/sdk/tracing/context.py +21 -0
  19. agenta/sdk/tracing/conventions.py +43 -0
  20. agenta/sdk/tracing/exporters.py +53 -0
  21. agenta/sdk/tracing/inline.py +1306 -0
  22. agenta/sdk/tracing/processors.py +65 -0
  23. agenta/sdk/tracing/spans.py +124 -0
  24. agenta/sdk/tracing/tracing.py +174 -0
  25. agenta/sdk/types.py +0 -12
  26. agenta/sdk/utils/{helper/openai_cost.py → costs.py} +3 -0
  27. agenta/sdk/utils/debug.py +5 -5
  28. agenta/sdk/utils/exceptions.py +19 -0
  29. agenta/sdk/utils/globals.py +3 -5
  30. agenta/sdk/{tracing/logger.py → utils/logging.py} +3 -5
  31. agenta/sdk/utils/singleton.py +13 -0
  32. {agenta-0.26.0a0.dist-info → agenta-0.27.0a0.dist-info}/METADATA +5 -1
  33. {agenta-0.26.0a0.dist-info → agenta-0.27.0a0.dist-info}/RECORD +35 -25
  34. agenta/sdk/context.py +0 -41
  35. agenta/sdk/decorators/base.py +0 -10
  36. agenta/sdk/tracing/callbacks.py +0 -187
  37. agenta/sdk/tracing/llm_tracing.py +0 -617
  38. agenta/sdk/tracing/tasks_manager.py +0 -129
  39. agenta/sdk/tracing/tracing_context.py +0 -27
  40. {agenta-0.26.0a0.dist-info → agenta-0.27.0a0.dist-info}/WHEEL +0 -0
  41. {agenta-0.26.0a0.dist-info → agenta-0.27.0a0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,1306 @@
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
+ NOF_CHARS = 8
15
+
16
+
17
+ def _p_id(id):
18
+ return repr(str(id)[:NOF_CHARS])
19
+
20
+
21
+ def _p_osa(o):
22
+ elements = []
23
+
24
+ for i in OrderedDict(sorted(o.items())).items():
25
+ if not i[0].startswith("_"):
26
+ if i[1].__class__.__module__ != "builtins":
27
+ if repr(i[1]).startswith("<"):
28
+ elements.append(f"{i[0]}: {i[1].name}")
29
+ elif repr(i[1]).startswith("UUID("):
30
+ elements.append(f"{i[0]}: {_p_id(i[1])}")
31
+ else:
32
+ elements.append(f"{i[0]}: {i[1].__str__()}")
33
+ else:
34
+ if isinstance(i[1], list):
35
+ elements.append(
36
+ f"{i[0]}: [" + ", ".join([el.__str__() for el in i[1]]) + "]"
37
+ )
38
+ elif isinstance(i[1], dict):
39
+ elements.append(f"{i[0]}: {{{_p_osa(i[1])}}}")
40
+ else:
41
+ if i[1] is not None:
42
+ if i[0] == "slug":
43
+ elements.append(f"{i[0]}: {repr(i[1][:8])}")
44
+ else:
45
+ elements.append(f"{i[0]}: {repr(i[1])}")
46
+
47
+ return ", ".join(elements)
48
+
49
+
50
+ def _p_ora(o, open="{", close="}", sep=": ", foo=repr):
51
+ if o.__class__.__module__ != "builtins":
52
+ if o.__class__.__name__ == "UUID":
53
+ return repr(o)
54
+ if isinstance(o, Enum):
55
+ return o
56
+ if isinstance(o, datetime):
57
+ return o.isoformat()
58
+ return f"{o.__class__.__name__}({_p_ora(o.__dict__, open='', close='', sep='=', foo=lambda x : x)})"
59
+ elif isinstance(o, list):
60
+ return f"[{', '.join([repr(el) for el in o])}]"
61
+ elif isinstance(o, dict):
62
+ o = OrderedDict(sorted(o.items()))
63
+ return f"{open}{', '.join([f'{foo(elk)}{sep}{_p_ora(elv)}' for elk, elv in o.items()])}{close}"
64
+ else:
65
+ if o is not None:
66
+ return repr(o)
67
+
68
+
69
+ def _str(o):
70
+ return f"{{{_p_osa(o.__dict__)}}}"
71
+
72
+
73
+ def _repr(o):
74
+ return _p_ora(o)
75
+
76
+
77
+ class DisplayBase(BaseModel):
78
+ def __str__(self):
79
+ return _str(self)
80
+
81
+ def __repr__(self):
82
+ return _repr(self)
83
+
84
+
85
+ class ProjectScopeDTO(DisplayBase):
86
+ project_id: UUID
87
+
88
+
89
+ class LifecycleDTO(DisplayBase):
90
+ created_at: datetime
91
+ updated_at: Optional[datetime] = None
92
+
93
+ updated_by_id: Optional[UUID] = None
94
+
95
+
96
+ ### -------------------- ###
97
+ ### services.shared.dtos ###
98
+ ############################
99
+
100
+
101
+ ###################################
102
+ ### services.observability.dtos ###
103
+ ### --------------------------- ###
104
+
105
+ from typing import List, Dict, Any, Union, Optional, Sequence
106
+
107
+ from enum import Enum
108
+ from datetime import datetime
109
+ from uuid import UUID
110
+
111
+
112
+ class TimeDTO(DisplayBase):
113
+ start: datetime
114
+ end: datetime
115
+ span: int
116
+
117
+
118
+ class StatusCode(Enum):
119
+ UNSET = "UNSET"
120
+ OK = "OK"
121
+ ERROR = "ERROR"
122
+
123
+
124
+ class StatusDTO(DisplayBase):
125
+ code: StatusCode
126
+ message: Optional[str] = None
127
+ stacktrace: Optional[str] = None
128
+
129
+
130
+ AttributeValueType = Any
131
+ Attributes = Dict[str, AttributeValueType]
132
+
133
+
134
+ class AttributesDTO(DisplayBase):
135
+ data: Optional[Attributes] = None
136
+ metrics: Optional[Attributes] = None
137
+ meta: Optional[Attributes] = None
138
+ tags: Optional[Attributes] = None
139
+ semconv: Optional[Attributes] = None
140
+
141
+
142
+ class TreeType(Enum):
143
+ # --- VARIANTS --- #
144
+ INVOCATION = "invocation"
145
+ # --- VARIANTS --- #
146
+
147
+
148
+ class NodeType(Enum):
149
+ # --- VARIANTS --- #
150
+ ## SPAN_KIND_SERVER
151
+ AGENT = "agent"
152
+ WORKFLOW = "workflow"
153
+ CHAIN = "chain"
154
+ ## SPAN_KIND_INTERNAL
155
+ TASK = "task"
156
+ ## SPAN_KIND_CLIENT
157
+ TOOL = "tool"
158
+ EMBEDDING = "embedding"
159
+ QUERY = "query"
160
+ COMPLETION = "completion"
161
+ CHAT = "chat"
162
+ RERANK = "rerank"
163
+ # --- VARIANTS --- #
164
+
165
+
166
+ class RootDTO(DisplayBase):
167
+ id: UUID
168
+
169
+
170
+ class TreeDTO(DisplayBase):
171
+ id: UUID
172
+ type: Optional[TreeType] = None
173
+
174
+
175
+ class NodeDTO(DisplayBase):
176
+ id: UUID
177
+ type: Optional[NodeType] = None
178
+ name: str
179
+
180
+
181
+ Data = Dict[str, Any]
182
+ Metrics = Dict[str, Any]
183
+ Metadata = Dict[str, Any]
184
+ Tags = Dict[str, str]
185
+ Refs = Dict[str, str]
186
+
187
+
188
+ class LinkDTO(DisplayBase):
189
+ type: str
190
+ id: UUID
191
+ tree_id: Optional[UUID] = None
192
+
193
+
194
+ class ParentDTO(DisplayBase):
195
+ id: UUID
196
+
197
+
198
+ class OTelSpanKind(Enum):
199
+ SPAN_KIND_UNSPECIFIED = "SPAN_KIND_UNSPECIFIED"
200
+ # INTERNAL
201
+ SPAN_KIND_INTERNAL = "SPAN_KIND_INTERNAL"
202
+ # SYNCHRONOUS
203
+ SPAN_KIND_SERVER = "SPAN_KIND_SERVER"
204
+ SPAN_KIND_CLIENT = "SPAN_KIND_CLIENT"
205
+ # ASYNCHRONOUS
206
+ SPAN_KIND_PRODUCER = "SPAN_KIND_PRODUCER"
207
+ SPAN_KIND_CONSUMER = "SPAN_KIND_CONSUMER"
208
+
209
+
210
+ class OTelStatusCode(Enum):
211
+ STATUS_CODE_OK = "STATUS_CODE_OK"
212
+ STATUS_CODE_ERROR = "STATUS_CODE_ERROR"
213
+ STATUS_CODE_UNSET = "STATUS_CODE_UNSET"
214
+
215
+
216
+ class OTelContextDTO(DisplayBase):
217
+ trace_id: str
218
+ span_id: str
219
+
220
+
221
+ class OTelEventDTO(DisplayBase):
222
+ name: str
223
+ timestamp: datetime
224
+
225
+ attributes: Optional[Attributes] = None
226
+
227
+
228
+ class OTelLinkDTO(DisplayBase):
229
+ context: OTelContextDTO
230
+
231
+ attributes: Optional[Attributes] = None
232
+
233
+
234
+ class OTelExtraDTO(DisplayBase):
235
+ kind: Optional[str] = None
236
+
237
+ attributes: Optional[Attributes] = None
238
+ events: Optional[List[OTelEventDTO]] = None
239
+ links: Optional[List[OTelLinkDTO]] = None
240
+
241
+
242
+ class SpanDTO(DisplayBase):
243
+ scope: ProjectScopeDTO
244
+
245
+ lifecycle: LifecycleDTO
246
+
247
+ root: RootDTO
248
+ tree: TreeDTO
249
+ node: NodeDTO
250
+
251
+ parent: Optional[ParentDTO] = None
252
+
253
+ time: TimeDTO
254
+ status: StatusDTO
255
+
256
+ data: Optional[Data] = None
257
+ metrics: Optional[Metrics] = None
258
+ meta: Optional[Metadata] = None
259
+ tags: Optional[Tags] = None
260
+ refs: Optional[Refs] = None
261
+
262
+ links: Optional[List[LinkDTO]] = None
263
+
264
+ otel: Optional[OTelExtraDTO] = None
265
+
266
+ nodes: Optional[Dict[str, Union["SpanDTO", List["SpanDTO"]]]] = None
267
+
268
+
269
+ class SpanCreateDTO(DisplayBase):
270
+ scope: ProjectScopeDTO
271
+
272
+ root: RootDTO
273
+ tree: TreeDTO
274
+ node: NodeDTO
275
+
276
+ parent: Optional[ParentDTO] = None
277
+
278
+ time: TimeDTO
279
+ status: StatusDTO
280
+
281
+ data: Optional[Data] = None
282
+ metrics: Optional[Metrics] = None
283
+ meta: Optional[Metadata] = None
284
+ tags: Optional[Tags] = None
285
+ refs: Optional[Refs] = None
286
+
287
+ links: Optional[List[LinkDTO]] = None
288
+
289
+ otel: Optional[OTelExtraDTO] = None
290
+
291
+
292
+ class OTelSpanDTO(DisplayBase):
293
+ context: OTelContextDTO
294
+
295
+ name: str
296
+ kind: OTelSpanKind = OTelSpanKind.SPAN_KIND_UNSPECIFIED
297
+
298
+ start_time: datetime
299
+ end_time: datetime
300
+
301
+ status_code: OTelStatusCode = OTelStatusCode.STATUS_CODE_UNSET
302
+ status_message: Optional[str] = None
303
+
304
+ attributes: Optional[Attributes] = None
305
+ events: Optional[List[OTelEventDTO]] = None
306
+
307
+ parent: Optional[OTelContextDTO] = None
308
+ links: Optional[List[OTelLinkDTO]] = None
309
+
310
+
311
+ ### --------------------------- ###
312
+ ### services.observability.dtos ###
313
+ ###################################
314
+
315
+
316
+ ####################################
317
+ ### services.observability.utils ###
318
+ ### ---------------------------- ###
319
+
320
+ from typing import List, Dict, OrderedDict
321
+
322
+
323
+ def parse_span_dtos_to_span_idx(
324
+ span_dtos: List[SpanCreateDTO],
325
+ ) -> Dict[str, SpanCreateDTO]:
326
+ span_idx = {span_dto.node.id: span_dto for span_dto in span_dtos}
327
+
328
+ return span_idx
329
+
330
+
331
+ def parse_span_idx_to_span_id_tree(
332
+ span_idx: Dict[str, SpanCreateDTO],
333
+ ) -> OrderedDict:
334
+ span_id_tree = OrderedDict()
335
+ index = {}
336
+
337
+ def push(span_dto: SpanCreateDTO) -> None:
338
+ if span_dto.parent is None:
339
+ span_id_tree[span_dto.node.id] = OrderedDict()
340
+ index[span_dto.node.id] = span_id_tree[span_dto.node.id]
341
+ elif span_dto.parent.id in index:
342
+ index[span_dto.parent.id][span_dto.node.id] = OrderedDict()
343
+ index[span_dto.node.id] = index[span_dto.parent.id][span_dto.node.id]
344
+
345
+ for span_dto in sorted(span_idx.values(), key=lambda span_dto: span_dto.time.start):
346
+ push(span_dto)
347
+
348
+ return span_id_tree
349
+
350
+
351
+ def cumulate_costs(
352
+ spans_id_tree: OrderedDict,
353
+ spans_idx: Dict[str, SpanCreateDTO],
354
+ ) -> None:
355
+ def _get_unit(span: SpanCreateDTO):
356
+ if span.metrics is not None:
357
+ return span.metrics.get("unit.costs.total", 0.0)
358
+
359
+ return 0.0
360
+
361
+ def _get_acc(span: SpanCreateDTO):
362
+ if span.metrics is not None:
363
+ return span.metrics.get("acc.costs.total", 0.0)
364
+
365
+ return 0.0
366
+
367
+ def _acc(a: float, b: float):
368
+ return a + b
369
+
370
+ def _set(span: SpanCreateDTO, cost: float):
371
+ if span.metrics is None:
372
+ span.metrics = {}
373
+
374
+ if cost != 0.0:
375
+ span.metrics["acc.costs.total"] = cost
376
+
377
+ _cumulate_tree_dfs(spans_id_tree, spans_idx, _get_unit, _get_acc, _acc, _set)
378
+
379
+
380
+ def cumulate_tokens(
381
+ spans_id_tree: OrderedDict,
382
+ spans_idx: Dict[str, dict],
383
+ ) -> None:
384
+ def _get_unit(span: SpanCreateDTO):
385
+ _tokens = {
386
+ "prompt": 0.0,
387
+ "completion": 0.0,
388
+ "total": 0.0,
389
+ }
390
+
391
+ if span.metrics is not None:
392
+ return {
393
+ "prompt": span.metrics.get("unit.tokens.prompt", 0.0),
394
+ "completion": span.metrics.get("unit.tokens.completion", 0.0),
395
+ "total": span.metrics.get("unit.tokens.total", 0.0),
396
+ }
397
+
398
+ return _tokens
399
+
400
+ def _get_acc(span: SpanCreateDTO):
401
+ _tokens = {
402
+ "prompt": 0.0,
403
+ "completion": 0.0,
404
+ "total": 0.0,
405
+ }
406
+
407
+ if span.metrics is not None:
408
+ return {
409
+ "prompt": span.metrics.get("acc.tokens.prompt", 0.0),
410
+ "completion": span.metrics.get("acc.tokens.completion", 0.0),
411
+ "total": span.metrics.get("acc.tokens.total", 0.0),
412
+ }
413
+
414
+ return _tokens
415
+
416
+ def _acc(a: dict, b: dict):
417
+ return {
418
+ "prompt": a.get("prompt", 0.0) + b.get("prompt", 0.0),
419
+ "completion": a.get("completion", 0.0) + b.get("completion", 0.0),
420
+ "total": a.get("total", 0.0) + b.get("total", 0.0),
421
+ }
422
+
423
+ def _set(span: SpanCreateDTO, tokens: dict):
424
+ if span.metrics is None:
425
+ span.metrics = {}
426
+
427
+ if tokens.get("prompt", 0.0) != 0.0:
428
+ span.metrics["acc.tokens.prompt"] = tokens.get("prompt", 0.0)
429
+ if tokens.get("completion", 0.0) != 0.0:
430
+ span.metrics["acc.tokens.completion"] = (
431
+ tokens.get("completion", 0.0)
432
+ if tokens.get("completion", 0.0) != 0.0
433
+ else None
434
+ )
435
+ if tokens.get("total", 0.0) != 0.0:
436
+ span.metrics["acc.tokens.total"] = (
437
+ tokens.get("total", 0.0) if tokens.get("total", 0.0) != 0.0 else None
438
+ )
439
+
440
+ _cumulate_tree_dfs(spans_id_tree, spans_idx, _get_unit, _get_acc, _acc, _set)
441
+
442
+
443
+ def _cumulate_tree_dfs(
444
+ spans_id_tree: OrderedDict,
445
+ spans_idx: Dict[str, SpanCreateDTO],
446
+ get_unit_metric,
447
+ get_acc_metric,
448
+ accumulate_metric,
449
+ set_metric,
450
+ ):
451
+ for span_id, children_spans_id_tree in spans_id_tree.items():
452
+ children_spans_id_tree: OrderedDict
453
+
454
+ cumulated_metric = get_unit_metric(spans_idx[span_id])
455
+
456
+ _cumulate_tree_dfs(
457
+ children_spans_id_tree,
458
+ spans_idx,
459
+ get_unit_metric,
460
+ get_acc_metric,
461
+ accumulate_metric,
462
+ set_metric,
463
+ )
464
+
465
+ for child_span_id in children_spans_id_tree.keys():
466
+ marginal_metric = get_acc_metric(spans_idx[child_span_id])
467
+ cumulated_metric = accumulate_metric(cumulated_metric, marginal_metric)
468
+
469
+ set_metric(spans_idx[span_id], cumulated_metric)
470
+
471
+
472
+ def connect_children(
473
+ spans_id_tree: OrderedDict,
474
+ spans_idx: Dict[str, dict],
475
+ ) -> None:
476
+ _connect_tree_dfs(spans_id_tree, spans_idx)
477
+
478
+
479
+ def _connect_tree_dfs(
480
+ spans_id_tree: OrderedDict,
481
+ spans_idx: Dict[str, SpanDTO],
482
+ ):
483
+ for span_id, children_spans_id_tree in spans_id_tree.items():
484
+ children_spans_id_tree: OrderedDict
485
+
486
+ parent_span = spans_idx[span_id]
487
+
488
+ parent_span.nodes = dict()
489
+
490
+ _connect_tree_dfs(children_spans_id_tree, spans_idx)
491
+
492
+ for child_span_id in children_spans_id_tree.keys():
493
+ child_span_name = spans_idx[child_span_id].node.name
494
+ if child_span_name not in parent_span.nodes:
495
+ parent_span.nodes[child_span_name] = spans_idx[child_span_id]
496
+ else:
497
+ if not isinstance(parent_span.nodes[child_span_name], list):
498
+ parent_span.nodes[child_span_name] = [
499
+ parent_span.nodes[child_span_name]
500
+ ]
501
+
502
+ parent_span.nodes[child_span_name].append(spans_idx[child_span_id])
503
+
504
+ if len(parent_span.nodes) == 0:
505
+ parent_span.nodes = None
506
+
507
+
508
+ ### ---------------------------- ###
509
+ ### services.observability.utils ###
510
+ ####################################
511
+
512
+
513
+ ########################################################
514
+ ### apis.fastapi.observability.opentelemetry.semconv ###
515
+ ### ------------------------------------------------ ###
516
+
517
+ VERSION = "0.4.1"
518
+
519
+ V_0_4_1_ATTRIBUTES_EXACT = [
520
+ ("gen_ai.system", "ag.meta.system"),
521
+ ("gen_ai.request.base_url", "ag.meta.request.base_url"),
522
+ ("gen_ai.request.endpoint", "ag.meta.request.endpoint"),
523
+ ("gen_ai.request.headers", "ag.meta.request.headers"),
524
+ ("gen_ai.request.type", "ag.type.node"),
525
+ ("gen_ai.request.streaming", "ag.meta.request.streaming"),
526
+ ("gen_ai.request.model", "ag.meta.request.model"),
527
+ ("gen_ai.request.max_tokens", "ag.meta.request.max_tokens"),
528
+ ("gen_ai.request.temperature", "ag.meta.request.temperature"),
529
+ ("gen_ai.request.top_p", "ag.meta.request.top_p"),
530
+ ("gen_ai.response.model", "ag.meta.response.model"),
531
+ ("gen_ai.usage.prompt_tokens", "ag.metrics.unit.tokens.prompt"),
532
+ ("gen_ai.usage.completion_tokens", "ag.metrics.unit.tokens.completion"),
533
+ ("gen_ai.usage.total_tokens", "ag.metrics.unit.tokens.total"),
534
+ ("llm.headers", "ag.meta.request.headers"),
535
+ ("llm.request.type", "ag.type.node"),
536
+ ("llm.top_k", "ag.meta.request.top_k"),
537
+ ("llm.is_streaming", "ag.meta.request.streaming"),
538
+ ("llm.usage.total_tokens", "ag.metrics.unit.tokens.total"),
539
+ ("gen_ai.openai.api_base", "ag.meta.request.base_url"),
540
+ ("db.system", "ag.meta.system"),
541
+ ("db.vector.query.top_k", "ag.meta.request.top_k"),
542
+ ("pinecone.query.top_k", "ag.meta.request.top_k"),
543
+ ("traceloop.span.kind", "ag.type.node"),
544
+ ]
545
+ V_0_4_1_ATTRIBUTES_PREFIX = [
546
+ ("gen_ai.prompt", "ag.data.inputs.prompt"),
547
+ ("gen_ai.completion", "ag.data.outputs.completion"),
548
+ ]
549
+
550
+ V_0_4_1_MAPS = {
551
+ "attributes": {
552
+ "exact": {
553
+ "from": {otel: agenta for otel, agenta in V_0_4_1_ATTRIBUTES_EXACT[::-1]},
554
+ "to": {agenta: otel for otel, agenta in V_0_4_1_ATTRIBUTES_EXACT[::-1]},
555
+ },
556
+ "prefix": {
557
+ "from": {otel: agenta for otel, agenta in V_0_4_1_ATTRIBUTES_PREFIX[::-1]},
558
+ "to": {agenta: otel for otel, agenta in V_0_4_1_ATTRIBUTES_PREFIX[::-1]},
559
+ },
560
+ },
561
+ }
562
+ V_0_4_1_KEYS = {
563
+ "attributes": {
564
+ "exact": {
565
+ "from": list(V_0_4_1_MAPS["attributes"]["exact"]["from"].keys()),
566
+ "to": list(V_0_4_1_MAPS["attributes"]["exact"]["to"].keys()),
567
+ },
568
+ "prefix": {
569
+ "from": list(V_0_4_1_MAPS["attributes"]["prefix"]["from"].keys()),
570
+ "to": list(V_0_4_1_MAPS["attributes"]["prefix"]["to"].keys()),
571
+ },
572
+ },
573
+ }
574
+
575
+
576
+ MAPS = {
577
+ "0.4.1": V_0_4_1_MAPS, # LATEST
578
+ }
579
+ KEYS = {
580
+ "0.4.1": V_0_4_1_KEYS, # LATEST
581
+ }
582
+
583
+ CODEX = {"maps": MAPS[VERSION], "keys": KEYS[VERSION]}
584
+
585
+ ### ------------------------------------------------ ###
586
+ ### apis.fastapi.observability.opentelemetry.semconv ###
587
+ ########################################################
588
+
589
+
590
+ ########################################
591
+ ### apis.fastapi.observability.utils ###
592
+ ### -------------------------------- ###
593
+
594
+ from typing import Optional, Union, Tuple, Any, List, Dict
595
+ from uuid import UUID
596
+ from collections import OrderedDict
597
+ from json import loads, JSONDecodeError, dumps
598
+ from copy import copy
599
+
600
+
601
+ def _unmarshal_attributes(
602
+ marshalled: Dict[str, Any],
603
+ ) -> Dict[str, Any]:
604
+ """
605
+ Unmarshals a dictionary of marshalled attributes into a nested dictionary
606
+
607
+ Example:
608
+ marshalled = {
609
+ "ag.type": "tree",
610
+ "ag.node.name": "root",
611
+ "ag.node.children.0.name": "child1",
612
+ "ag.node.children.1.name": "child2"
613
+ }
614
+ unmarshalled = {
615
+ "ag": {
616
+ "type": "tree",
617
+ "node": {
618
+ "name": "root",
619
+ "children": [
620
+ {
621
+ "name": "child1",
622
+ },
623
+ {
624
+ "name": "child2",
625
+ }
626
+ ]
627
+ }
628
+ }
629
+ }
630
+ """
631
+ unmarshalled = {}
632
+
633
+ for key, value in marshalled.items():
634
+ keys = key.split(".")
635
+
636
+ level = unmarshalled
637
+
638
+ for i, part in enumerate(keys[:-1]):
639
+ if part.isdigit():
640
+ part = int(part)
641
+
642
+ if not isinstance(level, list):
643
+ level = []
644
+
645
+ while len(level) <= part:
646
+ level.append({})
647
+
648
+ level = level[part]
649
+
650
+ else:
651
+ if part not in level:
652
+ level[part] = {} if not keys[i + 1].isdigit() else []
653
+
654
+ level = level[part]
655
+
656
+ last_key = keys[-1]
657
+
658
+ if last_key.isdigit():
659
+ last_key = int(last_key)
660
+
661
+ if not isinstance(level, list):
662
+ level = []
663
+
664
+ while len(level) <= last_key:
665
+ level.append(None)
666
+
667
+ level[last_key] = value
668
+
669
+ else:
670
+ level[last_key] = value
671
+
672
+ return unmarshalled
673
+
674
+
675
+ def _encode_key(
676
+ namespace,
677
+ key: str,
678
+ ) -> str:
679
+ return f"ag.{namespace}.{key}"
680
+
681
+
682
+ def _decode_key(
683
+ namespace,
684
+ key: str,
685
+ ) -> str:
686
+ return key.replace(f"ag.{namespace}.", "")
687
+
688
+
689
+ def _decode_value(
690
+ value: Any,
691
+ ) -> Any:
692
+ if isinstance(value, (int, float, bool, bytes)):
693
+ return value
694
+
695
+ if isinstance(value, str):
696
+ if value == "@ag.type=none:":
697
+ return None
698
+
699
+ if value.startswith("@ag.type=json:"):
700
+ encoded = value[len("@ag.type=json:") :]
701
+ value = loads(encoded)
702
+ return value
703
+
704
+ return value
705
+
706
+ return value
707
+
708
+
709
+ def _get_attributes(
710
+ attributes: Attributes,
711
+ namespace: str,
712
+ ):
713
+ return {
714
+ _decode_key(namespace, key): _decode_value(value)
715
+ for key, value in attributes.items()
716
+ if key != _decode_key(namespace, key)
717
+ }
718
+
719
+
720
+ def _parse_from_types(
721
+ otel_span_dto: OTelSpanDTO,
722
+ ) -> dict:
723
+ types = _get_attributes(otel_span_dto.attributes, "type")
724
+
725
+ if types.get("tree"):
726
+ del otel_span_dto.attributes[_encode_key("type", "tree")]
727
+
728
+ if types.get("node"):
729
+ del otel_span_dto.attributes[_encode_key("type", "node")]
730
+
731
+ return types
732
+
733
+
734
+ def _parse_from_semconv(
735
+ attributes: Attributes,
736
+ ) -> None:
737
+ _attributes = copy(attributes)
738
+
739
+ for old_key, value in _attributes.items():
740
+ if old_key in CODEX["keys"]["attributes"]["exact"]["from"]:
741
+ new_key = CODEX["maps"]["attributes"]["exact"]["from"][old_key]
742
+
743
+ attributes[new_key] = value
744
+
745
+ del attributes[old_key]
746
+
747
+ else:
748
+ for prefix_key in CODEX["keys"]["attributes"]["prefix"]["from"]:
749
+ if old_key.startswith(prefix_key):
750
+ prefix = CODEX["maps"]["attributes"]["prefix"]["from"][prefix_key]
751
+
752
+ new_key = old_key.replace(prefix_key, prefix)
753
+
754
+ attributes[new_key] = value
755
+
756
+ del attributes[old_key]
757
+
758
+
759
+ def _parse_from_links(
760
+ otel_span_dto: OTelSpanDTO,
761
+ ) -> dict:
762
+ # TESTING
763
+ otel_span_dto.links = [
764
+ OTelLinkDTO(
765
+ context=otel_span_dto.context,
766
+ attributes={"ag.type.link": "testcase"},
767
+ )
768
+ ]
769
+ # -------
770
+
771
+ # LINKS
772
+ links = None
773
+ otel_links = None
774
+
775
+ if otel_span_dto.links:
776
+ links = list()
777
+ otel_links = list()
778
+
779
+ for link in otel_span_dto.links:
780
+ _links = _get_attributes(link.attributes, "type")
781
+
782
+ if _links:
783
+ link_type = _links.get("link")
784
+ link_tree_id = str(UUID(link.context.trace_id[2:]))
785
+ link_node_id = str(
786
+ UUID(link.context.trace_id[2 + 16 :] + link.context.span_id[2:])
787
+ )
788
+
789
+ links.append(
790
+ LinkDTO(
791
+ type=link_type,
792
+ tree_id=link_tree_id,
793
+ id=link_node_id,
794
+ )
795
+ )
796
+ else:
797
+ otel_links.append(link)
798
+
799
+ links = links if links else None
800
+ otel_links = otel_links if otel_links else None
801
+
802
+ otel_span_dto.links = otel_links
803
+
804
+ return links
805
+
806
+
807
+ def _parse_from_attributes(
808
+ otel_span_dto: OTelSpanDTO,
809
+ ) -> Tuple[dict, dict, dict, dict, dict]:
810
+ # DATA
811
+ _data = _get_attributes(otel_span_dto.attributes, "data")
812
+
813
+ for key in _data.keys():
814
+ del otel_span_dto.attributes[_encode_key("data", key)]
815
+
816
+ # _data = _unmarshal_attributes(_data)
817
+ _data = _data if _data else None
818
+
819
+ # METRICS
820
+ _metrics = _get_attributes(otel_span_dto.attributes, "metrics")
821
+
822
+ for key in _metrics.keys():
823
+ del otel_span_dto.attributes[_encode_key("metrics", key)]
824
+
825
+ # _metrics = _unmarshal_attributes(_metrics)
826
+ _metrics = _metrics if _metrics else None
827
+
828
+ # META
829
+ _meta = _get_attributes(otel_span_dto.attributes, "meta")
830
+
831
+ for key in _meta.keys():
832
+ del otel_span_dto.attributes[_encode_key("meta", key)]
833
+
834
+ # _meta = _unmarshal_attributes(_meta)
835
+ _meta = _meta if _meta else None
836
+
837
+ # TAGS
838
+ _tags = _get_attributes(otel_span_dto.attributes, "tags")
839
+
840
+ for key in _tags.keys():
841
+ del otel_span_dto.attributes[_encode_key("tags", key)]
842
+
843
+ _tags = _tags if _tags else None
844
+
845
+ # REFS
846
+ _refs = _get_attributes(otel_span_dto.attributes, "refs")
847
+
848
+ for key in _refs.keys():
849
+ del otel_span_dto.attributes[_encode_key("refs", key)]
850
+
851
+ _refs = _refs if _refs else None
852
+
853
+ if len(otel_span_dto.attributes.keys()) < 1:
854
+ otel_span_dto.attributes = None
855
+
856
+ return _data, _metrics, _meta, _tags, _refs
857
+
858
+
859
+ def parse_from_otel_span_dto(
860
+ project_id: str,
861
+ otel_span_dto: OTelSpanDTO,
862
+ ) -> SpanDTO:
863
+ scope = ProjectScopeDTO(project_id=UUID(project_id))
864
+
865
+ lifecyle = LifecycleDTO(
866
+ created_at=datetime.now(),
867
+ )
868
+
869
+ _parse_from_semconv(otel_span_dto.attributes)
870
+
871
+ types = _parse_from_types(otel_span_dto)
872
+
873
+ tree_id = UUID(otel_span_dto.context.trace_id[2:])
874
+
875
+ tree_type: str = types.get("tree")
876
+
877
+ tree = TreeDTO(
878
+ id=tree_id,
879
+ type=tree_type.lower() if tree_type else None,
880
+ )
881
+
882
+ node_id = UUID(tree_id.hex[16:] + otel_span_dto.context.span_id[2:])
883
+
884
+ node_type: str = types.get("node")
885
+
886
+ node = NodeDTO(
887
+ id=node_id,
888
+ type=node_type.lower() if node_type else None,
889
+ name=otel_span_dto.name,
890
+ )
891
+
892
+ parent = (
893
+ ParentDTO(
894
+ id=(
895
+ UUID(
896
+ otel_span_dto.parent.trace_id[2 + 16 :]
897
+ + otel_span_dto.parent.span_id[2:]
898
+ )
899
+ )
900
+ )
901
+ if otel_span_dto.parent
902
+ else None
903
+ )
904
+
905
+ duration = (otel_span_dto.end_time - otel_span_dto.start_time).total_seconds()
906
+
907
+ time = TimeDTO(
908
+ start=otel_span_dto.start_time,
909
+ end=otel_span_dto.end_time,
910
+ span=round(duration * 1_000_000), # microseconds
911
+ )
912
+
913
+ status = StatusDTO(
914
+ code=otel_span_dto.status_code.value.replace("STATUS_CODE_", ""),
915
+ message=otel_span_dto.status_message,
916
+ )
917
+
918
+ links = _parse_from_links(otel_span_dto)
919
+
920
+ data, metrics, meta, tags, refs = _parse_from_attributes(otel_span_dto)
921
+
922
+ root_id = str(tree_id)
923
+ if refs is not None:
924
+ root_id = refs.get("scenario.id", root_id)
925
+
926
+ root = RootDTO(id=UUID(root_id))
927
+
928
+ otel = OTelExtraDTO(
929
+ kind=otel_span_dto.kind.value,
930
+ attributes=otel_span_dto.attributes,
931
+ events=otel_span_dto.events,
932
+ links=otel_span_dto.links,
933
+ )
934
+
935
+ span_dto = SpanDTO(
936
+ scope=scope,
937
+ lifecycle=lifecyle,
938
+ root=root,
939
+ tree=tree,
940
+ node=node,
941
+ parent=parent,
942
+ time=time,
943
+ status=status,
944
+ data=data,
945
+ metrics=metrics,
946
+ meta=meta,
947
+ tags=tags,
948
+ refs=refs,
949
+ links=links,
950
+ otel=otel,
951
+ )
952
+
953
+ return span_dto
954
+
955
+
956
+ def parse_to_agenta_span_dto(
957
+ span_dto: SpanDTO,
958
+ ) -> SpanDTO:
959
+ # DATA
960
+ if span_dto.data:
961
+ span_dto.data = _unmarshal_attributes(span_dto.data)
962
+
963
+ # if "outputs" in span_dto.data:
964
+ # if "__default__" in span_dto.data["outputs"]:
965
+ # span_dto.data["outputs"] = span_dto.data["outputs"]["__default__"]
966
+
967
+ # METRICS
968
+ if span_dto.metrics:
969
+ span_dto.metrics = _unmarshal_attributes(span_dto.metrics)
970
+
971
+ # META
972
+ if span_dto.meta:
973
+ span_dto.meta = _unmarshal_attributes(span_dto.meta)
974
+
975
+ # TAGS
976
+ if span_dto.tags:
977
+ span_dto.tags = _unmarshal_attributes(span_dto.tags)
978
+
979
+ # REFS
980
+ if span_dto.refs:
981
+ span_dto.refs = _unmarshal_attributes(span_dto.refs)
982
+
983
+ for link in span_dto.links:
984
+ link.tree_id = None
985
+
986
+ if span_dto.nodes:
987
+ for v in span_dto.nodes.values():
988
+ if isinstance(v, list):
989
+ for n in v:
990
+ parse_to_agenta_span_dto(n)
991
+ else:
992
+ parse_to_agenta_span_dto(v)
993
+
994
+ return span_dto
995
+
996
+
997
+ ### -------------------------------- ###
998
+ ### apis.fastapi.observability.utils ###
999
+ ########################################
1000
+
1001
+
1002
+ from litellm import cost_calculator
1003
+ from opentelemetry.sdk.trace import ReadableSpan
1004
+
1005
+
1006
+ def parse_inline_trace(
1007
+ project_id: str,
1008
+ spans: Dict[str, ReadableSpan],
1009
+ ):
1010
+ otel_span_dtos = _parse_readable_spans(spans)
1011
+
1012
+ ############################################################
1013
+ ### apis.fastapi.observability.api.otlp_collect_traces() ###
1014
+ ### ---------------------------------------------------- ###
1015
+ span_dtos = [
1016
+ parse_from_otel_span_dto(project_id, otel_span_dto)
1017
+ for otel_span_dto in otel_span_dtos
1018
+ ]
1019
+ ### ---------------------------------------------------- ###
1020
+ ### apis.fastapi.observability.api.otlp_collect_traces() ###
1021
+ ############################################################
1022
+
1023
+ #####################################################
1024
+ ### services.observability.service.ingest/query() ###
1025
+ ### --------------------------------------------- ###
1026
+ span_idx = parse_span_dtos_to_span_idx(span_dtos)
1027
+ span_id_tree = parse_span_idx_to_span_id_tree(span_idx)
1028
+ ### --------------------------------------------- ###
1029
+ ### services.observability.service.ingest/query() ###
1030
+ #####################################################
1031
+
1032
+ ###############################################
1033
+ ### services.observability.service.ingest() ###
1034
+ ### --------------------------------------- ###
1035
+ calculate_cost(span_idx)
1036
+ cumulate_costs(span_id_tree, span_idx)
1037
+ cumulate_tokens(span_id_tree, span_idx)
1038
+ ### --------------------------------------- ###
1039
+ ### services.observability.service.ingest() ###
1040
+ ###############################################
1041
+
1042
+ ##############################################
1043
+ ### services.observability.service.query() ###
1044
+ ### -------------------------------------- ###
1045
+ connect_children(span_id_tree, span_idx)
1046
+ root_span_dtos = [span_idx[span_id] for span_id in span_id_tree.keys()]
1047
+ agenta_span_dtos = [
1048
+ parse_to_agenta_span_dto(span_dto) for span_dto in root_span_dtos
1049
+ ]
1050
+ ### -------------------------------------- ###
1051
+ ### services.observability.service.query() ###
1052
+ ##############################################
1053
+
1054
+ LEGACY = True
1055
+ inline_trace = None
1056
+
1057
+ if LEGACY:
1058
+ legacy_spans = [
1059
+ _parse_to_legacy_span(span_dto) for span_dto in span_idx.values()
1060
+ ]
1061
+
1062
+ root_span = agenta_span_dtos[0]
1063
+
1064
+ trace_id = root_span.root.id.hex
1065
+ latency = root_span.time.span / 1_000_000
1066
+ cost = root_span.metrics.get("acc", {}).get("costs", {}).get("total", 0.0)
1067
+ tokens = {
1068
+ "prompt_tokens": root_span.metrics.get("acc", {})
1069
+ .get("tokens", {})
1070
+ .get("prompt", 0),
1071
+ "completion_tokens": root_span.metrics.get("acc", {})
1072
+ .get("tokens", {})
1073
+ .get("completion", 0),
1074
+ "total_tokens": root_span.metrics.get("acc", {})
1075
+ .get("tokens", {})
1076
+ .get("total", 0),
1077
+ }
1078
+
1079
+ spans = [
1080
+ loads(span.model_dump_json(exclude_none=True)) for span in legacy_spans
1081
+ ]
1082
+
1083
+ inline_trace = {
1084
+ "trace_id": trace_id,
1085
+ "latency": latency,
1086
+ "cost": cost,
1087
+ "usage": tokens,
1088
+ "spans": spans,
1089
+ }
1090
+
1091
+ else:
1092
+ spans = [
1093
+ loads(span_dto.model_dump_json(exclude_none=True))
1094
+ for span_dto in agenta_span_dtos
1095
+ ]
1096
+
1097
+ inline_trace = spans # turn into Agenta Model ?
1098
+
1099
+ return inline_trace
1100
+
1101
+
1102
+ def _parse_readable_spans(
1103
+ spans: List[ReadableSpan],
1104
+ ) -> List[OTelSpanDTO]:
1105
+ otel_span_dtos = list()
1106
+
1107
+ for span in spans:
1108
+ otel_span_dto = OTelSpanDTO(
1109
+ context=OTelContextDTO(
1110
+ trace_id=_int_to_hex(span.get_span_context().trace_id, 128),
1111
+ span_id=_int_to_hex(span.get_span_context().span_id, 64),
1112
+ ),
1113
+ name=span.name,
1114
+ kind=OTelSpanKind(
1115
+ "SPAN_KIND_"
1116
+ + (span.kind if isinstance(span.kind, str) else span.kind.name)
1117
+ ),
1118
+ start_time=_timestamp_ns_to_datetime(span.start_time),
1119
+ end_time=_timestamp_ns_to_datetime(span.end_time),
1120
+ status_code=OTelStatusCode("STATUS_CODE_" + span.status.status_code.name),
1121
+ status_message=span.status.description,
1122
+ attributes=span.attributes,
1123
+ events=[
1124
+ OTelEventDTO(
1125
+ name=event.name,
1126
+ timestamp=_timestamp_ns_to_datetime(event.timestamp),
1127
+ attributes=event.attributes,
1128
+ )
1129
+ for event in span.events
1130
+ ],
1131
+ parent=(
1132
+ OTelContextDTO(
1133
+ trace_id=_int_to_hex(span.parent.trace_id, 128),
1134
+ span_id=_int_to_hex(span.parent.span_id, 64),
1135
+ )
1136
+ if span.parent
1137
+ else None
1138
+ ),
1139
+ links=[
1140
+ OTelLinkDTO(
1141
+ context=OTelContextDTO(
1142
+ trace_id=_int_to_hex(link.context.trace_id, 128),
1143
+ span_id=_int_to_hex(link.context.span_id, 64),
1144
+ ),
1145
+ attributes=link.attributes,
1146
+ )
1147
+ for link in span.links
1148
+ ],
1149
+ )
1150
+
1151
+ otel_span_dtos.append(otel_span_dto)
1152
+
1153
+ return otel_span_dtos
1154
+
1155
+
1156
+ def _int_to_hex(integer, bits):
1157
+ _hex = hex(integer)[2:]
1158
+
1159
+ _hex = _hex.zfill(bits // 4)
1160
+
1161
+ _hex = "0x" + _hex
1162
+
1163
+ return _hex
1164
+
1165
+
1166
+ def _timestamp_ns_to_datetime(timestamp_ns):
1167
+ _datetime = datetime.fromtimestamp(
1168
+ timestamp_ns / 1_000_000_000,
1169
+ ).isoformat(
1170
+ timespec="microseconds",
1171
+ )
1172
+
1173
+ return _datetime
1174
+
1175
+
1176
+ class LlmTokens(BaseModel):
1177
+ prompt_tokens: Optional[int] = 0
1178
+ completion_tokens: Optional[int] = 0
1179
+ total_tokens: Optional[int] = 0
1180
+
1181
+
1182
+ class CreateSpan(BaseModel):
1183
+ id: str
1184
+ app_id: str
1185
+ variant_id: Optional[str] = None
1186
+ variant_name: Optional[str] = None
1187
+ inputs: Optional[Dict[str, Optional[Any]]] = None
1188
+ internals: Optional[Dict[str, Optional[Any]]] = None
1189
+ outputs: Optional[Union[str, Dict[str, Optional[Any]], List[Any]]] = None
1190
+ config: Optional[Dict[str, Optional[Any]]] = None
1191
+ environment: Optional[str] = None
1192
+ tags: Optional[List[str]] = None
1193
+ token_consumption: Optional[int] = None
1194
+ name: str
1195
+ parent_span_id: Optional[str] = None
1196
+ attributes: Optional[Dict[str, Optional[Any]]] = None
1197
+ spankind: str
1198
+ status: str
1199
+ user: Optional[str] = None
1200
+ start_time: datetime
1201
+ end_time: datetime
1202
+ tokens: Optional[LlmTokens] = None
1203
+ cost: Optional[float] = None
1204
+
1205
+
1206
+ def _parse_to_legacy_span(span: SpanDTO) -> CreateSpan:
1207
+ attributes = None
1208
+ if span.otel:
1209
+ attributes = span.otel.attributes or {}
1210
+
1211
+ if span.otel.events:
1212
+ for event in span.otel.events:
1213
+ if event.name == "exception":
1214
+ attributes.update(**event.attributes)
1215
+
1216
+ legacy_span = CreateSpan(
1217
+ id=span.node.id.hex[:24],
1218
+ spankind=span.node.type,
1219
+ name=span.node.name,
1220
+ #
1221
+ status=span.status.code.name,
1222
+ #
1223
+ start_time=span.time.start,
1224
+ end_time=span.time.end,
1225
+ #
1226
+ parent_span_id=span.parent.id.hex[:24] if span.parent else None,
1227
+ #
1228
+ inputs=span.data.get("inputs") if span.data else {},
1229
+ internals=span.data.get("internals") if span.data else {},
1230
+ outputs=span.data.get("outputs") if span.data else {},
1231
+ #
1232
+ environment=span.meta.get("environment") if span.meta else None,
1233
+ config=span.meta.get("configuration") if span.meta else None,
1234
+ #
1235
+ tokens=(
1236
+ LlmTokens(
1237
+ prompt_tokens=span.metrics.get("acc", {})
1238
+ .get("tokens", {})
1239
+ .get("prompt", 0.0),
1240
+ completion_tokens=span.metrics.get("acc", {})
1241
+ .get("tokens", {})
1242
+ .get("completion", 0.0),
1243
+ total_tokens=span.metrics.get("acc", {})
1244
+ .get("tokens", {})
1245
+ .get("total", 0.0),
1246
+ )
1247
+ if span.metrics
1248
+ else None
1249
+ ),
1250
+ cost=(
1251
+ span.metrics.get("acc", {}).get("costs", {}).get("total", 0.0)
1252
+ if span.metrics
1253
+ else None
1254
+ ),
1255
+ #
1256
+ app_id=(
1257
+ span.refs.get("application_id", "missing-app-id")
1258
+ if span.refs
1259
+ else "missing-app-id"
1260
+ ),
1261
+ #
1262
+ attributes=attributes,
1263
+ #
1264
+ variant_id=None,
1265
+ variant_name=None,
1266
+ tags=None,
1267
+ token_consumption=None,
1268
+ user=None,
1269
+ )
1270
+
1271
+ return legacy_span
1272
+
1273
+
1274
+ PAYING_TYPES = [
1275
+ "embedding",
1276
+ "query",
1277
+ "completion",
1278
+ "chat",
1279
+ "rerank",
1280
+ ]
1281
+
1282
+
1283
+ def calculate_cost(span_idx: Dict[str, SpanCreateDTO]):
1284
+ for span in span_idx.values():
1285
+ if span.node.type.name.lower() in PAYING_TYPES and span.meta and span.metrics:
1286
+ try:
1287
+ costs = cost_calculator.cost_per_token(
1288
+ model=span.meta.get("response.model"),
1289
+ prompt_tokens=span.metrics.get("unit.tokens.prompt", 0.0),
1290
+ completion_tokens=span.metrics.get("unit.tokens.completion", 0.0),
1291
+ call_type=span.node.type.name.lower(),
1292
+ response_time_ms=span.time.span // 1_000,
1293
+ )
1294
+
1295
+ if not costs:
1296
+ continue
1297
+
1298
+ prompt_cost, completion_cost = costs
1299
+ total_cost = prompt_cost + completion_cost
1300
+
1301
+ span.metrics["unit.costs.prompt"] = prompt_cost
1302
+ span.metrics["unit.costs.completion"] = completion_cost
1303
+ span.metrics["unit.costs.total"] = total_cost
1304
+
1305
+ except:
1306
+ pass