agenta 0.26.0__py3-none-any.whl → 0.27.0__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 (85) hide show
  1. agenta/__init__.py +29 -10
  2. agenta/cli/helper.py +5 -1
  3. agenta/client/backend/__init__.py +14 -0
  4. agenta/client/backend/apps/client.py +28 -20
  5. agenta/client/backend/client.py +47 -16
  6. agenta/client/backend/containers/client.py +5 -1
  7. agenta/client/backend/core/__init__.py +2 -1
  8. agenta/client/backend/core/client_wrapper.py +6 -6
  9. agenta/client/backend/core/file.py +33 -11
  10. agenta/client/backend/core/http_client.py +45 -31
  11. agenta/client/backend/core/pydantic_utilities.py +144 -29
  12. agenta/client/backend/core/request_options.py +3 -0
  13. agenta/client/backend/core/serialization.py +139 -42
  14. agenta/client/backend/evaluations/client.py +7 -2
  15. agenta/client/backend/evaluators/client.py +349 -1
  16. agenta/client/backend/observability/client.py +11 -2
  17. agenta/client/backend/testsets/client.py +10 -10
  18. agenta/client/backend/types/__init__.py +14 -0
  19. agenta/client/backend/types/app.py +1 -0
  20. agenta/client/backend/types/app_variant_response.py +3 -1
  21. agenta/client/backend/types/config_dto.py +32 -0
  22. agenta/client/backend/types/config_response_model.py +32 -0
  23. agenta/client/backend/types/create_span.py +3 -2
  24. agenta/client/backend/types/environment_output.py +1 -0
  25. agenta/client/backend/types/environment_output_extended.py +1 -0
  26. agenta/client/backend/types/evaluation.py +1 -2
  27. agenta/client/backend/types/evaluator.py +2 -0
  28. agenta/client/backend/types/evaluator_config.py +1 -0
  29. agenta/client/backend/types/evaluator_mapping_output_interface.py +21 -0
  30. agenta/client/backend/types/evaluator_output_interface.py +21 -0
  31. agenta/client/backend/types/human_evaluation.py +1 -2
  32. agenta/client/backend/types/lifecycle_dto.py +24 -0
  33. agenta/client/backend/types/llm_tokens.py +2 -2
  34. agenta/client/backend/types/reference_dto.py +23 -0
  35. agenta/client/backend/types/reference_request_model.py +23 -0
  36. agenta/client/backend/types/span.py +1 -0
  37. agenta/client/backend/types/span_detail.py +7 -1
  38. agenta/client/backend/types/test_set_output_response.py +5 -2
  39. agenta/client/backend/types/trace_detail.py +7 -1
  40. agenta/client/backend/types/with_pagination.py +4 -2
  41. agenta/client/backend/variants/client.py +1565 -272
  42. agenta/docker/docker-assets/Dockerfile.cloud.template +1 -1
  43. agenta/sdk/__init__.py +44 -7
  44. agenta/sdk/agenta_init.py +85 -33
  45. agenta/sdk/context/__init__.py +0 -0
  46. agenta/sdk/context/routing.py +26 -0
  47. agenta/sdk/context/tracing.py +3 -0
  48. agenta/sdk/decorators/__init__.py +0 -0
  49. agenta/sdk/decorators/{llm_entrypoint.py → routing.py} +216 -191
  50. agenta/sdk/decorators/tracing.py +218 -99
  51. agenta/sdk/litellm/__init__.py +1 -0
  52. agenta/sdk/litellm/litellm.py +288 -0
  53. agenta/sdk/managers/__init__.py +6 -0
  54. agenta/sdk/managers/config.py +318 -0
  55. agenta/sdk/managers/deployment.py +45 -0
  56. agenta/sdk/managers/shared.py +639 -0
  57. agenta/sdk/managers/variant.py +182 -0
  58. agenta/sdk/router.py +0 -7
  59. agenta/sdk/tracing/__init__.py +1 -0
  60. agenta/sdk/tracing/attributes.py +141 -0
  61. agenta/sdk/tracing/context.py +24 -0
  62. agenta/sdk/tracing/conventions.py +49 -0
  63. agenta/sdk/tracing/exporters.py +65 -0
  64. agenta/sdk/tracing/inline.py +1252 -0
  65. agenta/sdk/tracing/processors.py +117 -0
  66. agenta/sdk/tracing/spans.py +136 -0
  67. agenta/sdk/tracing/tracing.py +233 -0
  68. agenta/sdk/types.py +49 -2
  69. agenta/sdk/utils/{helper/openai_cost.py → costs.py} +3 -0
  70. agenta/sdk/utils/debug.py +5 -5
  71. agenta/sdk/utils/exceptions.py +52 -0
  72. agenta/sdk/utils/globals.py +3 -5
  73. agenta/sdk/{tracing/logger.py → utils/logging.py} +3 -5
  74. agenta/sdk/utils/singleton.py +13 -0
  75. {agenta-0.26.0.dist-info → agenta-0.27.0.dist-info}/METADATA +5 -1
  76. {agenta-0.26.0.dist-info → agenta-0.27.0.dist-info}/RECORD +78 -57
  77. agenta/sdk/config_manager.py +0 -205
  78. agenta/sdk/context.py +0 -41
  79. agenta/sdk/decorators/base.py +0 -10
  80. agenta/sdk/tracing/callbacks.py +0 -187
  81. agenta/sdk/tracing/llm_tracing.py +0 -617
  82. agenta/sdk/tracing/tasks_manager.py +0 -129
  83. agenta/sdk/tracing/tracing_context.py +0 -27
  84. {agenta-0.26.0.dist-info → agenta-0.27.0.dist-info}/WHEEL +0 -0
  85. {agenta-0.26.0.dist-info → agenta-0.27.0.dist-info}/entry_points.txt +0 -0
@@ -1,131 +1,250 @@
1
- # Stdlib Imports
2
- import inspect
3
- import traceback
1
+ from typing import Callable, Optional, Any, Dict, List, Union
4
2
  from functools import wraps
5
- from typing import Any, Callable, Optional, List, Union
3
+ from itertools import chain
4
+ from inspect import iscoroutinefunction, getfullargspec
6
5
 
7
- # Own Imports
8
- import agenta as ag
9
- from agenta.sdk.decorators.base import BaseDecorator
10
- from agenta.sdk.tracing.logger import llm_logger as logging
11
- from agenta.sdk.utils.debug import debug, DEBUG, SHIFT
12
-
13
-
14
- logging.setLevel("DEBUG")
15
-
16
-
17
- class instrument(BaseDecorator):
18
- """Decorator class for monitoring llm apps functions.
6
+ from agenta.sdk.utils.exceptions import suppress
7
+ from agenta.sdk.context.tracing import tracing_context
8
+ from agenta.sdk.tracing.conventions import parse_span_kind
19
9
 
20
- Args:
21
- BaseDecorator (object): base decorator class
22
-
23
- Example:
24
- ```python
25
- import agenta as ag
26
-
27
- prompt_config = {"system_prompt": ..., "temperature": 0.5, "max_tokens": ...}
10
+ import agenta as ag
28
11
 
29
- @ag.instrument(spankind="llm")
30
- async def litellm_openai_call(prompt:str) -> str:
31
- return "do something"
32
12
 
33
- @ag.instrument(config=prompt_config) # spankind for parent span defaults to workflow
34
- async def generate(prompt: str):
35
- return ...
36
- ```
37
- """
13
+ class instrument: # pylint: disable=invalid-name
14
+ DEFAULT_KEY = "__default__"
38
15
 
39
16
  def __init__(
40
17
  self,
41
- config: Optional[dict] = None,
42
- spankind: str = "workflow",
43
- ignore_inputs: Union[List[str], bool] = False,
44
- ignore_outputs: Union[List[str], bool] = False,
18
+ type: str = "task", # pylint: disable=redefined-builtin
19
+ config: Optional[Dict[str, Any]] = None,
20
+ ignore_inputs: Optional[bool] = None,
21
+ ignore_outputs: Optional[bool] = None,
22
+ max_depth: Optional[int] = 2,
23
+ # DEPRECATING
24
+ kind: str = "task",
25
+ spankind: Optional[str] = "TASK",
45
26
  ) -> None:
27
+ self.type = spankind or kind or type
28
+ self.kind = None
46
29
  self.config = config
47
- self.spankind = spankind
48
30
  self.ignore_inputs = ignore_inputs
49
31
  self.ignore_outputs = ignore_outputs
32
+ self.max_depth = max_depth
50
33
 
51
34
  def __call__(self, func: Callable[..., Any]):
52
- is_coroutine_function = inspect.iscoroutinefunction(func)
53
-
54
- def get_inputs(*args, **kwargs):
55
- func_args = inspect.getfullargspec(func).args
56
- input_dict = {name: value for name, value in zip(func_args, args)}
57
- input_dict.update(kwargs)
58
-
59
- return input_dict
60
-
61
- def redact(io, blacklist):
62
- return {
63
- key: io[key]
64
- for key in io.keys()
65
- if key
66
- not in (
67
- blacklist
68
- if isinstance(blacklist, list)
69
- else []
70
- if blacklist is False
71
- else io.keys()
72
- )
73
- }
74
-
75
- def patch(result):
76
- TRACE_DEFAULT_KEY = "__default__"
77
-
78
- outputs = result
79
-
80
- # PATCH : if result is not a dict, make it a dict
81
- if not isinstance(result, dict):
82
- outputs = {TRACE_DEFAULT_KEY: result}
83
- else:
84
- # PATCH : if result is a legacy dict, clean it up
85
- if (
86
- "message" in result.keys()
87
- and "cost" in result.keys()
88
- and "usage" in result.keys()
89
- ):
90
- outputs = {TRACE_DEFAULT_KEY: result["message"]}
91
-
92
- ag.tracing.store_cost(result["cost"])
93
- ag.tracing.store_usage(result["usage"])
94
-
95
- return outputs
35
+ is_coroutine_function = iscoroutinefunction(func)
96
36
 
97
37
  @wraps(func)
98
38
  async def async_wrapper(*args, **kwargs):
99
- async def wrapped_func(*args, **kwargs):
100
- with ag.tracing.Context(
101
- name=func.__name__,
102
- input=redact(get_inputs(*args, **kwargs), self.ignore_inputs),
103
- spankind=self.spankind,
104
- config=self.config,
105
- ):
39
+ async def _async_auto_instrumented(*args, **kwargs):
40
+ self._parse_type_and_kind()
41
+
42
+ with ag.tracer.start_as_current_span(func.__name__, kind=self.kind):
43
+ self._pre_instrument(func, *args, **kwargs)
44
+
106
45
  result = await func(*args, **kwargs)
107
46
 
108
- ag.tracing.store_outputs(redact(patch(result), self.ignore_outputs))
47
+ self._post_instrument(result)
109
48
 
110
49
  return result
111
50
 
112
- return await wrapped_func(*args, **kwargs)
51
+ return await _async_auto_instrumented(*args, **kwargs)
113
52
 
114
53
  @wraps(func)
115
54
  def sync_wrapper(*args, **kwargs):
116
- def wrapped_func(*args, **kwargs):
117
- with ag.tracing.Context(
118
- name=func.__name__,
119
- input=redact(get_inputs(*args, **kwargs), self.ignore_inputs),
120
- spankind=self.spankind,
121
- config=self.config,
122
- ):
55
+ def _sync_auto_instrumented(*args, **kwargs):
56
+ self._parse_type_and_kind()
57
+
58
+ with ag.tracer.start_as_current_span(func.__name__, kind=self.kind):
59
+ self._pre_instrument(func, *args, **kwargs)
60
+
123
61
  result = func(*args, **kwargs)
124
62
 
125
- ag.tracing.store_outputs(redact(patch(result), self.ignore_outputs))
63
+ self._post_instrument(result)
126
64
 
127
65
  return result
128
66
 
129
- return wrapped_func(*args, **kwargs)
67
+ return _sync_auto_instrumented(*args, **kwargs)
130
68
 
131
69
  return async_wrapper if is_coroutine_function else sync_wrapper
70
+
71
+ def _parse_type_and_kind(self):
72
+ if not ag.tracing.get_current_span().is_recording():
73
+ self.type = "workflow"
74
+
75
+ self.kind = parse_span_kind(self.type)
76
+
77
+ def _pre_instrument(
78
+ self,
79
+ func,
80
+ *args,
81
+ **kwargs,
82
+ ):
83
+ span = ag.tracing.get_current_span()
84
+
85
+ with suppress():
86
+ span.set_attributes(
87
+ attributes={"node": self.type},
88
+ namespace="type",
89
+ )
90
+
91
+ if span.parent is None:
92
+ rctx = tracing_context.get()
93
+
94
+ span.set_attributes(
95
+ attributes={"configuration": rctx.get("config", {})},
96
+ namespace="meta",
97
+ )
98
+ span.set_attributes(
99
+ attributes={"environment": rctx.get("environment", {})},
100
+ namespace="meta",
101
+ )
102
+ span.set_attributes(
103
+ attributes={"version": rctx.get("version", {})},
104
+ namespace="meta",
105
+ )
106
+ span.set_attributes(
107
+ attributes={"variant": rctx.get("variant", {})},
108
+ namespace="meta",
109
+ )
110
+
111
+ _inputs = self._redact(
112
+ self._parse(
113
+ func,
114
+ *args,
115
+ **kwargs,
116
+ ),
117
+ self.ignore_inputs,
118
+ )
119
+ span.set_attributes(
120
+ attributes={"inputs": _inputs},
121
+ namespace="data",
122
+ max_depth=self.max_depth,
123
+ )
124
+
125
+ def _post_instrument(
126
+ self,
127
+ result,
128
+ ):
129
+ span = ag.tracing.get_current_span()
130
+ with suppress():
131
+ cost = None
132
+ usage = {}
133
+
134
+ if isinstance(result, dict):
135
+ cost = result.get("cost", None)
136
+ usage = result.get("usage", {})
137
+
138
+ if isinstance(usage, (int, float)):
139
+ usage = {"total_tokens": usage}
140
+
141
+ span.set_attributes(
142
+ attributes={"total": cost},
143
+ namespace="metrics.unit.costs",
144
+ )
145
+ span.set_attributes(
146
+ attributes=(
147
+ {
148
+ "prompt": usage.get("prompt_tokens", None),
149
+ "completion": usage.get("completion_tokens", None),
150
+ "total": usage.get("total_tokens", None),
151
+ }
152
+ ),
153
+ namespace="metrics.unit.tokens",
154
+ )
155
+
156
+ _outputs = self._redact(self._patch(result), self.ignore_outputs)
157
+ span.set_attributes(
158
+ attributes={"outputs": _outputs},
159
+ namespace="data",
160
+ max_depth=self.max_depth,
161
+ )
162
+
163
+ span.set_status("OK")
164
+
165
+ with suppress():
166
+ if hasattr(span, "parent") and span.parent is None:
167
+ tracing_context.set(
168
+ tracing_context.get()
169
+ | {
170
+ "root": {
171
+ "trace_id": span.get_span_context().trace_id,
172
+ "span_id": span.get_span_context().span_id,
173
+ }
174
+ }
175
+ )
176
+
177
+ def _parse(
178
+ self,
179
+ func,
180
+ *args,
181
+ **kwargs,
182
+ ) -> Dict[str, Any]:
183
+ inputs = {
184
+ key: value
185
+ for key, value in chain(
186
+ zip(getfullargspec(func).args, args),
187
+ kwargs.items(),
188
+ )
189
+ }
190
+
191
+ return inputs
192
+
193
+ def _redact(
194
+ self,
195
+ io: Dict[str, Any],
196
+ ignore: Union[List[str], bool] = False,
197
+ ) -> Dict[str, Any]:
198
+ """
199
+ Redact user-defined sensitive information
200
+ from inputs and outputs as defined by the ignore list or boolean flag.
201
+
202
+ Example:
203
+ - ignore = ["password"] -> {"username": "admin", "password": "********"}
204
+ -> {"username": "admin"}
205
+ - ignore = True -> {"username": "admin", "password": "********"}
206
+ -> {}
207
+ - ignore = False -> {"username": "admin", "password": "********"}
208
+ -> {"username": "admin", "password": "********"}
209
+ """
210
+ io = {
211
+ key: value
212
+ for key, value in io.items()
213
+ if key
214
+ not in (
215
+ ignore
216
+ if isinstance(ignore, list)
217
+ else io.keys()
218
+ if ignore is True
219
+ else []
220
+ )
221
+ }
222
+
223
+ return io
224
+
225
+ def _patch(
226
+ self,
227
+ result: Any,
228
+ ) -> Dict[str, Any]:
229
+ """
230
+ Patch the result to ensure that it is a dictionary, with a default key when necessary.
231
+
232
+ Example:
233
+ - result = "Hello, World!"
234
+ -> {"__default__": "Hello, World!"}
235
+ - result = {"message": "Hello, World!", "cost": 0.0, "usage": {}}
236
+ -> {"__default__": "Hello, World!"}
237
+ - result = {"message": "Hello, World!"}
238
+ -> {"message": "Hello, World!"}
239
+ """
240
+ outputs = (
241
+ {instrument.DEFAULT_KEY: result}
242
+ if not isinstance(result, dict)
243
+ else (
244
+ {instrument.DEFAULT_KEY: result["message"]}
245
+ if all(key in result for key in ["message", "cost", "usage"])
246
+ else result
247
+ )
248
+ )
249
+
250
+ return outputs
@@ -0,0 +1 @@
1
+ from .litellm import litellm_handler
@@ -0,0 +1,288 @@
1
+ from opentelemetry.trace import SpanKind
2
+
3
+ import agenta as ag
4
+
5
+ from agenta.sdk.tracing.spans import CustomSpan
6
+ from agenta.sdk.utils.exceptions import suppress # TODO: use it !
7
+ from agenta.sdk.utils.logging import log
8
+
9
+
10
+ def litellm_handler():
11
+ try:
12
+ from litellm.integrations.custom_logger import ( # pylint: disable=import-outside-toplevel
13
+ CustomLogger as LitellmCustomLogger,
14
+ )
15
+ except ImportError as exc:
16
+ raise ImportError(
17
+ "The litellm SDK is not installed. Please install it using `pip install litellm`."
18
+ ) from exc
19
+ except Exception as exc:
20
+ raise Exception( # pylint: disable=broad-exception-raised
21
+ f"Unexpected error occurred when importing litellm: {exc}"
22
+ ) from exc
23
+
24
+ class LitellmHandler(LitellmCustomLogger):
25
+ """
26
+ This handler is responsible for instrumenting certain events,
27
+ when using litellm to call LLMs.
28
+
29
+ Args:
30
+ LitellmCustomLogger (object): custom logger that allows us
31
+ to override the events to capture.
32
+ """
33
+
34
+ def __init__(self):
35
+ super().__init__()
36
+
37
+ self.span = None
38
+
39
+ def log_pre_api_call(
40
+ self,
41
+ model,
42
+ messages,
43
+ kwargs,
44
+ ):
45
+ type = ( # pylint: disable=redefined-builtin
46
+ "chat"
47
+ if kwargs.get("call_type") in ["completion", "acompletion"]
48
+ else "embedding"
49
+ )
50
+
51
+ kind = SpanKind.CLIENT
52
+
53
+ self.span = CustomSpan(
54
+ ag.tracer.start_span(name=f"litellm_{kind.name.lower()}", kind=kind)
55
+ )
56
+
57
+ self.span.set_attributes(
58
+ attributes={"node": type},
59
+ namespace="type",
60
+ )
61
+
62
+ if not self.span:
63
+ log.error("LiteLLM callback error: span not found.")
64
+ return
65
+
66
+ self.span.set_attributes(
67
+ attributes={"inputs": {"prompt": kwargs["messages"]}},
68
+ namespace="data",
69
+ )
70
+
71
+ self.span.set_attributes(
72
+ attributes={
73
+ "configuration": {
74
+ "model": kwargs.get("model"),
75
+ **kwargs.get("optional_params"),
76
+ }
77
+ },
78
+ namespace="meta",
79
+ )
80
+
81
+ def log_stream_event(
82
+ self,
83
+ kwargs,
84
+ response_obj,
85
+ start_time,
86
+ end_time,
87
+ ):
88
+ if not self.span:
89
+ log.error("LiteLLM callback error: span not found.")
90
+ return
91
+
92
+ result = kwargs.get("complete_streaming_response")
93
+
94
+ outputs = (
95
+ {"__default__": result} if not isinstance(result, dict) else result
96
+ )
97
+
98
+ self.span.set_attributes(
99
+ attributes={"outputs": outputs},
100
+ namespace="data",
101
+ )
102
+
103
+ self.span.set_attributes(
104
+ attributes={"total": kwargs.get("response_cost")},
105
+ namespace="metrics.unit.costs",
106
+ )
107
+
108
+ self.span.set_attributes(
109
+ attributes=(
110
+ {
111
+ "prompt": response_obj.usage.prompt_tokens,
112
+ "completion": response_obj.usage.completion_tokens,
113
+ "total": response_obj.usage.total_tokens,
114
+ }
115
+ ),
116
+ namespace="metrics.unit.tokens",
117
+ )
118
+
119
+ self.span.set_status(status="OK")
120
+
121
+ self.span.end()
122
+
123
+ def log_success_event(
124
+ self,
125
+ kwargs,
126
+ response_obj,
127
+ start_time,
128
+ end_time,
129
+ ):
130
+ if not self.span:
131
+ log.error("LiteLLM callback error: span not found.")
132
+ return
133
+
134
+ try:
135
+ result = []
136
+ for choice in response_obj.choices:
137
+ message = choice.message.__dict__
138
+ result.append(message)
139
+
140
+ outputs = {"completion": result}
141
+ self.span.set_attributes(
142
+ attributes={"outputs": outputs},
143
+ namespace="data",
144
+ )
145
+
146
+ except Exception as e:
147
+ pass
148
+
149
+ self.span.set_attributes(
150
+ attributes={"total": kwargs.get("response_cost")},
151
+ namespace="metrics.unit.costs",
152
+ )
153
+
154
+ self.span.set_attributes(
155
+ attributes=(
156
+ {
157
+ "prompt": response_obj.usage.prompt_tokens,
158
+ "completion": response_obj.usage.completion_tokens,
159
+ "total": response_obj.usage.total_tokens,
160
+ }
161
+ ),
162
+ namespace="metrics.unit.tokens",
163
+ )
164
+
165
+ self.span.set_status(status="OK")
166
+
167
+ self.span.end()
168
+
169
+ def log_failure_event(
170
+ self,
171
+ kwargs,
172
+ response_obj,
173
+ start_time,
174
+ end_time,
175
+ ):
176
+ if not self.span:
177
+ log.error("LiteLLM callback error: span not found.")
178
+ return
179
+
180
+ self.span.record_exception(kwargs["exception"])
181
+
182
+ self.span.set_status(status="ERROR")
183
+
184
+ self.span.end()
185
+
186
+ async def async_log_stream_event(
187
+ self,
188
+ kwargs,
189
+ response_obj,
190
+ start_time,
191
+ end_time,
192
+ ):
193
+ if not self.span:
194
+ log.error("LiteLLM callback error: span not found.")
195
+ return
196
+
197
+ result = kwargs.get("complete_streaming_response")
198
+
199
+ outputs = (
200
+ {"__default__": result} if not isinstance(result, dict) else result
201
+ )
202
+
203
+ self.span.set_attributes(
204
+ attributes={"outputs": outputs},
205
+ namespace="data",
206
+ )
207
+
208
+ self.span.set_attributes(
209
+ attributes={"total": kwargs.get("response_cost")},
210
+ namespace="metrics.unit.costs",
211
+ )
212
+
213
+ self.span.set_attributes(
214
+ attributes=(
215
+ {
216
+ "prompt": response_obj.usage.prompt_tokens,
217
+ "completion": response_obj.usage.completion_tokens,
218
+ "total": response_obj.usage.total_tokens,
219
+ }
220
+ ),
221
+ namespace="metrics.unit.tokens",
222
+ )
223
+
224
+ self.span.set_status(status="OK")
225
+
226
+ self.span.end()
227
+
228
+ async def async_log_success_event(
229
+ self,
230
+ kwargs,
231
+ response_obj,
232
+ start_time,
233
+ end_time,
234
+ ):
235
+ if not self.span:
236
+ log.error("LiteLLM callback error: span not found.")
237
+ return
238
+
239
+ # result = kwargs.get("complete_streaming_response")
240
+ result = response_obj.choices[0].message.content
241
+
242
+ outputs = (
243
+ {"__default__": result} if not isinstance(result, dict) else result
244
+ )
245
+
246
+ self.span.set_attributes(
247
+ attributes={"outputs": outputs},
248
+ namespace="data",
249
+ )
250
+
251
+ self.span.set_attributes(
252
+ attributes={"total": kwargs.get("response_cost")},
253
+ namespace="metrics.unit.costs",
254
+ )
255
+
256
+ self.span.set_attributes(
257
+ attributes=(
258
+ {
259
+ "prompt": response_obj.usage.prompt_tokens,
260
+ "completion": response_obj.usage.completion_tokens,
261
+ "total": response_obj.usage.total_tokens,
262
+ }
263
+ ),
264
+ namespace="metrics.unit.tokens",
265
+ )
266
+
267
+ self.span.set_status(status="OK")
268
+
269
+ self.span.end()
270
+
271
+ async def async_log_failure_event(
272
+ self,
273
+ kwargs,
274
+ response_obj,
275
+ start_time,
276
+ end_time,
277
+ ):
278
+ if not self.span:
279
+ log.error("LiteLLM callback error: span not found.")
280
+ return
281
+
282
+ self.span.record_exception(kwargs["exception"])
283
+
284
+ self.span.set_status(status="ERROR")
285
+
286
+ self.span.end()
287
+
288
+ return LitellmHandler()
@@ -0,0 +1,6 @@
1
+ from agenta.sdk.managers.config import ConfigManager
2
+ from agenta.sdk.managers.variant import VariantManager
3
+ from agenta.sdk.managers.deployment import DeploymentManager
4
+
5
+
6
+ __all__ = ["ConfigManager", "VariantManager", "DeploymentManager"]