agenta 0.25.4__py3-none-any.whl → 0.25.4a1__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} +138 -124
  13. agenta/sdk/decorators/tracing.py +247 -79
  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 +1269 -0
  23. agenta/sdk/tracing/processors.py +65 -0
  24. agenta/sdk/tracing/spans.py +124 -0
  25. agenta/sdk/tracing/tracing.py +174 -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 +19 -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.4.dist-info → agenta-0.25.4a1.dist-info}/METADATA +4 -2
  34. {agenta-0.25.4.dist-info → agenta-0.25.4a1.dist-info}/RECORD +36 -26
  35. {agenta-0.25.4.dist-info → agenta-0.25.4a1.dist-info}/WHEEL +1 -1
  36. agenta/sdk/context.py +0 -41
  37. agenta/sdk/decorators/base.py +0 -10
  38. agenta/sdk/tracing/callbacks.py +0 -187
  39. agenta/sdk/tracing/llm_tracing.py +0 -617
  40. agenta/sdk/tracing/tasks_manager.py +0 -129
  41. agenta/sdk/tracing/tracing_context.py +0 -27
  42. {agenta-0.25.4.dist-info → agenta-0.25.4a1.dist-info}/entry_points.txt +0 -0
@@ -1,111 +1,196 @@
1
- # Stdlib Imports
2
1
  import inspect
3
2
  import traceback
4
3
  from functools import wraps
5
- from typing import Any, Callable, Optional, List, Union
4
+ from itertools import chain
5
+ from typing import Callable, Optional, Any, Dict, List
6
6
 
7
- # Own Imports
8
7
  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
8
 
13
9
 
14
- logging.setLevel("DEBUG")
10
+ from agenta.sdk.utils.exceptions import suppress
15
11
 
12
+ from agenta.sdk.context.tracing import tracing_context
13
+ from agenta.sdk.tracing.conventions import parse_span_kind
16
14
 
17
- class instrument(BaseDecorator):
18
- """Decorator class for monitoring llm apps functions.
19
15
 
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
- """
16
+ class instrument:
17
+ DEFAULT_KEY = "__default__"
38
18
 
39
19
  def __init__(
40
20
  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,
21
+ kind: str = "task",
22
+ config: Optional[Dict[str, Any]] = None,
23
+ ignore_inputs: Optional[bool] = None,
24
+ ignore_outputs: Optional[bool] = None,
25
+ max_depth: Optional[int] = 2,
26
+ # DEPRECATED
27
+ spankind: Optional[str] = "TASK",
45
28
  ) -> None:
29
+ self.kind = spankind if spankind is not None else kind
46
30
  self.config = config
47
- self.spankind = spankind
48
31
  self.ignore_inputs = ignore_inputs
49
32
  self.ignore_outputs = ignore_outputs
33
+ self.max_depth = max_depth
50
34
 
51
35
  def __call__(self, func: Callable[..., Any]):
52
36
  is_coroutine_function = inspect.iscoroutinefunction(func)
53
37
 
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)
38
+ def parse(*args, **kwargs) -> Dict[str, Any]:
39
+ inputs = {
40
+ key: value
41
+ for key, value in chain(
42
+ zip(inspect.getfullargspec(func).args, args),
43
+ kwargs.items(),
44
+ )
45
+ }
46
+
47
+ return inputs
58
48
 
59
- return input_dict
49
+ def redact(
50
+ io: Dict[str, Any], ignore: List[str] | bool = False
51
+ ) -> Dict[str, Any]:
52
+ """
53
+ Redact user-defined sensitive information from inputs and outputs as defined by the ignore list or boolean flag.
60
54
 
61
- def redact(io, blacklist):
62
- return {
63
- key: io[key]
64
- for key in io.keys()
55
+ Example:
56
+ - ignore = ["password"] -> {"username": "admin", "password": "********"} -> {"username": "admin"}
57
+ - ignore = True -> {"username": "admin", "password": "********"} -> {}
58
+ - ignore = False -> {"username": "admin", "password": "********"} -> {"username": "admin", "password": "********"}
59
+ """
60
+ io = {
61
+ key: value
62
+ for key, value in io.items()
65
63
  if key
66
64
  not in (
67
- blacklist
68
- if isinstance(blacklist, list)
69
- else []
70
- if blacklist is False
65
+ ignore
66
+ if isinstance(ignore, list)
71
67
  else io.keys()
68
+ if ignore is True
69
+ else []
72
70
  )
73
71
  }
74
72
 
75
- def patch(result):
76
- TRACE_DEFAULT_KEY = "__default__"
77
-
78
- outputs = result
73
+ return io
79
74
 
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"]}
75
+ def patch(result: Any) -> Dict[str, Any]:
76
+ """
77
+ Patch the result to ensure that it is a dictionary, with a default key when necessary.
91
78
 
92
- ag.tracing.store_cost(result["cost"])
93
- ag.tracing.store_usage(result["usage"])
79
+ Example:
80
+ - result = "Hello, World!" -> {"__default__": "Hello, World!"}
81
+ - result = {"message": "Hello, World!", "cost": 0.0, "usage": {}} -> {"__default__": "Hello, World!"}
82
+ - result = {"message": "Hello, World!"} -> {"message": "Hello, World!"}
83
+ """
84
+ outputs = (
85
+ {instrument.DEFAULT_KEY: result}
86
+ if not isinstance(result, dict)
87
+ else (
88
+ {instrument.DEFAULT_KEY: result["message"]}
89
+ if all(key in result for key in ["message", "cost", "usage"])
90
+ else result
91
+ )
92
+ )
94
93
 
95
94
  return outputs
96
95
 
97
96
  @wraps(func)
98
97
  async def async_wrapper(*args, **kwargs):
99
98
  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)
99
+ if not ag.tracing.get_current_span().is_recording():
100
+ self.kind = "workflow"
101
+
102
+ kind = parse_span_kind(self.kind)
103
+
104
+ with ag.tracer.start_as_current_span(func.__name__, kind=kind):
105
+ span = ag.tracing.get_current_span()
106
+
107
+ with suppress():
108
+ span.set_attributes(
109
+ attributes={"node": self.kind},
110
+ namespace="type",
111
+ )
112
+
113
+ if span.parent is None:
114
+ rctx = tracing_context.get()
115
+
116
+ span.set_attributes(
117
+ attributes={"configuration": rctx.get("config", {})},
118
+ namespace="meta",
119
+ )
120
+ span.set_attributes(
121
+ attributes={"environment": rctx.get("environment", {})},
122
+ namespace="meta",
123
+ )
124
+ span.set_attributes(
125
+ attributes={"version": rctx.get("version", {})},
126
+ namespace="meta",
127
+ )
128
+ span.set_attributes(
129
+ attributes={"variant": rctx.get("variant", {})},
130
+ namespace="meta",
131
+ )
132
+
133
+ _inputs = redact(parse(*args, **kwargs), self.ignore_inputs)
134
+ span.set_attributes(
135
+ attributes={"inputs": _inputs},
136
+ namespace="data",
137
+ max_depth=self.max_depth,
138
+ )
139
+
140
+ try:
141
+ result = await func(*args, **kwargs)
142
+ except Exception as e:
143
+ traceback.print_exc()
144
+
145
+ span.record_exception(e)
146
+
147
+ span.set_status("ERROR")
107
148
 
108
- ag.tracing.store_outputs(redact(patch(result), self.ignore_outputs))
149
+ raise e
150
+
151
+ with suppress():
152
+ cost = None
153
+ usage = {}
154
+
155
+ if isinstance(result, dict):
156
+ cost = result.get("cost", None)
157
+ usage = result.get("usage", {})
158
+
159
+ span.set_attributes(
160
+ attributes={"total": cost},
161
+ namespace="metrics.unit.costs",
162
+ )
163
+ span.set_attributes(
164
+ attributes=(
165
+ {
166
+ "prompt": usage.get("prompt_tokens", None),
167
+ "completion": usage.get("completion_tokens", None),
168
+ "total": usage.get("total_tokens", None),
169
+ }
170
+ ),
171
+ namespace="metrics.unit.tokens",
172
+ )
173
+
174
+ _outputs = redact(patch(result), self.ignore_outputs)
175
+ span.set_attributes(
176
+ attributes={"outputs": _outputs},
177
+ namespace="data",
178
+ max_depth=self.max_depth,
179
+ )
180
+
181
+ span.set_status("OK")
182
+
183
+ with suppress():
184
+ if hasattr(span, "parent") and span.parent is None:
185
+ tracing_context.set(
186
+ tracing_context.get()
187
+ | {
188
+ "root": {
189
+ "trace_id": span.get_span_context().trace_id,
190
+ "span_id": span.get_span_context().span_id,
191
+ }
192
+ }
193
+ )
109
194
 
110
195
  return result
111
196
 
@@ -114,15 +199,98 @@ class instrument(BaseDecorator):
114
199
  @wraps(func)
115
200
  def sync_wrapper(*args, **kwargs):
116
201
  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))
202
+ if not ag.tracing.get_current_span().is_recording():
203
+ self.kind = "workflow"
204
+
205
+ kind = parse_span_kind(self.kind)
206
+
207
+ with ag.tracer.start_as_current_span(func.__name__, kind=kind):
208
+ span = ag.tracing.get_current_span()
209
+
210
+ with suppress():
211
+ span.set_attributes(
212
+ attributes={"node": self.kind},
213
+ namespace="type",
214
+ )
215
+
216
+ if span.parent is None:
217
+ rctx = tracing_context.get()
218
+
219
+ span.set_attributes(
220
+ attributes={"configuration": rctx.get("config", {})},
221
+ namespace="meta",
222
+ )
223
+ span.set_attributes(
224
+ attributes={"environment": rctx.get("environment", {})},
225
+ namespace="meta",
226
+ )
227
+ span.set_attributes(
228
+ attributes={"version": rctx.get("version", {})},
229
+ namespace="meta",
230
+ )
231
+ span.set_attributes(
232
+ attributes={"variant": rctx.get("variant", {})},
233
+ namespace="meta",
234
+ )
235
+
236
+ _inputs = redact(parse(*args, **kwargs), self.ignore_inputs)
237
+ span.set_attributes(
238
+ attributes={"inputs": _inputs},
239
+ namespace="data",
240
+ max_depth=self.max_depth,
241
+ )
242
+
243
+ try:
244
+ result = func(*args, **kwargs)
245
+ except Exception as e:
246
+ span.record_exception(e)
247
+
248
+ span.set_status("ERROR")
249
+
250
+ raise e
251
+
252
+ with suppress():
253
+ cost = None
254
+ usage = {}
255
+ if isinstance(result, dict):
256
+ cost = result.get("cost", None)
257
+ usage = result.get("usage", {})
258
+
259
+ span.set_attributes(
260
+ attributes={"total": cost},
261
+ namespace="metrics.unit.costs",
262
+ )
263
+ span.set_attributes(
264
+ attributes=(
265
+ {
266
+ "prompt": usage.get("prompt_tokens", None),
267
+ "completion": usage.get("completion_tokens", None),
268
+ "total": usage.get("total_tokens", None),
269
+ }
270
+ ),
271
+ namespace="metrics.unit.tokens",
272
+ )
273
+
274
+ _outputs = redact(patch(result), self.ignore_outputs)
275
+ span.set_attributes(
276
+ attributes={"outputs": _outputs},
277
+ namespace="data",
278
+ max_depth=self.max_depth,
279
+ )
280
+
281
+ span.set_status("OK")
282
+
283
+ with suppress():
284
+ if hasattr(span, "parent") and span.parent is None:
285
+ tracing_context.set(
286
+ tracing_context.get()
287
+ | {
288
+ "root": {
289
+ "trace_id": span.get_span_context().trace_id,
290
+ "span_id": span.get_span_context().span_id,
291
+ }
292
+ }
293
+ )
126
294
 
127
295
  return result
128
296
 
@@ -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