agenta 0.25.3a1__py3-none-any.whl → 0.25.4__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 +7 -6
  2. agenta/client/backend/client.py +14 -22
  3. agenta/client/backend/core/http_client.py +15 -23
  4. agenta/client/backend/core/pydantic_utilities.py +2 -2
  5. agenta/sdk/__init__.py +6 -27
  6. agenta/sdk/agenta_init.py +26 -73
  7. agenta/sdk/config_manager.py +2 -2
  8. agenta/sdk/context.py +41 -0
  9. agenta/sdk/decorators/base.py +10 -0
  10. agenta/sdk/decorators/{routing.py → llm_entrypoint.py} +125 -136
  11. agenta/sdk/decorators/tracing.py +81 -243
  12. agenta/sdk/router.py +7 -0
  13. agenta/sdk/tracing/__init__.py +0 -1
  14. agenta/sdk/tracing/callbacks.py +187 -0
  15. agenta/sdk/tracing/llm_tracing.py +617 -0
  16. agenta/sdk/{utils/logging.py → tracing/logger.py} +5 -3
  17. agenta/sdk/tracing/tasks_manager.py +129 -0
  18. agenta/sdk/tracing/tracing_context.py +27 -0
  19. agenta/sdk/types.py +12 -0
  20. agenta/sdk/utils/debug.py +5 -5
  21. agenta/sdk/utils/globals.py +5 -3
  22. agenta/sdk/utils/{costs.py → helper/openai_cost.py} +0 -3
  23. {agenta-0.25.3a1.dist-info → agenta-0.25.4.dist-info}/METADATA +2 -4
  24. {agenta-0.25.3a1.dist-info → agenta-0.25.4.dist-info}/RECORD +26 -36
  25. {agenta-0.25.3a1.dist-info → agenta-0.25.4.dist-info}/WHEEL +1 -1
  26. agenta/sdk/context/__init__.py +0 -0
  27. agenta/sdk/context/routing.py +0 -25
  28. agenta/sdk/context/tracing.py +0 -3
  29. agenta/sdk/decorators/__init__.py +0 -0
  30. agenta/sdk/litellm/__init__.py +0 -1
  31. agenta/sdk/litellm/litellm.py +0 -275
  32. agenta/sdk/tracing/attributes.py +0 -181
  33. agenta/sdk/tracing/context.py +0 -21
  34. agenta/sdk/tracing/conventions.py +0 -43
  35. agenta/sdk/tracing/exporters.py +0 -53
  36. agenta/sdk/tracing/inline.py +0 -1230
  37. agenta/sdk/tracing/processors.py +0 -65
  38. agenta/sdk/tracing/spans.py +0 -124
  39. agenta/sdk/tracing/tracing.py +0 -171
  40. agenta/sdk/utils/exceptions.py +0 -18
  41. agenta/sdk/utils/singleton.py +0 -13
  42. {agenta-0.25.3a1.dist-info → agenta-0.25.4.dist-info}/entry_points.txt +0 -0
@@ -1,190 +1,111 @@
1
+ # Stdlib Imports
1
2
  import inspect
3
+ import traceback
2
4
  from functools import wraps
3
- from itertools import chain
4
- from typing import Callable, Optional, Any, Dict, List
5
+ from typing import Any, Callable, Optional, List, Union
5
6
 
7
+ # Own Imports
6
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
7
12
 
8
13
 
9
- from agenta.sdk.utils.exceptions import suppress
14
+ logging.setLevel("DEBUG")
10
15
 
11
- from agenta.sdk.context.tracing import tracing_context
12
- from agenta.sdk.tracing.conventions import parse_span_kind
13
16
 
17
+ class instrument(BaseDecorator):
18
+ """Decorator class for monitoring llm apps functions.
14
19
 
15
- class instrument:
16
- DEFAULT_KEY = "__default__"
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
+ """
17
38
 
18
39
  def __init__(
19
40
  self,
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",
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,
27
45
  ) -> None:
28
- self.kind = spankind if spankind is not None else kind
29
46
  self.config = config
47
+ self.spankind = spankind
30
48
  self.ignore_inputs = ignore_inputs
31
49
  self.ignore_outputs = ignore_outputs
32
- self.max_depth = max_depth
33
50
 
34
51
  def __call__(self, func: Callable[..., Any]):
35
52
  is_coroutine_function = inspect.iscoroutinefunction(func)
36
53
 
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
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)
47
58
 
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.
59
+ return input_dict
53
60
 
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()
61
+ def redact(io, blacklist):
62
+ return {
63
+ key: io[key]
64
+ for key in io.keys()
62
65
  if key
63
66
  not in (
64
- ignore
65
- if isinstance(ignore, list)
66
- else io.keys() if ignore is True else []
67
+ blacklist
68
+ if isinstance(blacklist, list)
69
+ else []
70
+ if blacklist is False
71
+ else io.keys()
67
72
  )
68
73
  }
69
74
 
70
- return io
75
+ def patch(result):
76
+ TRACE_DEFAULT_KEY = "__default__"
71
77
 
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.
78
+ outputs = result
75
79
 
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
- )
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"])
90
94
 
91
95
  return outputs
92
96
 
93
97
  @wraps(func)
94
98
  async def async_wrapper(*args, **kwargs):
95
99
  async def wrapped_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
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)
145
107
 
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
- )
108
+ ag.tracing.store_outputs(redact(patch(result), self.ignore_outputs))
188
109
 
189
110
  return result
190
111
 
@@ -193,98 +114,15 @@ class instrument:
193
114
  @wraps(func)
194
115
  def sync_wrapper(*args, **kwargs):
195
116
  def wrapped_func(*args, **kwargs):
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
- )
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))
288
126
 
289
127
  return result
290
128
 
agenta/sdk/router.py CHANGED
@@ -1,8 +1,15 @@
1
1
  from fastapi import APIRouter
2
+ from .context import get_contexts
2
3
 
3
4
  router = APIRouter()
4
5
 
5
6
 
7
+ @router.get("/contexts/")
8
+ def get_all_contexts():
9
+ contexts = get_contexts()
10
+ return {"contexts": contexts}
11
+
12
+
6
13
  @router.get("/health")
7
14
  def health():
8
15
  return {"status": "ok"}
@@ -1 +0,0 @@
1
- from .tracing import Tracing, get_tracer
@@ -0,0 +1,187 @@
1
+ import agenta as ag
2
+
3
+ from agenta.sdk.tracing.tracing_context import tracing_context, TracingContext
4
+ from agenta.sdk.tracing.logger import llm_logger as logging
5
+
6
+ from agenta.sdk.utils.debug import debug
7
+
8
+ TRACE_DEFAULT_KEY = "__default__"
9
+
10
+
11
+ def litellm_handler():
12
+ try:
13
+ from litellm.utils import ModelResponse
14
+ from litellm.integrations.custom_logger import (
15
+ CustomLogger as LitellmCustomLogger,
16
+ )
17
+ except ImportError as exc:
18
+ raise ImportError(
19
+ "The litellm SDK is not installed. Please install it using `pip install litellm`."
20
+ ) from exc
21
+ except Exception as exc:
22
+ raise Exception(
23
+ "Unexpected error occurred when importing litellm: {}".format(exc)
24
+ ) from exc
25
+
26
+ class LitellmHandler(LitellmCustomLogger):
27
+ """This handler is responsible for instrumenting certain events when using litellm to call LLMs.
28
+
29
+ Args:
30
+ LitellmCustomLogger (object): custom logger that allows us to override the events to capture.
31
+ """
32
+
33
+ def __init__(self):
34
+ self.span = None
35
+
36
+ @property
37
+ def _trace(self):
38
+ return ag.tracing
39
+
40
+ @debug()
41
+ def log_pre_api_call(self, model, messages, kwargs):
42
+ call_type = kwargs.get("call_type")
43
+ span_kind = (
44
+ "llm" if call_type in ["completion", "acompletion"] else "embedding"
45
+ )
46
+
47
+ self.span = ag.tracing.open_span(
48
+ name=f"{span_kind}_call",
49
+ input={"messages": kwargs["messages"]},
50
+ spankind=span_kind,
51
+ active=False,
52
+ )
53
+ logging.info(f"log_pre_api_call({self.span.id})")
54
+ ag.tracing.set_attributes(
55
+ {
56
+ "model_config": {
57
+ "model": kwargs.get("model"),
58
+ **kwargs.get(
59
+ "optional_params"
60
+ ), # model-specific params passed in
61
+ },
62
+ }
63
+ )
64
+
65
+ @debug()
66
+ def log_stream_event(self, kwargs, response_obj, start_time, end_time):
67
+ logging.info(f"log_stream_event({self.span.id})")
68
+ ag.tracing.set_status(status="OK", span_id=self.span.id)
69
+ ag.tracing.store_cost(kwargs.get("response_cost"), span_id=self.span.id)
70
+ ag.tracing.store_usage(
71
+ response_obj.usage.dict() if hasattr(response_obj, "usage") else None,
72
+ span_id=self.span.id,
73
+ )
74
+ ag.tracing.store_outputs(
75
+ # the complete streamed response (only set if `completion(..stream=True)`
76
+ outputs={TRACE_DEFAULT_KEY: kwargs.get("complete_streaming_response")},
77
+ span_id=self.span.id,
78
+ )
79
+ ag.tracing.close_span(span_id=self.span.id)
80
+
81
+ @debug()
82
+ def log_success_event(self, kwargs, response_obj, start_time, end_time):
83
+ logging.info(f"log_success_event({self.span.id})")
84
+ ag.tracing.set_status(status="OK", span_id=self.span.id)
85
+ ag.tracing.store_cost(kwargs.get("response_cost"), span_id=self.span.id)
86
+ ag.tracing.store_usage(
87
+ response_obj.usage.dict() if hasattr(response_obj, "usage") else None,
88
+ span_id=self.span.id,
89
+ )
90
+ ag.tracing.store_outputs(
91
+ outputs={TRACE_DEFAULT_KEY: response_obj.choices[0].message.content},
92
+ span_id=self.span.id,
93
+ )
94
+ ag.tracing.close_span(span_id=self.span.id)
95
+
96
+ @debug()
97
+ def log_failure_event(self, kwargs, response_obj, start_time, end_time):
98
+ logging.info("log_failure_event()", self.span)
99
+ ag.tracing.set_status(status="ERROR", span_id=self.span.id)
100
+ ag.tracing.set_attributes(
101
+ {
102
+ "traceback_exception": repr(
103
+ kwargs["traceback_exception"]
104
+ ), # the traceback generated via `traceback.format_exc()`
105
+ "call_end_time": kwargs[
106
+ "end_time"
107
+ ], # datetime object of when call was completed
108
+ },
109
+ span_id=self.span.id,
110
+ )
111
+ ag.tracing.store_cost(kwargs.get("response_cost"), span_id=self.span.id)
112
+ ag.tracing.store_usage(
113
+ response_obj.usage.dict() if hasattr(response_obj, "usage") else None,
114
+ span_id=self.span.id,
115
+ )
116
+ ag.tracing.store_outputs(
117
+ # the Exception raised
118
+ outputs={TRACE_DEFAULT_KEY: repr(kwargs["exception"])},
119
+ span_id=self.span.id,
120
+ )
121
+ ag.tracing.close_span(span_id=self.span.id)
122
+
123
+ @debug()
124
+ async def async_log_stream_event(
125
+ self, kwargs, response_obj, start_time, end_time
126
+ ):
127
+ logging.info(f"async_log_stream_event({self.span.id})")
128
+ ag.tracing.set_status(status="OK", span_id=self.span.id)
129
+ ag.tracing.store_cost(kwargs.get("response_cost"), span_id=self.span.id)
130
+ ag.tracing.store_usage(
131
+ response_obj.usage.dict() if hasattr(response_obj, "usage") else None,
132
+ span_id=self.span.id,
133
+ )
134
+ ag.tracing.store_outputs(
135
+ # the complete streamed response (only set if `completion(..stream=True)`)
136
+ outputs={TRACE_DEFAULT_KEY: kwargs.get("complete_streaming_response")},
137
+ span_id=self.span.id,
138
+ )
139
+ ag.tracing.close_span(span_id=self.span.id)
140
+
141
+ @debug()
142
+ async def async_log_success_event(
143
+ self, kwargs, response_obj, start_time, end_time
144
+ ):
145
+ logging.info(f"async_log_success_event({self.span.id})")
146
+ ag.tracing.set_status(status="OK", span_id=self.span.id)
147
+ ag.tracing.store_cost(kwargs.get("response_cost"), span_id=self.span.id)
148
+ ag.tracing.store_usage(
149
+ response_obj.usage.dict() if hasattr(response_obj, "usage") else None,
150
+ span_id=self.span.id,
151
+ )
152
+ ag.tracing.store_outputs(
153
+ outputs={TRACE_DEFAULT_KEY: response_obj.choices[0].message.content},
154
+ span_id=self.span.id,
155
+ )
156
+ ag.tracing.close_span(span_id=self.span.id)
157
+
158
+ @debug()
159
+ async def async_log_failure_event(
160
+ self, kwargs, response_obj, start_time, end_time
161
+ ):
162
+ logging.info(f"async_log_failure_event({self.span.id})")
163
+ ag.tracing.set_status(status="ERROR", span_id=self.span.id)
164
+ ag.tracing.set_attributes(
165
+ {
166
+ "traceback_exception": kwargs[
167
+ "traceback_exception"
168
+ ], # the traceback generated via `traceback.format_exc()`
169
+ "call_end_time": kwargs[
170
+ "end_time"
171
+ ], # datetime object of when call was completed
172
+ },
173
+ span_id=self.span.id,
174
+ )
175
+ ag.tracing.store_cost(kwargs.get("response_cost"), span_id=self.span.id)
176
+ ag.tracing.store_usage(
177
+ response_obj.usage.dict() if hasattr(response_obj, "usage") else None,
178
+ span_id=self.span.id,
179
+ )
180
+ ag.tracing.store_outputs(
181
+ # the Exception raised
182
+ outputs={TRACE_DEFAULT_KEY: repr(kwargs["exception"])},
183
+ span_id=self.span.id,
184
+ )
185
+ ag.tracing.close_span(span_id=self.span.id)
186
+
187
+ return LitellmHandler()