agenta 0.25.4a3__py3-none-any.whl → 0.25.4a4__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 +0 -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} +124 -137
  11. agenta/sdk/decorators/tracing.py +79 -247
  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.4a3.dist-info → agenta-0.25.4a4.dist-info}/METADATA +1 -5
  24. {agenta-0.25.4a3.dist-info → agenta-0.25.4a4.dist-info}/RECORD +26 -36
  25. agenta/sdk/context/__init__.py +0 -0
  26. agenta/sdk/context/routing.py +0 -25
  27. agenta/sdk/context/tracing.py +0 -3
  28. agenta/sdk/decorators/__init__.py +0 -0
  29. agenta/sdk/litellm/__init__.py +0 -1
  30. agenta/sdk/litellm/litellm.py +0 -277
  31. agenta/sdk/tracing/attributes.py +0 -181
  32. agenta/sdk/tracing/context.py +0 -21
  33. agenta/sdk/tracing/conventions.py +0 -43
  34. agenta/sdk/tracing/exporters.py +0 -53
  35. agenta/sdk/tracing/inline.py +0 -1305
  36. agenta/sdk/tracing/processors.py +0 -65
  37. agenta/sdk/tracing/spans.py +0 -124
  38. agenta/sdk/tracing/tracing.py +0 -174
  39. agenta/sdk/utils/exceptions.py +0 -19
  40. agenta/sdk/utils/singleton.py +0 -13
  41. {agenta-0.25.4a3.dist-info → agenta-0.25.4a4.dist-info}/WHEEL +0 -0
  42. {agenta-0.25.4a3.dist-info → agenta-0.25.4a4.dist-info}/entry_points.txt +0 -0
@@ -1,196 +1,111 @@
1
+ # Stdlib Imports
1
2
  import inspect
2
3
  import traceback
3
4
  from functools import wraps
4
- from itertools import chain
5
- from typing import Callable, Optional, Any, Dict, List
5
+ from typing import Any, Callable, Optional, List, Union
6
6
 
7
+ # Own Imports
7
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
8
12
 
9
13
 
10
- from agenta.sdk.utils.exceptions import suppress
14
+ logging.setLevel("DEBUG")
11
15
 
12
- from agenta.sdk.context.tracing import tracing_context
13
- from agenta.sdk.tracing.conventions import parse_span_kind
14
16
 
17
+ class instrument(BaseDecorator):
18
+ """Decorator class for monitoring llm apps functions.
15
19
 
16
- class instrument:
17
- 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
+ """
18
38
 
19
39
  def __init__(
20
40
  self,
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",
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,
28
45
  ) -> None:
29
- self.kind = spankind if spankind is not None else kind
30
46
  self.config = config
47
+ self.spankind = spankind
31
48
  self.ignore_inputs = ignore_inputs
32
49
  self.ignore_outputs = ignore_outputs
33
- self.max_depth = max_depth
34
50
 
35
51
  def __call__(self, func: Callable[..., Any]):
36
52
  is_coroutine_function = inspect.iscoroutinefunction(func)
37
53
 
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
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)
48
58
 
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.
59
+ return input_dict
54
60
 
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()
61
+ def redact(io, blacklist):
62
+ return {
63
+ key: io[key]
64
+ for key in io.keys()
63
65
  if key
64
66
  not in (
65
- ignore
66
- if isinstance(ignore, list)
67
- else io.keys()
68
- if ignore is True
67
+ blacklist
68
+ if isinstance(blacklist, list)
69
69
  else []
70
+ if blacklist is False
71
+ else io.keys()
70
72
  )
71
73
  }
72
74
 
73
- return io
75
+ def patch(result):
76
+ TRACE_DEFAULT_KEY = "__default__"
74
77
 
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.
78
+ outputs = result
78
79
 
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
- )
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"])
93
94
 
94
95
  return outputs
95
96
 
96
97
  @wraps(func)
97
98
  async def async_wrapper(*args, **kwargs):
98
99
  async def wrapped_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")
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)
148
107
 
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
- )
108
+ ag.tracing.store_outputs(redact(patch(result), self.ignore_outputs))
194
109
 
195
110
  return result
196
111
 
@@ -199,98 +114,15 @@ class instrument:
199
114
  @wraps(func)
200
115
  def sync_wrapper(*args, **kwargs):
201
116
  def wrapped_func(*args, **kwargs):
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
- )
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))
294
126
 
295
127
  return result
296
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()