agenta 0.15.0a0__py3-none-any.whl → 0.15.0a1__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,11 @@ 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[str, Any] = {} # used to save the trace configuration before starting the first span
57
80
  self.span_dict: Dict[str, CreateSpan] = {} # type: ignore
58
81
 
59
82
  @property
@@ -65,69 +88,35 @@ class Tracing(object):
65
88
  """
66
89
 
67
90
  return AsyncAgentaApi(
68
- base_url=self.base_url, api_key=self.api_key, timeout=120 # type: ignore
91
+ base_url=self.host, api_key=self.api_key, timeout=120 # type: ignore
69
92
  ).observability
70
93
 
71
94
  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
95
  self,
80
- span_attributes: Dict[str, Any],
81
- key: str,
82
- value: Any,
83
- parent_key: Optional[str] = None,
96
+ attributes: Dict[str, Any] = {},
84
97
  ):
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
98
+ if self.active_span is None: # This is the case where entrypoint wants to save the trace information but the parent span has not been initialized yet
99
+ for key, value in attributes.items():
100
+ self.trace_config_cache[key] = value
90
101
  else:
91
- span_attributes[key] = value
102
+ for key, value in attributes.items():
103
+ self.active_span.attributes[key] = value
92
104
 
93
105
  def set_trace_tags(self, tags: List[str]):
94
106
  self.tags.extend(tags)
95
107
 
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
108
  def start_span(
123
109
  self,
124
110
  name: str,
125
111
  spankind: str,
126
112
  input: Dict[str, Any],
127
- config: Dict[str, Any] = {},
113
+ config: Optional[Dict[str, Any]] = None,
114
+ **kwargs,
128
115
  ) -> CreateSpan:
129
116
  span_id = self._create_span_id()
130
- self.llm_logger.info(f"Recording {spankind} span...")
117
+ self.llm_logger.info(
118
+ f"Recording {'parent' if spankind == 'workflow' else spankind} span..."
119
+ )
131
120
  span = CreateSpan(
132
121
  id=span_id,
133
122
  inputs=input,
@@ -136,60 +125,88 @@ class Tracing(object):
136
125
  variant_id=self.variant_id,
137
126
  variant_name=self.variant_name,
138
127
  config=config,
139
- environment=self.active_trace.environment,
140
- parent_span_id=self.parent_span_id,
141
128
  spankind=spankind.upper(),
142
129
  attributes={},
143
130
  status=SpanStatusCode.UNSET.value,
144
131
  start_time=datetime.now(timezone.utc),
132
+ outputs=None,
133
+ tags=None,
134
+ user=None,
135
+ end_time=None,
136
+ tokens=None,
137
+ cost=None,
138
+ token_consumption=None,
139
+ parent_span_id=None,
145
140
  )
146
141
 
147
- self.active_span = span
142
+ if self.active_trace_id is None: # This is a parent span
143
+ self.active_trace_id = self._create_trace_id()
144
+ span.environment = (
145
+ self.trace_config_cache.get("environment")
146
+ if self.trace_config_cache is not None
147
+ else os.environ.get("environment", "unset")
148
+ )
149
+ span.config = (
150
+ self.trace_config_cache.get("config")
151
+ if not config and self.trace_config_cache is not None
152
+ else None
153
+ )
154
+ else:
155
+ span.parent_span_id = self.active_span.id
148
156
  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
- )
157
+ self.active_span = span
158
+
159
+ self.llm_logger.info(f"Recorded span and setting parent_span_id: {span.id}")
153
160
  return span
154
161
 
155
162
  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
- )
163
+ span.status = value
164
+
165
+ def end_span(self, outputs: Dict[str, Any]):
166
+ """
167
+ Ends the active span, if it is a parent span, ends the trace too.
168
+ """
169
+ if self.active_span is None:
170
+ raise ValueError("There is no active span to end.")
171
+ self.active_span.end_time = datetime.now(timezone.utc)
172
+ self.active_span.outputs = [outputs.get("message", "")]
173
+ self.active_span.cost = outputs.get("cost", None)
174
+ self.active_span.tokens = outputs.get("usage", None)
167
175
 
168
176
  # Push span to list of recorded spans
169
- self.recorded_spans.append(updated_span)
177
+ self.pending_spans.append(self.active_span)
170
178
  self.llm_logger.info(
171
- f"Pushed {updated_span.spankind} span {updated_span.id} to recorded spans."
179
+ f"Pushed {self.active_span.spankind} span {self.active_span.id} to recorded spans."
172
180
  )
181
+ if self.active_span.parent_span_id is None:
182
+ self.end_trace(parent_span=self.active_span)
183
+ else:
184
+ self.active_span = self.span_dict[self.active_span.parent_span_id]
173
185
 
174
- def end_recording(self, outputs: Dict[str, Any], span: CreateSpan, **kwargs):
175
- self.end_span(outputs=outputs, span=span, **kwargs)
186
+ def end_trace(self, parent_span: CreateSpan):
176
187
  if self.api_key == "":
177
188
  return
178
189
 
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)}")
190
+ if not self.active_trace_id:
191
+ raise RuntimeError("No active trace to end.")
192
+
193
+ self.llm_logger.info("Preparing to send recorded spans for processing.")
194
+ self.llm_logger.info(f"Recorded spans => {len(self.pending_spans)}")
181
195
  self.tasks_manager.add_task(
182
- self.active_trace.id,
196
+ self.active_trace_id,
183
197
  "trace",
184
198
  self.client.create_traces(
185
- trace=self.recording_trace_id, spans=self.recorded_spans # type: ignore
199
+ trace=self.active_trace_id, spans=self.pending_spans # type: ignore
186
200
  ),
187
201
  self.client,
188
202
  )
189
203
  self.llm_logger.info(
190
- f"Tracing for {span.id} recorded successfully and sent for processing."
204
+ f"Tracing for {parent_span.id} recorded successfully and sent for processing."
191
205
  )
192
- self._clear_recorded_spans()
206
+ self._clear_pending_spans()
207
+ self.active_trace_id = None
208
+ self.active_span = None
209
+ self.trace_config_cache.clear()
193
210
 
194
211
  def _create_trace_id(self) -> str:
195
212
  """Creates a unique mongo id for the trace object.
@@ -209,12 +226,12 @@ class Tracing(object):
209
226
 
210
227
  return str(ObjectId())
211
228
 
212
- def _clear_recorded_spans(self) -> None:
229
+ def _clear_pending_spans(self) -> None:
213
230
  """
214
231
  Clear the list of recorded spans to prepare for next batch processing.
215
232
  """
216
233
 
217
- self.recorded_spans = []
234
+ self.pending_spans = []
218
235
  self.llm_logger.info(
219
- f"Cleared all recorded spans from batch: {self.recorded_spans}"
236
+ f"Cleared all recorded spans from batch: {self.pending_spans}"
220
237
  )
@@ -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.0a1
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)