agenta 0.14.14a1__py3-none-any.whl → 0.15.0__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,59 +1,79 @@
1
- # Stdlib Imports
1
+ import os
2
+ from threading import Lock
2
3
  from datetime import datetime, timezone
3
4
  from typing import Optional, Dict, Any, List, Union
4
5
 
5
- # Own Imports
6
6
  from agenta.sdk.tracing.logger import llm_logger
7
7
  from agenta.sdk.tracing.tasks_manager import TaskQueue
8
8
  from agenta.client.backend.client import AsyncAgentaApi
9
9
  from agenta.client.backend.client import AsyncObservabilityClient
10
10
  from agenta.client.backend.types.create_span import CreateSpan, SpanKind, SpanStatusCode
11
11
 
12
- # Third Party Imports
13
12
  from bson.objectid import ObjectId
14
13
 
14
+ VARIANT_TRACKING_FEATURE_FLAG = False
15
15
 
16
- class Tracing(object):
17
- """Agenta llm tracing object.
18
16
 
19
- Args:
20
- base_url (str): The URL of the backend host
17
+ class SingletonMeta(type):
18
+ """
19
+ Thread-safe implementation of Singleton.
20
+ """
21
+
22
+ _instances = {} # type: ignore
23
+
24
+ # We need the lock mechanism to synchronize threads \
25
+ # during the initial access to the Singleton object.
26
+ _lock: Lock = Lock()
27
+
28
+ def __call__(cls, *args, **kwargs):
29
+ """
30
+ Ensures that changes to the `__init__` arguments do not affect the
31
+ returned instance.
32
+
33
+ Uses a lock to make this method thread-safe. If an instance of the class
34
+ does not already exist, it creates one. Otherwise, it returns the
35
+ existing instance.
36
+ """
37
+
38
+ with cls._lock:
39
+ if cls not in cls._instances:
40
+ instance = super().__call__(*args, **kwargs)
41
+ cls._instances[cls] = instance
42
+ return cls._instances[cls]
43
+
44
+
45
+ class Tracing(metaclass=SingletonMeta):
46
+ """The `Tracing` class is an agent for LLM tracing with specific initialization arguments.
47
+
48
+ __init__ args:
49
+ host (str): The URL of the backend host
21
50
  api_key (str): The API Key of the backend host
22
51
  tasks_manager (TaskQueue): The tasks manager dedicated to handling asynchronous tasks
23
52
  llm_logger (Logger): The logger associated with the LLM tracing
24
53
  max_workers (int): The maximum number of workers to run tracing
25
54
  """
26
55
 
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
56
  def __init__(
35
57
  self,
36
- base_url: str,
58
+ host: str,
37
59
  app_id: str,
38
- variant_id: str,
39
- variant_name: Optional[str] = None,
40
60
  api_key: Optional[str] = None,
41
61
  max_workers: Optional[int] = None,
42
62
  ):
43
- self.base_url = base_url + "/api"
63
+ self.host = host + "/api"
44
64
  self.api_key = api_key if api_key is not None else ""
45
65
  self.llm_logger = llm_logger
46
66
  self.app_id = app_id
47
- self.variant_id = variant_id
48
- self.variant_name = variant_name
49
67
  self.tasks_manager = TaskQueue(
50
68
  max_workers if max_workers else 4, logger=llm_logger
51
69
  )
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] = []
70
+ self.active_span: Optional[CreateSpan] = None
71
+ self.active_trace_id: Optional[str] = None
72
+ self.pending_spans: List[CreateSpan] = []
56
73
  self.tags: List[str] = []
74
+ self.trace_config_cache: Dict[
75
+ str, Any
76
+ ] = {} # used to save the trace configuration before starting the first span
57
77
  self.span_dict: Dict[str, CreateSpan] = {} # type: ignore
58
78
 
59
79
  @property
@@ -65,131 +85,130 @@ class Tracing(object):
65
85
  """
66
86
 
67
87
  return AsyncAgentaApi(
68
- base_url=self.base_url, api_key=self.api_key, timeout=120 # type: ignore
88
+ base_url=self.host, api_key=self.api_key, timeout=120 # type: ignore
69
89
  ).observability
70
90
 
71
91
  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
92
  self,
80
- span_attributes: Dict[str, Any],
81
- key: str,
82
- value: Any,
83
- parent_key: Optional[str] = None,
93
+ attributes: Dict[str, Any] = {},
84
94
  ):
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
95
+ if (
96
+ self.active_span is None
97
+ ): # This is the case where entrypoint wants to save the trace information but the parent span has not been initialized yet
98
+ for key, value in attributes.items():
99
+ self.trace_config_cache[key] = value
90
100
  else:
91
- span_attributes[key] = value
101
+ for key, value in attributes.items():
102
+ self.active_span.attributes[key] = value
92
103
 
93
104
  def set_trace_tags(self, tags: List[str]):
94
105
  self.tags.extend(tags)
95
106
 
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
107
  def start_span(
123
108
  self,
124
109
  name: str,
125
110
  spankind: str,
126
111
  input: Dict[str, Any],
127
- config: Dict[str, Any] = {},
112
+ config: Optional[Dict[str, Any]] = None,
113
+ **kwargs,
128
114
  ) -> CreateSpan:
129
115
  span_id = self._create_span_id()
130
- self.llm_logger.info(f"Recording {spankind} span...")
116
+ self.llm_logger.info(
117
+ f"Recording {'parent' if spankind == 'workflow' else spankind} span..."
118
+ )
131
119
  span = CreateSpan(
132
120
  id=span_id,
133
121
  inputs=input,
134
122
  name=name,
135
123
  app_id=self.app_id,
136
- variant_id=self.variant_id,
137
- variant_name=self.variant_name,
138
124
  config=config,
139
- environment=self.active_trace.environment,
140
- parent_span_id=self.parent_span_id,
141
125
  spankind=spankind.upper(),
142
126
  attributes={},
143
127
  status=SpanStatusCode.UNSET.value,
144
128
  start_time=datetime.now(timezone.utc),
129
+ outputs=None,
130
+ tags=None,
131
+ user=None,
132
+ end_time=None,
133
+ tokens=None,
134
+ cost=None,
135
+ token_consumption=None,
136
+ parent_span_id=None,
145
137
  )
146
138
 
147
- self.active_span = span
139
+ if self.active_trace_id is None: # This is a parent span
140
+ self.active_trace_id = self._create_trace_id()
141
+ span.environment = (
142
+ self.trace_config_cache.get("environment")
143
+ if self.trace_config_cache is not None
144
+ else os.environ.get("environment", "unset")
145
+ )
146
+ span.config = (
147
+ self.trace_config_cache.get("config")
148
+ if not config and self.trace_config_cache is not None
149
+ else None
150
+ )
151
+ if VARIANT_TRACKING_FEATURE_FLAG:
152
+ # TODO: we should get the variant_id and variant_name (and environment) from the config object
153
+ span.variant_id = config.variant_id
154
+ span.variant_name = (config.variant_name,)
155
+
156
+ else:
157
+ span.parent_span_id = self.active_span.id
148
158
  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
- )
159
+ self.active_span = span
160
+
161
+ self.llm_logger.info(f"Recorded span and setting parent_span_id: {span.id}")
153
162
  return span
154
163
 
155
164
  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
- )
165
+ span.status = value
166
+
167
+ def end_span(self, outputs: Dict[str, Any]):
168
+ """
169
+ Ends the active span, if it is a parent span, ends the trace too.
170
+ """
171
+ if self.active_span is None:
172
+ raise ValueError("There is no active span to end.")
173
+ self.active_span.end_time = datetime.now(timezone.utc)
174
+ self.active_span.outputs = [outputs.get("message", "")]
175
+ self.active_span.cost = outputs.get("cost", None)
176
+ self.active_span.tokens = outputs.get("usage", None)
167
177
 
168
178
  # Push span to list of recorded spans
169
- self.recorded_spans.append(updated_span)
179
+ self.pending_spans.append(self.active_span)
170
180
  self.llm_logger.info(
171
- f"Pushed {updated_span.spankind} span {updated_span.id} to recorded spans."
181
+ f"Pushed {self.active_span.spankind} span {self.active_span.id} to recorded spans."
172
182
  )
183
+ if self.active_span.parent_span_id is None:
184
+ self.end_trace(parent_span=self.active_span)
185
+ else:
186
+ self.active_span = self.span_dict[self.active_span.parent_span_id]
173
187
 
174
- def end_recording(self, outputs: Dict[str, Any], span: CreateSpan, **kwargs):
175
- self.end_span(outputs=outputs, span=span, **kwargs)
188
+ def end_trace(self, parent_span: CreateSpan):
176
189
  if self.api_key == "":
177
190
  return
178
191
 
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)}")
192
+ if not self.active_trace_id:
193
+ raise RuntimeError("No active trace to end.")
194
+
195
+ self.llm_logger.info("Preparing to send recorded spans for processing.")
196
+ self.llm_logger.info(f"Recorded spans => {len(self.pending_spans)}")
181
197
  self.tasks_manager.add_task(
182
- self.active_trace.id,
198
+ self.active_trace_id,
183
199
  "trace",
184
200
  self.client.create_traces(
185
- trace=self.recording_trace_id, spans=self.recorded_spans # type: ignore
201
+ trace=self.active_trace_id, spans=self.pending_spans # type: ignore
186
202
  ),
187
203
  self.client,
188
204
  )
189
205
  self.llm_logger.info(
190
- f"Tracing for {span.id} recorded successfully and sent for processing."
206
+ f"Tracing for {parent_span.id} recorded successfully and sent for processing."
191
207
  )
192
- self._clear_recorded_spans()
208
+ self._clear_pending_spans()
209
+ self.active_trace_id = None
210
+ self.active_span = None
211
+ self.trace_config_cache.clear()
193
212
 
194
213
  def _create_trace_id(self) -> str:
195
214
  """Creates a unique mongo id for the trace object.
@@ -209,12 +228,12 @@ class Tracing(object):
209
228
 
210
229
  return str(ObjectId())
211
230
 
212
- def _clear_recorded_spans(self) -> None:
231
+ def _clear_pending_spans(self) -> None:
213
232
  """
214
233
  Clear the list of recorded spans to prepare for next batch processing.
215
234
  """
216
235
 
217
- self.recorded_spans = []
236
+ self.pending_spans = []
218
237
  self.llm_logger.info(
219
- f"Cleared all recorded spans from batch: {self.recorded_spans}"
238
+ f"Cleared all recorded spans from batch: {self.pending_spans}"
220
239
  )
@@ -2,7 +2,7 @@ import logging
2
2
 
3
3
 
4
4
  class LLMLogger:
5
- def __init__(self, name="LLMLogger", level=logging.INFO):
5
+ def __init__(self, name="LLMLogger", level=logging.DEBUG):
6
6
  self.logger = logging.getLogger(name)
7
7
  self.logger.setLevel(level)
8
8
 
@@ -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,25 +112,28 @@ 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
-
121
- if default and not any(default in choices for choices in choices.values()):
134
+ if default and not any(
135
+ default in choice_list for choice_list in choices.values()
136
+ ):
122
137
  if not choices:
123
138
  print(
124
139
  f"Warning: Default value {default} provided but choices are empty."
@@ -129,23 +144,31 @@ class GroupedMultipleChoiceParam(str):
129
144
  )
130
145
 
131
146
  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]
147
+ for choices in choices.values():
148
+ if choices:
149
+ default = choices[0]
150
+ break
137
151
 
138
152
  instance = super().__new__(cls, default)
139
- instance.choices = choices # type: ignore
140
- instance.default = default # type: ignore
153
+ instance.choices = choices
154
+ instance.default = default
141
155
  return instance
142
156
 
143
157
  @classmethod
144
- def __schema_type_properties__(cls) -> dict:
145
- return {
146
- "x-parameter": "grouped_choice",
147
- "type": "string",
148
- }
158
+ def __modify_schema__(cls, field_schema: dict[str, Any], **kwargs):
159
+ choices = kwargs.get("choices", {})
160
+ field_schema.update(
161
+ {
162
+ "x-parameter": "grouped_choice",
163
+ "type": "string",
164
+ "choices": choices,
165
+ }
166
+ )
167
+
168
+
169
+ class Message(BaseModel):
170
+ role: str
171
+ content: str
149
172
 
150
173
 
151
174
  class MessagesInput(list):
@@ -160,32 +183,28 @@ class MessagesInput(list):
160
183
 
161
184
  """
162
185
 
163
- def __new__(cls, messages: List[Dict[str, str]] = []):
164
- instance = super().__new__(cls)
165
- instance.default = messages # type: ignore
186
+ def __new__(cls, messages: List[Dict[str, str]] = None):
187
+ instance = super().__new__(cls, messages)
188
+ instance.default = messages
166
189
  return instance
167
190
 
168
191
  @classmethod
169
- def __schema_type_properties__(cls) -> dict:
170
- return {"x-parameter": "messages", "type": "array"}
192
+ def __modify_schema__(cls, field_schema: dict[str, Any]):
193
+ field_schema.update({"x-parameter": "messages", "type": "array"})
171
194
 
172
195
 
173
196
  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
197
  @classmethod
180
- def __schema_type_properties__(cls) -> dict:
181
- return {"x-parameter": "file_url", "type": "string"}
198
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
199
+ field_schema.update({"x-parameter": "file_url", "type": "string"})
182
200
 
183
201
 
184
202
  class Context(BaseModel):
185
- model_config = ConfigDict(extra="allow")
203
+ class Config:
204
+ extra = Extra.allow
186
205
 
187
206
  def to_json(self):
188
- return self.model_dump()
207
+ return self.json()
189
208
 
190
209
  @classmethod
191
210
  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.14.14a1
3
+ Version: 0.15.0
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)