agenta 0.25.3__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.
- agenta/__init__.py +6 -7
- agenta/client/backend/client.py +22 -14
- agenta/client/backend/core/http_client.py +23 -15
- agenta/client/backend/core/pydantic_utilities.py +2 -2
- agenta/sdk/__init__.py +27 -6
- agenta/sdk/agenta_init.py +73 -26
- agenta/sdk/config_manager.py +2 -2
- agenta/sdk/context/__init__.py +0 -0
- agenta/sdk/context/routing.py +25 -0
- agenta/sdk/context/tracing.py +3 -0
- agenta/sdk/decorators/__init__.py +0 -0
- agenta/sdk/decorators/{llm_entrypoint.py → routing.py} +136 -125
- agenta/sdk/decorators/tracing.py +243 -81
- agenta/sdk/litellm/__init__.py +1 -0
- agenta/sdk/litellm/litellm.py +275 -0
- agenta/sdk/router.py +0 -7
- agenta/sdk/tracing/__init__.py +1 -0
- agenta/sdk/tracing/attributes.py +181 -0
- agenta/sdk/tracing/context.py +21 -0
- agenta/sdk/tracing/conventions.py +43 -0
- agenta/sdk/tracing/exporters.py +53 -0
- agenta/sdk/tracing/inline.py +1230 -0
- agenta/sdk/tracing/processors.py +65 -0
- agenta/sdk/tracing/spans.py +124 -0
- agenta/sdk/tracing/tracing.py +171 -0
- agenta/sdk/types.py +0 -12
- agenta/sdk/utils/{helper/openai_cost.py → costs.py} +3 -0
- agenta/sdk/utils/debug.py +5 -5
- agenta/sdk/utils/exceptions.py +18 -0
- agenta/sdk/utils/globals.py +3 -5
- agenta/sdk/{tracing/logger.py → utils/logging.py} +3 -5
- agenta/sdk/utils/singleton.py +13 -0
- {agenta-0.25.3.dist-info → agenta-0.25.3a1.dist-info}/METADATA +4 -1
- {agenta-0.25.3.dist-info → agenta-0.25.3a1.dist-info}/RECORD +36 -26
- agenta/sdk/context.py +0 -41
- agenta/sdk/decorators/base.py +0 -10
- agenta/sdk/tracing/callbacks.py +0 -187
- agenta/sdk/tracing/llm_tracing.py +0 -617
- agenta/sdk/tracing/tasks_manager.py +0 -129
- agenta/sdk/tracing/tracing_context.py +0 -27
- {agenta-0.25.3.dist-info → agenta-0.25.3a1.dist-info}/WHEEL +0 -0
- {agenta-0.25.3.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
|