agenta 0.15.0a0__py3-none-any.whl → 0.15.0a2__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.

@@ -1,4 +1,6 @@
1
1
  # Stdlib Imports
2
+ import os
3
+ from threading import Lock
2
4
  from datetime import datetime, timezone
3
5
  from typing import Optional, Dict, Any, List, Union
4
6
 
@@ -13,34 +15,55 @@ from agenta.client.backend.types.create_span import CreateSpan, SpanKind, SpanSt
13
15
  from bson.objectid import ObjectId
14
16
 
15
17
 
16
- class Tracing(object):
17
- """Agenta llm tracing object.
18
+ class SingletonMeta(type):
19
+ """
20
+ Thread-safe implementation of Singleton.
21
+ """
22
+
23
+ _instances = {} # type: ignore
24
+
25
+ # We need the lock mechanism to synchronize threads \
26
+ # during the initial access to the Singleton object.
27
+ _lock: Lock = Lock()
28
+
29
+ def __call__(cls, *args, **kwargs):
30
+ """
31
+ Ensures that changes to the `__init__` arguments do not affect the
32
+ returned instance.
33
+
34
+ Uses a lock to make this method thread-safe. If an instance of the class
35
+ does not already exist, it creates one. Otherwise, it returns the
36
+ existing instance.
37
+ """
38
+
39
+ with cls._lock:
40
+ if cls not in cls._instances:
41
+ instance = super().__call__(*args, **kwargs)
42
+ cls._instances[cls] = instance
43
+ return cls._instances[cls]
44
+
45
+
46
+ class Tracing(metaclass=SingletonMeta):
47
+ """The `Tracing` class is an agent for LLM tracing with specific initialization arguments.
18
48
 
19
- Args:
20
- base_url (str): The URL of the backend host
49
+ __init__ args:
50
+ host (str): The URL of the backend host
21
51
  api_key (str): The API Key of the backend host
22
52
  tasks_manager (TaskQueue): The tasks manager dedicated to handling asynchronous tasks
23
53
  llm_logger (Logger): The logger associated with the LLM tracing
24
54
  max_workers (int): The maximum number of workers to run tracing
25
55
  """
26
56
 
27
- _instance = None
28
-
29
- def __new__(cls, *args, **kwargs):
30
- if not cls._instance:
31
- cls._instance = super().__new__(cls)
32
- return cls._instance
33
-
34
57
  def __init__(
35
58
  self,
36
- base_url: str,
59
+ host: str,
37
60
  app_id: str,
38
- variant_id: str,
61
+ variant_id: Optional[str] = None,
39
62
  variant_name: Optional[str] = None,
40
63
  api_key: Optional[str] = None,
41
64
  max_workers: Optional[int] = None,
42
65
  ):
43
- self.base_url = base_url + "/api"
66
+ self.host = host + "/api"
44
67
  self.api_key = api_key if api_key is not None else ""
45
68
  self.llm_logger = llm_logger
46
69
  self.app_id = app_id
@@ -49,11 +72,13 @@ class Tracing(object):
49
72
  self.tasks_manager = TaskQueue(
50
73
  max_workers if max_workers else 4, logger=llm_logger
51
74
  )
52
- self.active_span = CreateSpan
53
- self.active_trace = CreateSpan
54
- self.recording_trace_id: Union[str, None] = None
55
- self.recorded_spans: List[CreateSpan] = []
75
+ self.active_span: Optional[CreateSpan] = None
76
+ self.active_trace_id: Optional[str] = None
77
+ self.pending_spans: List[CreateSpan] = []
56
78
  self.tags: List[str] = []
79
+ self.trace_config_cache: Dict[
80
+ str, Any
81
+ ] = {} # used to save the trace configuration before starting the first span
57
82
  self.span_dict: Dict[str, CreateSpan] = {} # type: ignore
58
83
 
59
84
  @property
@@ -65,69 +90,37 @@ class Tracing(object):
65
90
  """
66
91
 
67
92
  return AsyncAgentaApi(
68
- base_url=self.base_url, api_key=self.api_key, timeout=120 # type: ignore
93
+ base_url=self.host, api_key=self.api_key, timeout=120 # type: ignore
69
94
  ).observability
70
95
 
71
96
  def set_span_attribute(
72
- self, parent_key: Optional[str] = None, attributes: Dict[str, Any] = {}
73
- ):
74
- span = self.span_dict[self.active_span.id] # type: ignore
75
- for key, value in attributes.items():
76
- self.set_attribute(span.attributes, key, value, parent_key) # type: ignore
77
-
78
- def set_attribute(
79
97
  self,
80
- span_attributes: Dict[str, Any],
81
- key: str,
82
- value: Any,
83
- parent_key: Optional[str] = None,
98
+ attributes: Dict[str, Any] = {},
84
99
  ):
85
- if parent_key is not None:
86
- model_config = span_attributes.get(parent_key, None)
87
- if not model_config:
88
- span_attributes[parent_key] = {}
89
- span_attributes[parent_key][key] = value
100
+ if (
101
+ self.active_span is None
102
+ ): # This is the case where entrypoint wants to save the trace information but the parent span has not been initialized yet
103
+ for key, value in attributes.items():
104
+ self.trace_config_cache[key] = value
90
105
  else:
91
- span_attributes[key] = value
106
+ for key, value in attributes.items():
107
+ self.active_span.attributes[key] = value
92
108
 
93
109
  def set_trace_tags(self, tags: List[str]):
94
110
  self.tags.extend(tags)
95
111
 
96
- def start_parent_span(
97
- self, name: str, inputs: Dict[str, Any], config: Dict[str, Any], **kwargs
98
- ):
99
- trace_id = self._create_trace_id()
100
- span_id = self._create_span_id()
101
- self.llm_logger.info("Recording parent span...")
102
- span = CreateSpan(
103
- id=span_id,
104
- app_id=self.app_id,
105
- variant_id=self.variant_id,
106
- variant_name=self.variant_name,
107
- inputs=inputs,
108
- name=name,
109
- config=config,
110
- environment=kwargs.get("environment"),
111
- spankind=SpanKind.WORKFLOW.value,
112
- status=SpanStatusCode.UNSET.value,
113
- start_time=datetime.now(timezone.utc),
114
- )
115
- self.active_trace = span
116
- self.recording_trace_id = trace_id
117
- self.parent_span_id = span.id
118
- self.llm_logger.info(
119
- f"Recorded active_trace and setting parent_span_id: {span.id}"
120
- )
121
-
122
112
  def start_span(
123
113
  self,
124
114
  name: str,
125
115
  spankind: str,
126
116
  input: Dict[str, Any],
127
- config: Dict[str, Any] = {},
117
+ config: Optional[Dict[str, Any]] = None,
118
+ **kwargs,
128
119
  ) -> CreateSpan:
129
120
  span_id = self._create_span_id()
130
- self.llm_logger.info(f"Recording {spankind} span...")
121
+ self.llm_logger.info(
122
+ f"Recording {'parent' if spankind == 'workflow' else spankind} span..."
123
+ )
131
124
  span = CreateSpan(
132
125
  id=span_id,
133
126
  inputs=input,
@@ -136,60 +129,88 @@ class Tracing(object):
136
129
  variant_id=self.variant_id,
137
130
  variant_name=self.variant_name,
138
131
  config=config,
139
- environment=self.active_trace.environment,
140
- parent_span_id=self.parent_span_id,
141
132
  spankind=spankind.upper(),
142
133
  attributes={},
143
134
  status=SpanStatusCode.UNSET.value,
144
135
  start_time=datetime.now(timezone.utc),
136
+ outputs=None,
137
+ tags=None,
138
+ user=None,
139
+ end_time=None,
140
+ tokens=None,
141
+ cost=None,
142
+ token_consumption=None,
143
+ parent_span_id=None,
145
144
  )
146
145
 
147
- self.active_span = span
146
+ if self.active_trace_id is None: # This is a parent span
147
+ self.active_trace_id = self._create_trace_id()
148
+ span.environment = (
149
+ self.trace_config_cache.get("environment")
150
+ if self.trace_config_cache is not None
151
+ else os.environ.get("environment", "unset")
152
+ )
153
+ span.config = (
154
+ self.trace_config_cache.get("config")
155
+ if not config and self.trace_config_cache is not None
156
+ else None
157
+ )
158
+ else:
159
+ span.parent_span_id = self.active_span.id
148
160
  self.span_dict[span.id] = span
149
- self.parent_span_id = span.id
150
- self.llm_logger.info(
151
- f"Recorded active_span and setting parent_span_id: {span.id}"
152
- )
161
+ self.active_span = span
162
+
163
+ self.llm_logger.info(f"Recorded span and setting parent_span_id: {span.id}")
153
164
  return span
154
165
 
155
166
  def update_span_status(self, span: CreateSpan, value: str):
156
- updated_span = CreateSpan(**{**span.dict(), "status": value})
157
- self.active_span = updated_span
158
-
159
- def end_span(self, outputs: Dict[str, Any], span: CreateSpan, **kwargs):
160
- updated_span = CreateSpan(
161
- **span.dict(),
162
- end_time=datetime.now(timezone.utc),
163
- outputs=[outputs["message"]],
164
- cost=outputs.get("cost", None),
165
- tokens=outputs.get("usage"),
166
- )
167
+ span.status = value
168
+
169
+ def end_span(self, outputs: Dict[str, Any]):
170
+ """
171
+ Ends the active span, if it is a parent span, ends the trace too.
172
+ """
173
+ if self.active_span is None:
174
+ raise ValueError("There is no active span to end.")
175
+ self.active_span.end_time = datetime.now(timezone.utc)
176
+ self.active_span.outputs = [outputs.get("message", "")]
177
+ self.active_span.cost = outputs.get("cost", None)
178
+ self.active_span.tokens = outputs.get("usage", None)
167
179
 
168
180
  # Push span to list of recorded spans
169
- self.recorded_spans.append(updated_span)
181
+ self.pending_spans.append(self.active_span)
170
182
  self.llm_logger.info(
171
- f"Pushed {updated_span.spankind} span {updated_span.id} to recorded spans."
183
+ f"Pushed {self.active_span.spankind} span {self.active_span.id} to recorded spans."
172
184
  )
185
+ if self.active_span.parent_span_id is None:
186
+ self.end_trace(parent_span=self.active_span)
187
+ else:
188
+ self.active_span = self.span_dict[self.active_span.parent_span_id]
173
189
 
174
- def end_recording(self, outputs: Dict[str, Any], span: CreateSpan, **kwargs):
175
- self.end_span(outputs=outputs, span=span, **kwargs)
190
+ def end_trace(self, parent_span: CreateSpan):
176
191
  if self.api_key == "":
177
192
  return
178
193
 
179
- self.llm_logger.info(f"Preparing to send recorded spans for processing.")
180
- self.llm_logger.info(f"Recorded spans => {len(self.recorded_spans)}")
194
+ if not self.active_trace_id:
195
+ raise RuntimeError("No active trace to end.")
196
+
197
+ self.llm_logger.info("Preparing to send recorded spans for processing.")
198
+ self.llm_logger.info(f"Recorded spans => {len(self.pending_spans)}")
181
199
  self.tasks_manager.add_task(
182
- self.active_trace.id,
200
+ self.active_trace_id,
183
201
  "trace",
184
202
  self.client.create_traces(
185
- trace=self.recording_trace_id, spans=self.recorded_spans # type: ignore
203
+ trace=self.active_trace_id, spans=self.pending_spans # type: ignore
186
204
  ),
187
205
  self.client,
188
206
  )
189
207
  self.llm_logger.info(
190
- f"Tracing for {span.id} recorded successfully and sent for processing."
208
+ f"Tracing for {parent_span.id} recorded successfully and sent for processing."
191
209
  )
192
- self._clear_recorded_spans()
210
+ self._clear_pending_spans()
211
+ self.active_trace_id = None
212
+ self.active_span = None
213
+ self.trace_config_cache.clear()
193
214
 
194
215
  def _create_trace_id(self) -> str:
195
216
  """Creates a unique mongo id for the trace object.
@@ -209,12 +230,12 @@ class Tracing(object):
209
230
 
210
231
  return str(ObjectId())
211
232
 
212
- def _clear_recorded_spans(self) -> None:
233
+ def _clear_pending_spans(self) -> None:
213
234
  """
214
235
  Clear the list of recorded spans to prepare for next batch processing.
215
236
  """
216
237
 
217
- self.recorded_spans = []
238
+ self.pending_spans = []
218
239
  self.llm_logger.info(
219
- f"Cleared all recorded spans from batch: {self.recorded_spans}"
240
+ f"Cleared all recorded spans from batch: {self.pending_spans}"
220
241
  )
@@ -106,9 +106,7 @@ class TaskQueue(object):
106
106
  future.result()
107
107
  except Exception as exc:
108
108
  self._logger.error(f"Error running task: {str(exc)}")
109
- self._logger.error(
110
- f"Recording trace {task.coroutine_type} status to ERROR."
111
- )
109
+ self._logger.error(f"Recording {task.coroutine_type} status to ERROR.")
112
110
  break
113
111
  finally:
114
112
  self.tasks.task_done()
agenta/sdk/types.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import json
2
- from typing import Dict, List, Optional
2
+ from typing import Any, Dict, List, Optional
3
3
 
4
- from pydantic import ConfigDict, BaseModel, HttpUrl
4
+ from pydantic import BaseModel, Extra, HttpUrl, Field
5
5
 
6
6
 
7
7
  class InFile:
@@ -24,75 +24,87 @@ class FuncResponse(BaseModel):
24
24
 
25
25
 
26
26
  class DictInput(dict):
27
- def __new__(cls, default_keys: Optional[List[str]] = None):
27
+ def __new__(cls, default_keys=None):
28
28
  instance = super().__new__(cls, default_keys)
29
29
  if default_keys is None:
30
30
  default_keys = []
31
- instance.data = [key for key in default_keys] # type: ignore
31
+ instance.data = [key for key in default_keys]
32
32
  return instance
33
33
 
34
34
  @classmethod
35
- def __schema_type_properties__(cls) -> dict:
36
- return {"x-parameter": "dict"}
35
+ def __modify_schema__(cls, field_schema):
36
+ field_schema.update({"x-parameter": "dict"})
37
37
 
38
38
 
39
39
  class TextParam(str):
40
40
  @classmethod
41
- def __schema_type_properties__(cls) -> dict:
42
- return {"x-parameter": "text", "type": "string"}
41
+ def __modify_schema__(cls, field_schema):
42
+ field_schema.update({"x-parameter": "text"})
43
43
 
44
44
 
45
45
  class BinaryParam(int):
46
46
  def __new__(cls, value: bool = False):
47
47
  instance = super().__new__(cls, int(value))
48
- instance.default = value # type: ignore
48
+ instance.default = value
49
49
  return instance
50
50
 
51
51
  @classmethod
52
- def __schema_type_properties__(cls) -> dict:
53
- return {
54
- "x-parameter": "bool",
55
- "type": "boolean",
56
- }
52
+ def __modify_schema__(cls, field_schema):
53
+ field_schema.update(
54
+ {
55
+ "x-parameter": "bool",
56
+ "type": "boolean",
57
+ }
58
+ )
57
59
 
58
60
 
59
61
  class IntParam(int):
60
62
  def __new__(cls, default: int = 6, minval: float = 1, maxval: float = 10):
61
63
  instance = super().__new__(cls, default)
62
- instance.minval = minval # type: ignore
63
- instance.maxval = maxval # type: ignore
64
+ instance.minval = minval
65
+ instance.maxval = maxval
64
66
  return instance
65
67
 
66
68
  @classmethod
67
- def __schema_type_properties__(cls) -> dict:
68
- return {"x-parameter": "int", "type": "integer"}
69
+ def __modify_schema__(cls, field_schema):
70
+ field_schema.update(
71
+ {
72
+ "x-parameter": "int",
73
+ "type": "integer",
74
+ "minimum": 1,
75
+ "maximum": 10,
76
+ }
77
+ )
69
78
 
70
79
 
71
80
  class FloatParam(float):
72
81
  def __new__(cls, default: float = 0.5, minval: float = 0.0, maxval: float = 1.0):
73
82
  instance = super().__new__(cls, default)
74
- instance.default = default # type: ignore
75
- instance.minval = minval # type: ignore
76
- instance.maxval = maxval # type: ignore
83
+ instance.minval = minval
84
+ instance.maxval = maxval
77
85
  return instance
78
86
 
79
87
  @classmethod
80
- def __schema_type_properties__(cls) -> dict:
81
- return {"x-parameter": "float", "type": "number"}
88
+ def __modify_schema__(cls, field_schema):
89
+ field_schema.update(
90
+ {
91
+ "x-parameter": "float",
92
+ "type": "number",
93
+ "minimum": 0.0,
94
+ "maximum": 1.0,
95
+ }
96
+ )
82
97
 
83
98
 
84
99
  class MultipleChoiceParam(str):
85
- def __new__(
86
- cls, default: Optional[str] = None, choices: Optional[List[str]] = None
87
- ):
88
- if default is not None and type(default) is list:
100
+ def __new__(cls, default: str = None, choices: List[str] = None):
101
+ if type(default) is list:
89
102
  raise ValueError(
90
103
  "The order of the parameters for MultipleChoiceParam is wrong! It's MultipleChoiceParam(default, choices) and not the opposite"
91
104
  )
92
-
93
- if not default and choices is not None:
105
+ if default is None and choices:
94
106
  # if a default value is not provided,
95
- # set the first value in the choices list
107
+ # uset the first value in the choices list
96
108
  default = choices[0]
97
109
 
98
110
  if default is None and not choices:
@@ -100,21 +112,23 @@ class MultipleChoiceParam(str):
100
112
  raise ValueError("You must provide either a default value or choices")
101
113
 
102
114
  instance = super().__new__(cls, default)
103
- instance.choices = choices # type: ignore
104
- instance.default = default # type: ignore
115
+ instance.choices = choices
116
+ instance.default = default
105
117
  return instance
106
118
 
107
119
  @classmethod
108
- def __schema_type_properties__(cls) -> dict:
109
- return {"x-parameter": "choice", "type": "string", "enum": []}
120
+ def __modify_schema__(cls, field_schema: dict[str, Any]):
121
+ field_schema.update(
122
+ {
123
+ "x-parameter": "choice",
124
+ "type": "string",
125
+ "enum": [],
126
+ }
127
+ )
110
128
 
111
129
 
112
130
  class GroupedMultipleChoiceParam(str):
113
- def __new__(
114
- cls,
115
- default: Optional[str] = None,
116
- choices: Optional[Dict[str, List[str]]] = None,
117
- ):
131
+ def __new__(cls, default: str = None, choices: Dict[str, List[str]] = None):
118
132
  if choices is None:
119
133
  choices = {}
120
134
 
@@ -129,23 +143,31 @@ class GroupedMultipleChoiceParam(str):
129
143
  )
130
144
 
131
145
  if not default:
132
- default_selected_choice = next(
133
- (choices for choices in choices.values()), None
134
- )
135
- if default_selected_choice:
136
- default = default_selected_choice[0]
146
+ for choices in choices.values():
147
+ if choices:
148
+ default = choices[0]
149
+ break
137
150
 
138
151
  instance = super().__new__(cls, default)
139
- instance.choices = choices # type: ignore
140
- instance.default = default # type: ignore
152
+ instance.choices = choices
153
+ instance.default = default
141
154
  return instance
142
155
 
143
156
  @classmethod
144
- def __schema_type_properties__(cls) -> dict:
145
- return {
146
- "x-parameter": "grouped_choice",
147
- "type": "string",
148
- }
157
+ def __modify_schema__(cls, field_schema: dict[str, Any], **kwargs):
158
+ choices = kwargs.get("choices", {})
159
+ field_schema.update(
160
+ {
161
+ "x-parameter": "grouped_choice",
162
+ "type": "string",
163
+ "choices": choices,
164
+ }
165
+ )
166
+
167
+
168
+ class Message(BaseModel):
169
+ role: str
170
+ content: str
149
171
 
150
172
 
151
173
  class MessagesInput(list):
@@ -160,32 +182,28 @@ class MessagesInput(list):
160
182
 
161
183
  """
162
184
 
163
- def __new__(cls, messages: List[Dict[str, str]] = []):
164
- instance = super().__new__(cls)
165
- instance.default = messages # type: ignore
185
+ def __new__(cls, messages: List[Dict[str, str]] = None):
186
+ instance = super().__new__(cls, messages)
187
+ instance.default = messages
166
188
  return instance
167
189
 
168
190
  @classmethod
169
- def __schema_type_properties__(cls) -> dict:
170
- return {"x-parameter": "messages", "type": "array"}
191
+ def __modify_schema__(cls, field_schema: dict[str, Any]):
192
+ field_schema.update({"x-parameter": "messages", "type": "array"})
171
193
 
172
194
 
173
195
  class FileInputURL(HttpUrl):
174
- def __new__(cls, url: str):
175
- instance = super().__new__(cls, url)
176
- instance.default = url # type: ignore
177
- return instance
178
-
179
196
  @classmethod
180
- def __schema_type_properties__(cls) -> dict:
181
- return {"x-parameter": "file_url", "type": "string"}
197
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
198
+ field_schema.update({"x-parameter": "file_url", "type": "string"})
182
199
 
183
200
 
184
201
  class Context(BaseModel):
185
- model_config = ConfigDict(extra="allow")
202
+ class Config:
203
+ extra = Extra.allow
186
204
 
187
205
  def to_json(self):
188
- return self.model_dump()
206
+ return self.json()
189
207
 
190
208
  @classmethod
191
209
  def from_json(cls, json_str: str):
@@ -1,7 +1,7 @@
1
1
  import agenta
2
2
 
3
3
 
4
- def set_global(setup=None, config=None):
4
+ def set_global(setup=None, config=None, tracing=None):
5
5
  """Allows usage of agenta.config and agenta.setup in the user's code.
6
6
 
7
7
  Args:
@@ -12,3 +12,5 @@ def set_global(setup=None, config=None):
12
12
  agenta.setup = setup
13
13
  if config is not None:
14
14
  agenta.config = config
15
+ if tracing is not None:
16
+ agenta.tracing = tracing
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: agenta
3
- Version: 0.15.0a0
3
+ Version: 0.15.0a2
4
4
  Summary: The SDK for agenta is an open-source LLMOps platform.
5
5
  Home-page: https://agenta.ai
6
6
  Keywords: LLMOps,LLM,evaluation,prompt engineering
@@ -18,12 +18,12 @@ Classifier: Topic :: Software Development :: Libraries
18
18
  Requires-Dist: cachetools (>=5.3.3,<6.0.0)
19
19
  Requires-Dist: click (>=8.1.3,<9.0.0)
20
20
  Requires-Dist: docker (>=6.1.1,<8.0.0)
21
- Requires-Dist: fastapi (>=0.111.0,<0.112.0)
21
+ Requires-Dist: fastapi (>=0.96.1)
22
22
  Requires-Dist: httpx (>=0.24,<0.28)
23
23
  Requires-Dist: importlib-metadata (>=6.7,<8.0)
24
24
  Requires-Dist: ipdb (>=0.13)
25
25
  Requires-Dist: posthog (>=3.1.0,<4.0.0)
26
- Requires-Dist: pydantic (>=2.7.1,<3.0.0)
26
+ Requires-Dist: pydantic (==1.10.13)
27
27
  Requires-Dist: pymongo (>=4.6.3,<5.0.0)
28
28
  Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
29
29
  Requires-Dist: python-multipart (>=0.0.6,<0.0.10)