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