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.

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.3.dist-info → agenta-0.25.3a1.dist-info}/METADATA +4 -1
  34. {agenta-0.25.3.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.3.dist-info → agenta-0.25.3a1.dist-info}/WHEEL +0 -0
  42. {agenta-0.25.3.dist-info → agenta-0.25.3a1.dist-info}/entry_points.txt +0 -0
@@ -1,111 +1,190 @@
1
- # Stdlib Imports
2
1
  import inspect
3
- import traceback
4
2
  from functools import wraps
5
- from typing import Any, Callable, Optional, List, Union
3
+ from itertools import chain
4
+ from typing import Callable, Optional, Any, Dict, List
6
5
 
7
- # Own Imports
8
6
  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
7
 
13
8
 
14
- logging.setLevel("DEBUG")
9
+ from agenta.sdk.utils.exceptions import suppress
15
10
 
11
+ from agenta.sdk.context.tracing import tracing_context
12
+ from agenta.sdk.tracing.conventions import parse_span_kind
16
13
 
17
- class instrument(BaseDecorator):
18
- """Decorator class for monitoring llm apps functions.
19
14
 
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": ...}
28
-
29
- @ag.instrument(spankind="llm")
30
- async def litellm_openai_call(prompt:str) -> str:
31
- return "do something"
32
-
33
- @ag.instrument(config=prompt_config) # spankind for parent span defaults to workflow
34
- async def generate(prompt: str):
35
- return ...
36
- ```
37
- """
15
+ class instrument:
16
+ DEFAULT_KEY = "__default__"
38
17
 
39
18
  def __init__(
40
19
  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,
20
+ kind: str = "task",
21
+ config: Optional[Dict[str, Any]] = None,
22
+ ignore_inputs: Optional[bool] = None,
23
+ ignore_outputs: Optional[bool] = None,
24
+ max_depth: Optional[int] = 2,
25
+ # DEPRECATED
26
+ spankind: Optional[str] = "TASK",
45
27
  ) -> None:
28
+ self.kind = spankind if spankind is not None else kind
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
35
  is_coroutine_function = inspect.iscoroutinefunction(func)
53
36
 
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)
37
+ def parse(*args, **kwargs) -> Dict[str, Any]:
38
+ inputs = {
39
+ key: value
40
+ for key, value in chain(
41
+ zip(inspect.getfullargspec(func).args, args),
42
+ kwargs.items(),
43
+ )
44
+ }
45
+
46
+ return inputs
58
47
 
59
- return input_dict
48
+ def redact(
49
+ io: Dict[str, Any], ignore: List[str] | bool = False
50
+ ) -> Dict[str, Any]:
51
+ """
52
+ Redact user-defined sensitive information from inputs and outputs as defined by the ignore list or boolean flag.
60
53
 
61
- def redact(io, blacklist):
62
- return {
63
- key: io[key]
64
- for key in io.keys()
54
+ Example:
55
+ - ignore = ["password"] -> {"username": "admin", "password": "********"} -> {"username": "admin"}
56
+ - ignore = True -> {"username": "admin", "password": "********"} -> {}
57
+ - ignore = False -> {"username": "admin", "password": "********"} -> {"username": "admin", "password": "********"}
58
+ """
59
+ io = {
60
+ key: value
61
+ for key, value in io.items()
65
62
  if key
66
63
  not in (
67
- blacklist
68
- if isinstance(blacklist, list)
69
- else []
70
- if blacklist is False
71
- else io.keys()
64
+ ignore
65
+ if isinstance(ignore, list)
66
+ else io.keys() if ignore is True else []
72
67
  )
73
68
  }
74
69
 
75
- def patch(result):
76
- TRACE_DEFAULT_KEY = "__default__"
70
+ return io
77
71
 
78
- outputs = result
72
+ def patch(result: Any) -> Dict[str, Any]:
73
+ """
74
+ Patch the result to ensure that it is a dictionary, with a default key when necessary.
79
75
 
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"])
76
+ Example:
77
+ - result = "Hello, World!" -> {"__default__": "Hello, World!"}
78
+ - result = {"message": "Hello, World!", "cost": 0.0, "usage": {}} -> {"__default__": "Hello, World!"}
79
+ - result = {"message": "Hello, World!"} -> {"message": "Hello, World!"}
80
+ """
81
+ outputs = (
82
+ {instrument.DEFAULT_KEY: result}
83
+ if not isinstance(result, dict)
84
+ else (
85
+ {instrument.DEFAULT_KEY: result["message"]}
86
+ if all(key in result for key in ["message", "cost", "usage"])
87
+ else result
88
+ )
89
+ )
94
90
 
95
91
  return outputs
96
92
 
97
93
  @wraps(func)
98
94
  async def async_wrapper(*args, **kwargs):
99
95
  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
- ):
106
- result = await func(*args, **kwargs)
96
+ if not ag.tracing.get_current_span().is_recording():
97
+ self.kind = "workflow"
98
+
99
+ kind = parse_span_kind(self.kind)
100
+
101
+ with ag.tracer.start_as_current_span(func.__name__, kind=kind):
102
+ span = ag.tracing.get_current_span()
103
+
104
+ with suppress():
105
+ span.set_attributes(
106
+ attributes={"node": self.kind},
107
+ namespace="type",
108
+ )
109
+
110
+ if span.parent is None:
111
+ rctx = tracing_context.get()
112
+
113
+ span.set_attributes(
114
+ attributes={"configuration": rctx.get("config", {})},
115
+ namespace="meta",
116
+ )
117
+ span.set_attributes(
118
+ attributes={"environment": rctx.get("environment", {})},
119
+ namespace="meta",
120
+ )
121
+ span.set_attributes(
122
+ attributes={"version": rctx.get("version", {})},
123
+ namespace="meta",
124
+ )
125
+ span.set_attributes(
126
+ attributes={"variant": rctx.get("variant", {})},
127
+ namespace="meta",
128
+ )
129
+
130
+ _inputs = redact(parse(*args, **kwargs), self.ignore_inputs)
131
+ span.set_attributes(
132
+ attributes={"inputs": _inputs},
133
+ namespace="data",
134
+ max_depth=self.max_depth,
135
+ )
136
+
137
+ try:
138
+ result = await func(*args, **kwargs)
139
+ except Exception as e:
140
+ span.record_exception(e)
141
+
142
+ span.set_status("ERROR")
143
+
144
+ raise e
107
145
 
108
- ag.tracing.store_outputs(redact(patch(result), self.ignore_outputs))
146
+ with suppress():
147
+ cost = None
148
+ usage = {}
149
+ if isinstance(result, dict):
150
+ cost = result.get("cost", None)
151
+ usage = result.get("usage", {})
152
+
153
+ span.set_attributes(
154
+ attributes={"total": cost},
155
+ namespace="metrics.unit.costs",
156
+ )
157
+ span.set_attributes(
158
+ attributes=(
159
+ {
160
+ "prompt": usage.get("prompt_tokens", None),
161
+ "completion": usage.get("completion_tokens", None),
162
+ "total": usage.get("total_tokens", None),
163
+ }
164
+ ),
165
+ namespace="metrics.unit.tokens",
166
+ )
167
+
168
+ _outputs = redact(patch(result), self.ignore_outputs)
169
+ span.set_attributes(
170
+ attributes={"outputs": _outputs},
171
+ namespace="data",
172
+ max_depth=self.max_depth,
173
+ )
174
+
175
+ span.set_status("OK")
176
+
177
+ with suppress():
178
+ if hasattr(span, "parent") and span.parent is None:
179
+ tracing_context.set(
180
+ tracing_context.get()
181
+ | {
182
+ "root": {
183
+ "trace_id": span.get_span_context().trace_id,
184
+ "span_id": span.get_span_context().span_id,
185
+ }
186
+ }
187
+ )
109
188
 
110
189
  return result
111
190
 
@@ -114,15 +193,98 @@ class instrument(BaseDecorator):
114
193
  @wraps(func)
115
194
  def sync_wrapper(*args, **kwargs):
116
195
  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
- ):
123
- result = func(*args, **kwargs)
124
-
125
- ag.tracing.store_outputs(redact(patch(result), self.ignore_outputs))
196
+ if not ag.tracing.get_current_span().is_recording():
197
+ self.kind = "workflow"
198
+
199
+ kind = parse_span_kind(self.kind)
200
+
201
+ with ag.tracer.start_as_current_span(func.__name__, kind=kind):
202
+ span = ag.tracing.get_current_span()
203
+
204
+ with suppress():
205
+ span.set_attributes(
206
+ attributes={"node": self.kind},
207
+ namespace="type",
208
+ )
209
+
210
+ if span.parent is None:
211
+ rctx = tracing_context.get()
212
+
213
+ span.set_attributes(
214
+ attributes={"configuration": rctx.get("config", {})},
215
+ namespace="meta",
216
+ )
217
+ span.set_attributes(
218
+ attributes={"environment": rctx.get("environment", {})},
219
+ namespace="meta",
220
+ )
221
+ span.set_attributes(
222
+ attributes={"version": rctx.get("version", {})},
223
+ namespace="meta",
224
+ )
225
+ span.set_attributes(
226
+ attributes={"variant": rctx.get("variant", {})},
227
+ namespace="meta",
228
+ )
229
+
230
+ _inputs = redact(parse(*args, **kwargs), self.ignore_inputs)
231
+ span.set_attributes(
232
+ attributes={"inputs": _inputs},
233
+ namespace="data",
234
+ max_depth=self.max_depth,
235
+ )
236
+
237
+ try:
238
+ result = func(*args, **kwargs)
239
+ except Exception as e:
240
+ span.record_exception(e)
241
+
242
+ span.set_status("ERROR")
243
+
244
+ raise e
245
+
246
+ with suppress():
247
+ cost = None
248
+ usage = {}
249
+ if isinstance(result, dict):
250
+ cost = result.get("cost", None)
251
+ usage = result.get("usage", {})
252
+
253
+ span.set_attributes(
254
+ attributes={"total": cost},
255
+ namespace="metrics.unit.costs",
256
+ )
257
+ span.set_attributes(
258
+ attributes=(
259
+ {
260
+ "prompt": usage.get("prompt_tokens", None),
261
+ "completion": usage.get("completion_tokens", None),
262
+ "total": usage.get("total_tokens", None),
263
+ }
264
+ ),
265
+ namespace="metrics.unit.tokens",
266
+ )
267
+
268
+ _outputs = redact(patch(result), self.ignore_outputs)
269
+ span.set_attributes(
270
+ attributes={"outputs": _outputs},
271
+ namespace="data",
272
+ max_depth=self.max_depth,
273
+ )
274
+
275
+ span.set_status("OK")
276
+
277
+ with suppress():
278
+ if hasattr(span, "parent") and span.parent is None:
279
+ tracing_context.set(
280
+ tracing_context.get()
281
+ | {
282
+ "root": {
283
+ "trace_id": span.get_span_context().trace_id,
284
+ "span_id": span.get_span_context().span_id,
285
+ }
286
+ }
287
+ )
126
288
 
127
289
  return result
128
290
 
@@ -0,0 +1 @@
1
+ from .litellm import litellm_handler
@@ -0,0 +1,275 @@
1
+ import agenta as ag
2
+
3
+ from agenta.sdk.tracing.spans import CustomSpan
4
+ from agenta.sdk.utils.exceptions import suppress
5
+ from agenta.sdk.utils.logging import log
6
+
7
+
8
+ def litellm_handler():
9
+ try:
10
+ from litellm.utils import ModelResponse
11
+ from litellm.integrations.custom_logger import (
12
+ CustomLogger as LitellmCustomLogger,
13
+ )
14
+ except ImportError as exc:
15
+ raise ImportError(
16
+ "The litellm SDK is not installed. Please install it using `pip install litellm`."
17
+ ) from exc
18
+ except Exception as exc:
19
+ raise Exception(
20
+ "Unexpected error occurred when importing litellm: {}".format(exc)
21
+ ) from exc
22
+
23
+ class LitellmHandler(LitellmCustomLogger):
24
+ """This handler is responsible for instrumenting certain events when using litellm to call LLMs.
25
+
26
+ Args:
27
+ LitellmCustomLogger (object): custom logger that allows us to override the events to capture.
28
+ """
29
+
30
+ def __init__(self):
31
+ self.span = None
32
+
33
+ def log_pre_api_call(
34
+ self,
35
+ model,
36
+ messages,
37
+ kwargs,
38
+ ):
39
+ type = (
40
+ "chat"
41
+ if kwargs.get("call_type") in ["completion", "acompletion"]
42
+ else "embedding"
43
+ )
44
+
45
+ kind = "CLIENT"
46
+
47
+ self.span = CustomSpan(
48
+ ag.tracer.start_span(name=f"litellm_{kind.lower()}", kind=kind)
49
+ )
50
+
51
+ self.span.set_attributes(
52
+ attributes={"node": type},
53
+ namespace="type",
54
+ )
55
+
56
+ if not self.span:
57
+ log.error("LiteLLM callback error: span not found.")
58
+ return
59
+
60
+ log.info(f"log_pre_api_call({hex(self.span.context.span_id)[2:]})")
61
+
62
+ self.span.set_attributes(
63
+ attributes={"inputs": {"messages": kwargs["messages"]}},
64
+ namespace="data",
65
+ )
66
+
67
+ self.span.set_attributes(
68
+ attributes={
69
+ "configuration": {
70
+ "model": kwargs.get("model"),
71
+ **kwargs.get("optional_params"),
72
+ }
73
+ },
74
+ namespace="meta",
75
+ )
76
+
77
+ def log_stream_event(
78
+ self,
79
+ kwargs,
80
+ response_obj,
81
+ start_time,
82
+ end_time,
83
+ ):
84
+ if not self.span:
85
+ log.error("LiteLLM callback error: span not found.")
86
+ return
87
+
88
+ # log.info(f"log_stream({hex(self.span.context.span_id)[2:]})")
89
+
90
+ self.span.set_attributes(
91
+ attributes={
92
+ "output": {"__default__": kwargs.get("complete_streaming_response")}
93
+ },
94
+ namespace="data",
95
+ )
96
+
97
+ self.span.set_attributes(
98
+ attributes={"total": kwargs.get("response_cost")},
99
+ namespace="metrics.unit.costs",
100
+ )
101
+
102
+ self.span.set_attributes(
103
+ attributes=(
104
+ {
105
+ "prompt": response_obj.usage.prompt_tokens,
106
+ "completion": response_obj.usage.completion_tokens,
107
+ "total": response_obj.usage.total_tokens,
108
+ }
109
+ ),
110
+ namespace="metrics.unit.tokens",
111
+ )
112
+
113
+ self.span.set_status(status="OK")
114
+
115
+ self.span.end()
116
+
117
+ def log_success_event(
118
+ self,
119
+ kwargs,
120
+ response_obj,
121
+ start_time,
122
+ end_time,
123
+ ):
124
+ if not self.span:
125
+ log.error("LiteLLM callback error: span not found.")
126
+ return
127
+
128
+ # log.info(f"log_success({hex(self.span.context.span_id)[2:]})")
129
+
130
+ self.span.set_attributes(
131
+ attributes={
132
+ "output": {"__default__": response_obj.choices[0].message.content}
133
+ },
134
+ namespace="data",
135
+ )
136
+
137
+ self.span.set_attributes(
138
+ attributes={"total": kwargs.get("response_cost")},
139
+ namespace="metrics.unit.costs",
140
+ )
141
+
142
+ self.span.set_attributes(
143
+ attributes=(
144
+ {
145
+ "prompt": response_obj.usage.prompt_tokens,
146
+ "completion": response_obj.usage.completion_tokens,
147
+ "total": response_obj.usage.total_tokens,
148
+ }
149
+ ),
150
+ namespace="metrics.unit.tokens",
151
+ )
152
+
153
+ self.span.set_status(status="OK")
154
+
155
+ self.span.end()
156
+
157
+ def log_failure_event(
158
+ self,
159
+ kwargs,
160
+ response_obj,
161
+ start_time,
162
+ end_time,
163
+ ):
164
+ if not self.span:
165
+ log.error("LiteLLM callback error: span not found.")
166
+ return
167
+
168
+ # log.info(f"log_failure({hex(self.span.context.span_id)[2:]})")
169
+
170
+ self.span.record_exception(kwargs["exception"])
171
+
172
+ self.span.set_status(status="ERROR")
173
+
174
+ self.span.end()
175
+
176
+ async def async_log_stream_event(
177
+ self,
178
+ kwargs,
179
+ response_obj,
180
+ start_time,
181
+ end_time,
182
+ ):
183
+ if not self.span:
184
+ log.error("LiteLLM callback error: span not found.")
185
+ return
186
+
187
+ # log.info(f"async_log_stream({hex(self.span.context.span_id)[2:]})")
188
+
189
+ self.span.set_attributes(
190
+ attributes={
191
+ "output": {"__default__": kwargs.get("complete_streaming_response")}
192
+ },
193
+ namespace="data",
194
+ )
195
+
196
+ self.span.set_attributes(
197
+ attributes={"total": kwargs.get("response_cost")},
198
+ namespace="metrics.unit.costs",
199
+ )
200
+
201
+ self.span.set_attributes(
202
+ attributes=(
203
+ {
204
+ "prompt": response_obj.usage.prompt_tokens,
205
+ "completion": response_obj.usage.completion_tokens,
206
+ "total": response_obj.usage.total_tokens,
207
+ }
208
+ ),
209
+ namespace="metrics.unit.tokens",
210
+ )
211
+
212
+ self.span.set_status(status="OK")
213
+
214
+ self.span.end()
215
+
216
+ async def async_log_success_event(
217
+ self,
218
+ kwargs,
219
+ response_obj,
220
+ start_time,
221
+ end_time,
222
+ ):
223
+ if not self.span:
224
+ log.error("LiteLLM callback error: span not found.")
225
+ return
226
+
227
+ log.info(f"async_log_success({hex(self.span.context.span_id)[2:]})")
228
+
229
+ self.span.set_attributes(
230
+ attributes={
231
+ "output": {"__default__": kwargs.get("complete_streaming_response")}
232
+ },
233
+ namespace="data",
234
+ )
235
+
236
+ self.span.set_attributes(
237
+ attributes={"total": kwargs.get("response_cost")},
238
+ namespace="metrics.unit.costs",
239
+ )
240
+
241
+ self.span.set_attributes(
242
+ attributes=(
243
+ {
244
+ "prompt": response_obj.usage.prompt_tokens,
245
+ "completion": response_obj.usage.completion_tokens,
246
+ "total": response_obj.usage.total_tokens,
247
+ }
248
+ ),
249
+ namespace="metrics.unit.tokens",
250
+ )
251
+
252
+ self.span.set_status(status="OK")
253
+
254
+ self.span.end()
255
+
256
+ async def async_log_failure_event(
257
+ self,
258
+ kwargs,
259
+ response_obj,
260
+ start_time,
261
+ end_time,
262
+ ):
263
+ if not self.span:
264
+ log.error("LiteLLM callback error: span not found.")
265
+ return
266
+
267
+ # log.info(f"async_log_failure({hex(self.span.context.span_id)[2:]})")
268
+
269
+ self.span.record_exception(kwargs["exception"])
270
+
271
+ self.span.set_status(status="ERROR")
272
+
273
+ self.span.end()
274
+
275
+ return LitellmHandler()
agenta/sdk/router.py CHANGED
@@ -1,15 +1,8 @@
1
1
  from fastapi import APIRouter
2
- from .context import get_contexts
3
2
 
4
3
  router = APIRouter()
5
4
 
6
5
 
7
- @router.get("/contexts/")
8
- def get_all_contexts():
9
- contexts = get_contexts()
10
- return {"contexts": contexts}
11
-
12
-
13
6
  @router.get("/health")
14
7
  def health():
15
8
  return {"status": "ok"}
@@ -0,0 +1 @@
1
+ from .tracing import Tracing, get_tracer