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