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
@@ -1,617 +0,0 @@
1
- import os
2
- import copy
3
- import json
4
- from uuid import uuid4
5
-
6
- import traceback
7
- from threading import Lock
8
- from datetime import datetime, timezone
9
- from typing import Optional, Dict, Any, List
10
-
11
- from contextlib import contextmanager
12
-
13
- from agenta.sdk.tracing.tracing_context import tracing_context, TracingContext
14
- from agenta.sdk.tracing.logger import llm_logger as logging
15
- from agenta.sdk.tracing.tasks_manager import TaskQueue
16
- from agenta.client.backend.client import AsyncAgentaApi
17
- from agenta.client.backend.client import AsyncObservabilityClient
18
- from agenta.client.backend.types.create_span import (
19
- CreateSpan,
20
- LlmTokens,
21
- )
22
- from agenta.client.backend.types.span_status_code import SpanStatusCode
23
-
24
- from bson.objectid import ObjectId
25
-
26
- VARIANT_TRACKING_FEATURE_FLAG = False
27
-
28
- from agenta.sdk.utils.debug import debug
29
-
30
-
31
- logging.setLevel("DEBUG")
32
-
33
-
34
- class SingletonMeta(type):
35
- """
36
- Thread-safe implementation of Singleton.
37
- """
38
-
39
- _instances = {} # type: ignore
40
-
41
- # We need the lock mechanism to synchronize threads \
42
- # during the initial access to the Singleton object.
43
- _lock: Lock = Lock()
44
-
45
- def __call__(cls, *args, **kwargs):
46
- """
47
- Ensures that changes to the `__init__` arguments do not affect the
48
- returned instance.
49
-
50
- Uses a lock to make this method thread-safe. If an instance of the class
51
- does not already exist, it creates one. Otherwise, it returns the
52
- existing instance.
53
- """
54
-
55
- with cls._lock:
56
- if cls not in cls._instances:
57
- instance = super().__call__(*args, **kwargs)
58
- cls._instances[cls] = instance
59
- return cls._instances[cls]
60
-
61
-
62
- class Tracing(metaclass=SingletonMeta):
63
- """The `Tracing` class is an agent for LLM tracing with specific initialization arguments.
64
-
65
- __init__ args:
66
- host (str): The URL of the backend host
67
- api_key (str): The API Key of the backend host
68
- tasks_manager (TaskQueue): The tasks manager dedicated to handling asynchronous tasks
69
- max_workers (int): The maximum number of workers to run tracing
70
- """
71
-
72
- def __init__(
73
- self,
74
- host: str,
75
- app_id: str,
76
- api_key: Optional[str] = None,
77
- max_workers: Optional[int] = None,
78
- ):
79
- self.host = host + "/api"
80
- self.api_key = api_key if api_key is not None else ""
81
- self.app_id = app_id
82
- self.tasks_manager = TaskQueue(
83
- max_workers if max_workers else 4, logger=logging
84
- )
85
- self.baggage = None
86
-
87
- @property
88
- def client(self) -> AsyncObservabilityClient:
89
- """Initialize observability async client
90
-
91
- Returns:
92
- AsyncObservabilityClient: async client
93
- """
94
-
95
- return AsyncAgentaApi(
96
- base_url=self.host, api_key=self.api_key, timeout=120 # type: ignore
97
- ).observability
98
-
99
- ### --- Context Manager --- ###
100
-
101
- @contextmanager
102
- def Context(self, **kwargs):
103
- # This will evolve as be work towards OTel compliance
104
-
105
- token = None
106
-
107
- try:
108
- if tracing_context.get() is None:
109
- token = tracing_context.set(TracingContext())
110
-
111
- self.open_span(**kwargs)
112
-
113
- yield
114
-
115
- self.set_status(status="OK")
116
-
117
- except Exception as e:
118
- logging.error(e)
119
-
120
- result = {
121
- "message": str(e),
122
- "stacktrace": traceback.format_exc(),
123
- }
124
-
125
- self.set_status(status="ERROR")
126
- self.set_attributes({"traceback_exception": traceback.format_exc()})
127
- self.store_outputs(result)
128
-
129
- raise
130
-
131
- finally:
132
- self.close_span()
133
-
134
- if token is not None:
135
- self.flush_spans()
136
-
137
- tracing_context.reset(token)
138
-
139
- ### --- API --- ###
140
-
141
- @debug()
142
- def get_context(self):
143
- tracing = tracing_context.get()
144
-
145
- return tracing
146
-
147
- @debug()
148
- def update_baggage(
149
- self,
150
- attributes: Dict[str, Any] = {},
151
- ):
152
- if self.baggage is None:
153
- self.baggage = {}
154
-
155
- for key, value in attributes.items():
156
- self.baggage[key] = value
157
-
158
- @debug()
159
- def open_trace(
160
- self,
161
- span: Optional[CreateSpan],
162
- config: Optional[Dict[str, Any]] = None,
163
- **kwargs,
164
- ) -> None:
165
- tracing = tracing_context.get()
166
-
167
- tracing.trace_id = self._create_trace_id()
168
-
169
- logging.info(f"Opening trace {tracing.trace_id}")
170
-
171
- if span is not None:
172
- ### --- TO BE CLEANED --- >>>
173
- span.environment = (
174
- self.baggage.get("environment")
175
- if self.baggage is not None
176
- else os.environ.get("environment", "unset")
177
- )
178
-
179
- span.config = (
180
- self.baggage.get("config")
181
- if not config and self.baggage is not None
182
- else None
183
- )
184
- if VARIANT_TRACKING_FEATURE_FLAG:
185
- # TODO: we should get the variant_id and variant_name (and environment) from the config object
186
- span.variant_id = config.variant_id # type: ignore
187
- span.variant_name = (config.variant_name,) # type: ignore
188
- ### --- TO BE CLEANED --- <<<
189
-
190
- logging.info(f"Opened trace {tracing.trace_id}")
191
-
192
- @debug()
193
- def set_trace_tags(self, tags: List[str]) -> None:
194
- tracing = tracing_context.get()
195
-
196
- tracing.trace_tags.extend(tags)
197
-
198
- @debug()
199
- def is_trace_ready(self):
200
- tracing = tracing_context.get()
201
-
202
- are_spans_ready = [
203
- (span.end_time == span.start_time) for span in tracing.spans.values()
204
- ]
205
-
206
- return all(are_spans_ready)
207
-
208
- @debug()
209
- def close_trace(self) -> None:
210
- """
211
- Ends the active trace and sends the recorded spans for processing.
212
-
213
- Args:
214
- parent_span (CreateSpan): The parent span of the trace.
215
-
216
- Raises:
217
- RuntimeError: If there is no active trace to end.
218
-
219
- Returns:
220
- None
221
- """
222
- tracing = tracing_context.get()
223
-
224
- logging.info(f"Closing trace {tracing.trace_id}")
225
-
226
- trace_id = tracing.trace_id
227
-
228
- if tracing.trace_id is None:
229
- logging.error("Cannot close trace, no trace to close")
230
- return
231
-
232
- if not self.api_key:
233
- logging.error("No API key")
234
- else:
235
- self._process_spans()
236
-
237
- self._clear_trace_tags()
238
-
239
- logging.info(f"Closed trace {trace_id}")
240
-
241
- @debug()
242
- def open_span(
243
- self,
244
- name: str,
245
- spankind: str,
246
- input: Dict[str, Any],
247
- active: bool = True,
248
- config: Optional[Dict[str, Any]] = None,
249
- **kwargs,
250
- ) -> CreateSpan:
251
- tracing = tracing_context.get()
252
-
253
- span_id = self._create_span_id()
254
-
255
- logging.info(f"Opening span {span_id} {spankind.upper()}")
256
-
257
- ### --- TO BE CLEANED --- >>>
258
- now = datetime.now(timezone.utc)
259
-
260
- span = CreateSpan(
261
- id=span_id,
262
- inputs=input,
263
- name=name,
264
- app_id=self.app_id,
265
- config=config,
266
- spankind=spankind.upper(),
267
- attributes={},
268
- status="UNSET",
269
- start_time=now,
270
- internals=None,
271
- outputs=None,
272
- tags=None,
273
- user=None,
274
- end_time=now,
275
- tokens=None,
276
- cost=None,
277
- token_consumption=None,
278
- parent_span_id=None,
279
- )
280
-
281
- if tracing.trace_id is None:
282
- self.open_trace(span, config)
283
- else:
284
- span.parent_span_id = tracing.active_span.id # type: ignore
285
-
286
- tracing.spans[span.id] = span
287
-
288
- if active:
289
- tracing.active_span = span
290
- else:
291
- # DETACHED SPAN
292
- pass
293
-
294
- ### --- TO BE CLEANED --- <<<
295
-
296
- logging.info(f"Opened span {span_id} {spankind.upper()}")
297
-
298
- return span
299
-
300
- @debug(req=True)
301
- def set_attributes(
302
- self, attributes: Dict[str, Any] = {}, span_id: Optional[str] = None
303
- ) -> None:
304
- """
305
- Set attributes for the active span.
306
-
307
- Args:
308
- attributes (Dict[str, Any], optional): A dictionary of attributes to set. Defaults to {}.
309
- """
310
-
311
- span = self._get_target_span(span_id)
312
-
313
- logging.info(
314
- f"Setting span {span.id} {span.spankind.upper()} attributes={attributes}"
315
- )
316
-
317
- for key, value in attributes.items():
318
- span.attributes[key] = value # type: ignore
319
-
320
- @debug()
321
- def set_status(self, status: str, span_id: Optional[str] = None) -> None:
322
- """
323
- Set status for the active span.
324
-
325
- Args:
326
- status: Enum ( UNSET, OK, ERROR )
327
- """
328
- span = self._get_target_span(span_id)
329
-
330
- logging.info(f"Setting span {span.id} {span.spankind.upper()} status={status}")
331
-
332
- span.status = status
333
-
334
- @debug()
335
- def close_span(self, span_id: Optional[str] = None) -> None:
336
- """
337
- Ends the active span, if it is a parent span, ends the trace too.
338
-
339
- Args:
340
- outputs (Dict[str, Any]): A dictionary containing the outputs of the span.
341
- It should have the following keys:
342
- - "message" (str): The message output of the span.
343
- - "cost" (Optional[Any]): The cost of the span.
344
- - "usage" (Optional[Any]): The number of tokens used in the span.
345
-
346
- Raises:
347
- ValueError: If there is no active span to end.
348
-
349
- Returns:
350
- None
351
- """
352
- span = self._get_target_span(span_id)
353
-
354
- spankind = span.spankind
355
-
356
- logging.info(f"Closing span {span.id} {spankind}")
357
-
358
- ### --- TO BE CLEANED --- >>>
359
- span.end_time = datetime.now(timezone.utc)
360
-
361
- # TODO: Remove this whole part. Setting the cost should be done through set_span_attribute
362
- if isinstance(span.outputs, dict):
363
- self._update_span_cost(span, span.outputs.get("cost", None))
364
- self._update_span_tokens(span, span.outputs.get("usage", None))
365
-
366
- span_parent_id = span.parent_span_id
367
-
368
- if span_parent_id is not None:
369
- tracing = tracing_context.get()
370
-
371
- parent_span = tracing.spans[span_parent_id]
372
- self._update_span_cost(parent_span, span.cost)
373
- self._update_span_tokens(parent_span, span.tokens)
374
-
375
- if span_id is None:
376
- tracing.active_span = parent_span
377
- ### --- TO BE CLEANED --- <<<
378
-
379
- logging.info(f"Closed span {span.id} {spankind}")
380
-
381
- @debug()
382
- def store_internals(
383
- self, internals: Dict[str, Any] = {}, span_id: Optional[str] = None
384
- ) -> None:
385
- """
386
- Set internals for the active span.
387
-
388
- Args:
389
- internals (Dict[str, Any], optional): A dictionary of local variables to set. Defaults to {}.
390
- """
391
- span = self._get_target_span(span_id)
392
-
393
- logging.info(
394
- f"Setting span {span.id} {span.spankind.upper()} internals={internals}"
395
- )
396
-
397
- if span.internals is None:
398
- span.internals = dict()
399
-
400
- for key, value in internals.items():
401
- span.internals[key] = value # type: ignore
402
-
403
- @debug()
404
- def store_outputs(
405
- self, outputs: Dict[str, Any] = {}, span_id: Optional[str] = None
406
- ) -> None:
407
- """
408
- Set outputs for the active span.
409
-
410
- Args:
411
- outputs (Dict[str, Any], optional): A dictionary of output variables to set. Defaults to {}.
412
- """
413
- span = self._get_target_span(span_id)
414
-
415
- logging.info(
416
- f"Setting span {span.id} {span.spankind.upper()} outputs={outputs}"
417
- )
418
-
419
- span.outputs = outputs
420
-
421
- @debug()
422
- def store_cost(self, cost: float = 0.0, span_id: Optional[str] = None) -> None:
423
- """
424
- ...
425
- """
426
- span = self._get_target_span(span_id)
427
-
428
- logging.info(f"Setting span {span.id} {span.spankind.upper()} cost={cost}")
429
-
430
- self._update_span_cost(span, cost)
431
-
432
- @debug()
433
- def store_usage(self, tokens: dict = {}, span_id: Optional[str] = None) -> None:
434
- """
435
- ...
436
- """
437
- span = self._get_target_span(span_id)
438
-
439
- logging.info(
440
- f"Setting span {span.id} {span.spankind.upper()} tokens={repr(tokens)}"
441
- )
442
-
443
- self._update_span_tokens(span, tokens)
444
-
445
- @debug()
446
- def dump_trace(self):
447
- """
448
- Collects and organizes tracing information into a dictionary.
449
- This function retrieves the current tracing context and extracts relevant data such as `trace_id`, `cost`, `tokens`, and `latency` for the whole trace.
450
- It also dumps detailed span information using the `dump_spans` method and includes it in the trace dictionary.
451
- If an error occurs during the process, it logs the error message and stack trace.
452
-
453
- Returns:
454
- dict: A dictionary containing the trace information.
455
- """
456
- try:
457
- trace = dict()
458
-
459
- tracing = tracing_context.get()
460
-
461
- trace["trace_id"] = tracing.trace_id
462
-
463
- for span in tracing.spans.values():
464
- if span.parent_span_id is None:
465
- trace["cost"] = span.cost
466
- trace["usage"] = (
467
- None if span.tokens is None else json.loads(span.tokens.json())
468
- )
469
- trace["latency"] = (span.end_time - span.start_time).total_seconds()
470
-
471
- spans = (
472
- []
473
- if len(tracing.spans) == 0
474
- else [json.loads(span.json()) for span in tracing.spans.values()]
475
- )
476
-
477
- if spans is not None:
478
- trace["spans"] = spans
479
-
480
- except Exception as e:
481
- logging.error(e)
482
- logging.error(traceback.format_exc())
483
-
484
- return trace
485
-
486
- def flush_spans(self) -> None:
487
- self.close_trace()
488
-
489
- ### --- Legacy API --- ###
490
-
491
- def start_trace(
492
- self,
493
- span: CreateSpan,
494
- config: Optional[Dict[str, Any]] = None,
495
- **kwargs,
496
- ) -> None: # Legacy
497
- self.open_trace(span, config, **kwargs)
498
-
499
- def end_trace(self, _: CreateSpan) -> None: # Legacy
500
- self.close_trace()
501
-
502
- def start_span(
503
- self,
504
- name: str,
505
- spankind: str,
506
- input: Dict[str, Any],
507
- config: Optional[Dict[str, Any]] = None,
508
- **kwargs,
509
- ) -> CreateSpan: # Legacy
510
- return self.open_span(name, spankind, input, config, **kwargs)
511
-
512
- def update_span_status(self, _: CreateSpan, status: str) -> None: # Legacy
513
- self.update_span_status(status)
514
-
515
- def set_span_attribute(
516
- self,
517
- attributes: Dict[str, Any] = {},
518
- ) -> None: # Legacy
519
- self.set_attributes(attributes)
520
-
521
- def end_span(self, outputs: Dict[str, Any]) -> None: # Legacy
522
- self.store_outputs(outputs)
523
- self.close_span()
524
-
525
- ### --- Helper Functions --- ###
526
-
527
- def _create_trace_id(self) -> str:
528
- """Creates a 32HEXDIGL / ObjectId ID for the trace object.
529
-
530
- Returns:
531
- str: stringify oid of the trace
532
- """
533
-
534
- # return uuid4().hex
535
- return str(ObjectId())
536
-
537
- def _clear_trace_tags(self) -> None:
538
- tracing = tracing_context.get()
539
-
540
- tracing.trace_tags.clear()
541
-
542
- def _create_span_id(self) -> str:
543
- """Creates a 16HEXDIGL / ObjectId ID for the span object.
544
-
545
- Returns:
546
- str: stringify oid of the span
547
- """
548
-
549
- # return uuid4().hex[:16]
550
- return str(ObjectId())
551
-
552
- def _get_target_span(self, span_id) -> CreateSpan:
553
- tracing = tracing_context.get()
554
-
555
- span = None
556
-
557
- if span_id is None:
558
- if tracing.active_span is None:
559
- logging.error(f"Cannot set attributes, no active span")
560
- return
561
-
562
- span = tracing.active_span
563
- else:
564
- if span_id not in tracing.spans.keys():
565
- logging.error(f"Cannot set attributes, span ({span_id}) not found")
566
- return
567
-
568
- span = tracing.spans[span_id]
569
-
570
- return span
571
-
572
- def _process_spans(self) -> None:
573
- tracing = tracing_context.get()
574
-
575
- spans = list(tracing.spans.values())
576
-
577
- logging.info(f"Sending trace {tracing.trace_id} spans={len(spans)} ")
578
-
579
- self.tasks_manager.add_task(
580
- tracing.trace_id,
581
- "send-trace",
582
- # mock_create_traces(
583
- self.client.create_traces(
584
- trace=tracing.trace_id, spans=spans # type: ignore
585
- ),
586
- self.client,
587
- )
588
-
589
- logging.info(f"Sent trace {tracing.trace_id}")
590
-
591
- def _update_span_cost(self, span: CreateSpan, cost: Optional[float]) -> None:
592
- if span is not None and cost is not None and isinstance(cost, float):
593
- if span.cost is None:
594
- span.cost = cost
595
- else:
596
- span.cost += cost
597
-
598
- def _update_span_tokens(self, span: CreateSpan, tokens: Optional[dict]) -> None:
599
- if isinstance(tokens, LlmTokens):
600
- tokens = tokens.dict()
601
- if span is not None and tokens is not None and isinstance(tokens, dict):
602
- if span.tokens is None:
603
- span.tokens = LlmTokens(**tokens)
604
- else:
605
- span.tokens.prompt_tokens += (
606
- tokens["prompt_tokens"]
607
- if tokens["prompt_tokens"] is not None
608
- else 0
609
- )
610
- span.tokens.completion_tokens += (
611
- tokens["completion_tokens"]
612
- if tokens["completion_tokens"] is not None
613
- else 0
614
- )
615
- span.tokens.total_tokens += (
616
- tokens["total_tokens"] if tokens["total_tokens"] is not None else 0
617
- )