agenta 0.15.0__py3-none-any.whl → 0.15.0a0__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,79 +1,59 @@
1
- import os
2
- from threading import Lock
1
+ # Stdlib Imports
3
2
  from datetime import datetime, timezone
4
3
  from typing import Optional, Dict, Any, List, Union
5
4
 
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
12
13
  from bson.objectid import ObjectId
13
14
 
14
- VARIANT_TRACKING_FEATURE_FLAG = False
15
15
 
16
+ class Tracing(object):
17
+ """Agenta llm tracing object.
16
18
 
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
19
+ Args:
20
+ base_url (str): The URL of the backend host
50
21
  api_key (str): The API Key of the backend host
51
22
  tasks_manager (TaskQueue): The tasks manager dedicated to handling asynchronous tasks
52
23
  llm_logger (Logger): The logger associated with the LLM tracing
53
24
  max_workers (int): The maximum number of workers to run tracing
54
25
  """
55
26
 
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
+
56
34
  def __init__(
57
35
  self,
58
- host: str,
36
+ base_url: str,
59
37
  app_id: str,
38
+ variant_id: str,
39
+ variant_name: Optional[str] = None,
60
40
  api_key: Optional[str] = None,
61
41
  max_workers: Optional[int] = None,
62
42
  ):
63
- self.host = host + "/api"
43
+ self.base_url = base_url + "/api"
64
44
  self.api_key = api_key if api_key is not None else ""
65
45
  self.llm_logger = llm_logger
66
46
  self.app_id = app_id
47
+ self.variant_id = variant_id
48
+ self.variant_name = variant_name
67
49
  self.tasks_manager = TaskQueue(
68
50
  max_workers if max_workers else 4, logger=llm_logger
69
51
  )
70
- self.active_span: Optional[CreateSpan] = None
71
- self.active_trace_id: Optional[str] = None
72
- self.pending_spans: List[CreateSpan] = []
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] = []
73
56
  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
77
57
  self.span_dict: Dict[str, CreateSpan] = {} # type: ignore
78
58
 
79
59
  @property
@@ -85,130 +65,131 @@ class Tracing(metaclass=SingletonMeta):
85
65
  """
86
66
 
87
67
  return AsyncAgentaApi(
88
- base_url=self.host, api_key=self.api_key, timeout=120 # type: ignore
68
+ base_url=self.base_url, api_key=self.api_key, timeout=120 # type: ignore
89
69
  ).observability
90
70
 
91
71
  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(
92
79
  self,
93
- attributes: Dict[str, Any] = {},
80
+ span_attributes: Dict[str, Any],
81
+ key: str,
82
+ value: Any,
83
+ parent_key: Optional[str] = None,
94
84
  ):
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
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
90
  else:
101
- for key, value in attributes.items():
102
- self.active_span.attributes[key] = value
91
+ span_attributes[key] = value
103
92
 
104
93
  def set_trace_tags(self, tags: List[str]):
105
94
  self.tags.extend(tags)
106
95
 
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
+
107
122
  def start_span(
108
123
  self,
109
124
  name: str,
110
125
  spankind: str,
111
126
  input: Dict[str, Any],
112
- config: Optional[Dict[str, Any]] = None,
113
- **kwargs,
127
+ config: Dict[str, Any] = {},
114
128
  ) -> CreateSpan:
115
129
  span_id = self._create_span_id()
116
- self.llm_logger.info(
117
- f"Recording {'parent' if spankind == 'workflow' else spankind} span..."
118
- )
130
+ self.llm_logger.info(f"Recording {spankind} span...")
119
131
  span = CreateSpan(
120
132
  id=span_id,
121
133
  inputs=input,
122
134
  name=name,
123
135
  app_id=self.app_id,
136
+ variant_id=self.variant_id,
137
+ variant_name=self.variant_name,
124
138
  config=config,
139
+ environment=self.active_trace.environment,
140
+ parent_span_id=self.parent_span_id,
125
141
  spankind=spankind.upper(),
126
142
  attributes={},
127
143
  status=SpanStatusCode.UNSET.value,
128
144
  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,
137
145
  )
138
146
 
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
158
- self.span_dict[span.id] = span
159
147
  self.active_span = span
160
-
161
- self.llm_logger.info(f"Recorded span and setting parent_span_id: {span.id}")
148
+ 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
+ )
162
153
  return span
163
154
 
164
155
  def update_span_status(self, span: CreateSpan, value: str):
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)
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
+ )
177
167
 
178
168
  # Push span to list of recorded spans
179
- self.pending_spans.append(self.active_span)
169
+ self.recorded_spans.append(updated_span)
180
170
  self.llm_logger.info(
181
- f"Pushed {self.active_span.spankind} span {self.active_span.id} to recorded spans."
171
+ f"Pushed {updated_span.spankind} span {updated_span.id} to recorded spans."
182
172
  )
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]
187
173
 
188
- def end_trace(self, parent_span: CreateSpan):
174
+ def end_recording(self, outputs: Dict[str, Any], span: CreateSpan, **kwargs):
175
+ self.end_span(outputs=outputs, span=span, **kwargs)
189
176
  if self.api_key == "":
190
177
  return
191
178
 
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)}")
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)}")
197
181
  self.tasks_manager.add_task(
198
- self.active_trace_id,
182
+ self.active_trace.id,
199
183
  "trace",
200
184
  self.client.create_traces(
201
- trace=self.active_trace_id, spans=self.pending_spans # type: ignore
185
+ trace=self.recording_trace_id, spans=self.recorded_spans # type: ignore
202
186
  ),
203
187
  self.client,
204
188
  )
205
189
  self.llm_logger.info(
206
- f"Tracing for {parent_span.id} recorded successfully and sent for processing."
190
+ f"Tracing for {span.id} recorded successfully and sent for processing."
207
191
  )
208
- self._clear_pending_spans()
209
- self.active_trace_id = None
210
- self.active_span = None
211
- self.trace_config_cache.clear()
192
+ self._clear_recorded_spans()
212
193
 
213
194
  def _create_trace_id(self) -> str:
214
195
  """Creates a unique mongo id for the trace object.
@@ -228,12 +209,12 @@ class Tracing(metaclass=SingletonMeta):
228
209
 
229
210
  return str(ObjectId())
230
211
 
231
- def _clear_pending_spans(self) -> None:
212
+ def _clear_recorded_spans(self) -> None:
232
213
  """
233
214
  Clear the list of recorded spans to prepare for next batch processing.
234
215
  """
235
216
 
236
- self.pending_spans = []
217
+ self.recorded_spans = []
237
218
  self.llm_logger.info(
238
- f"Cleared all recorded spans from batch: {self.pending_spans}"
219
+ f"Cleared all recorded spans from batch: {self.recorded_spans}"
239
220
  )
@@ -2,7 +2,7 @@ import logging
2
2
 
3
3
 
4
4
  class LLMLogger:
5
- def __init__(self, name="LLMLogger", level=logging.DEBUG):
5
+ def __init__(self, name="LLMLogger", level=logging.INFO):
6
6
  self.logger = logging.getLogger(name)
7
7
  self.logger.setLevel(level)
8
8
 
@@ -106,7 +106,9 @@ 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(f"Recording {task.coroutine_type} status to ERROR.")
109
+ self._logger.error(
110
+ f"Recording trace {task.coroutine_type} status to ERROR."
111
+ )
110
112
  break
111
113
  finally:
112
114
  self.tasks.task_done()
agenta/sdk/types.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import json
2
- from typing import Any, Dict, List, Optional
2
+ from typing import Dict, List, Optional
3
3
 
4
- from pydantic import BaseModel, Extra, HttpUrl, Field
4
+ from pydantic import ConfigDict, BaseModel, HttpUrl
5
5
 
6
6
 
7
7
  class InFile:
@@ -24,87 +24,75 @@ class FuncResponse(BaseModel):
24
24
 
25
25
 
26
26
  class DictInput(dict):
27
- def __new__(cls, default_keys=None):
27
+ def __new__(cls, default_keys: Optional[List[str]] = 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]
31
+ instance.data = [key for key in default_keys] # type: ignore
32
32
  return instance
33
33
 
34
34
  @classmethod
35
- def __modify_schema__(cls, field_schema):
36
- field_schema.update({"x-parameter": "dict"})
35
+ def __schema_type_properties__(cls) -> dict:
36
+ return {"x-parameter": "dict"}
37
37
 
38
38
 
39
39
  class TextParam(str):
40
40
  @classmethod
41
- def __modify_schema__(cls, field_schema):
42
- field_schema.update({"x-parameter": "text"})
41
+ def __schema_type_properties__(cls) -> dict:
42
+ return {"x-parameter": "text", "type": "string"}
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
48
+ instance.default = value # type: ignore
49
49
  return instance
50
50
 
51
51
  @classmethod
52
- def __modify_schema__(cls, field_schema):
53
- field_schema.update(
54
- {
55
- "x-parameter": "bool",
56
- "type": "boolean",
57
- }
58
- )
52
+ def __schema_type_properties__(cls) -> dict:
53
+ return {
54
+ "x-parameter": "bool",
55
+ "type": "boolean",
56
+ }
59
57
 
60
58
 
61
59
  class IntParam(int):
62
60
  def __new__(cls, default: int = 6, minval: float = 1, maxval: float = 10):
63
61
  instance = super().__new__(cls, default)
64
- instance.minval = minval
65
- instance.maxval = maxval
62
+ instance.minval = minval # type: ignore
63
+ instance.maxval = maxval # type: ignore
66
64
  return instance
67
65
 
68
66
  @classmethod
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
- )
67
+ def __schema_type_properties__(cls) -> dict:
68
+ return {"x-parameter": "int", "type": "integer"}
78
69
 
79
70
 
80
71
  class FloatParam(float):
81
72
  def __new__(cls, default: float = 0.5, minval: float = 0.0, maxval: float = 1.0):
82
73
  instance = super().__new__(cls, default)
83
- instance.minval = minval
84
- instance.maxval = maxval
74
+ instance.default = default # type: ignore
75
+ instance.minval = minval # type: ignore
76
+ instance.maxval = maxval # type: ignore
85
77
  return instance
86
78
 
87
79
  @classmethod
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
- )
80
+ def __schema_type_properties__(cls) -> dict:
81
+ return {"x-parameter": "float", "type": "number"}
97
82
 
98
83
 
99
84
  class MultipleChoiceParam(str):
100
- def __new__(cls, default: str = None, choices: List[str] = None):
101
- if type(default) is list:
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:
102
89
  raise ValueError(
103
90
  "The order of the parameters for MultipleChoiceParam is wrong! It's MultipleChoiceParam(default, choices) and not the opposite"
104
91
  )
105
- if default is None and choices:
92
+
93
+ if not default and choices is not None:
106
94
  # if a default value is not provided,
107
- # uset the first value in the choices list
95
+ # set the first value in the choices list
108
96
  default = choices[0]
109
97
 
110
98
  if default is None and not choices:
@@ -112,28 +100,25 @@ class MultipleChoiceParam(str):
112
100
  raise ValueError("You must provide either a default value or choices")
113
101
 
114
102
  instance = super().__new__(cls, default)
115
- instance.choices = choices
116
- instance.default = default
103
+ instance.choices = choices # type: ignore
104
+ instance.default = default # type: ignore
117
105
  return instance
118
106
 
119
107
  @classmethod
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
- )
108
+ def __schema_type_properties__(cls) -> dict:
109
+ return {"x-parameter": "choice", "type": "string", "enum": []}
128
110
 
129
111
 
130
112
  class GroupedMultipleChoiceParam(str):
131
- def __new__(cls, default: str = None, choices: Dict[str, List[str]] = None):
113
+ def __new__(
114
+ cls,
115
+ default: Optional[str] = None,
116
+ choices: Optional[Dict[str, List[str]]] = None,
117
+ ):
132
118
  if choices is None:
133
119
  choices = {}
134
- if default and not any(
135
- default in choice_list for choice_list in choices.values()
136
- ):
120
+
121
+ if default and not any(default in choices for choices in choices.values()):
137
122
  if not choices:
138
123
  print(
139
124
  f"Warning: Default value {default} provided but choices are empty."
@@ -144,31 +129,23 @@ class GroupedMultipleChoiceParam(str):
144
129
  )
145
130
 
146
131
  if not default:
147
- for choices in choices.values():
148
- if choices:
149
- default = choices[0]
150
- break
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]
151
137
 
152
138
  instance = super().__new__(cls, default)
153
- instance.choices = choices
154
- instance.default = default
139
+ instance.choices = choices # type: ignore
140
+ instance.default = default # type: ignore
155
141
  return instance
156
142
 
157
143
  @classmethod
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
144
+ def __schema_type_properties__(cls) -> dict:
145
+ return {
146
+ "x-parameter": "grouped_choice",
147
+ "type": "string",
148
+ }
172
149
 
173
150
 
174
151
  class MessagesInput(list):
@@ -183,28 +160,32 @@ class MessagesInput(list):
183
160
 
184
161
  """
185
162
 
186
- def __new__(cls, messages: List[Dict[str, str]] = None):
187
- instance = super().__new__(cls, messages)
188
- instance.default = messages
163
+ def __new__(cls, messages: List[Dict[str, str]] = []):
164
+ instance = super().__new__(cls)
165
+ instance.default = messages # type: ignore
189
166
  return instance
190
167
 
191
168
  @classmethod
192
- def __modify_schema__(cls, field_schema: dict[str, Any]):
193
- field_schema.update({"x-parameter": "messages", "type": "array"})
169
+ def __schema_type_properties__(cls) -> dict:
170
+ return {"x-parameter": "messages", "type": "array"}
194
171
 
195
172
 
196
173
  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
+
197
179
  @classmethod
198
- def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
199
- field_schema.update({"x-parameter": "file_url", "type": "string"})
180
+ def __schema_type_properties__(cls) -> dict:
181
+ return {"x-parameter": "file_url", "type": "string"}
200
182
 
201
183
 
202
184
  class Context(BaseModel):
203
- class Config:
204
- extra = Extra.allow
185
+ model_config = ConfigDict(extra="allow")
205
186
 
206
187
  def to_json(self):
207
- return self.json()
188
+ return self.model_dump()
208
189
 
209
190
  @classmethod
210
191
  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, tracing=None):
4
+ def set_global(setup=None, config=None):
5
5
  """Allows usage of agenta.config and agenta.setup in the user's code.
6
6
 
7
7
  Args:
@@ -12,5 +12,3 @@ def set_global(setup=None, config=None, tracing=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.0
3
+ Version: 0.15.0a0
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.96.1)
21
+ Requires-Dist: fastapi (>=0.111.0,<0.112.0)
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 (==1.10.13)
26
+ Requires-Dist: pydantic (>=2.7.1,<3.0.0)
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)