edsl 0.1.30.dev4__py3-none-any.whl → 0.1.31__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.
Files changed (47) hide show
  1. edsl/__version__.py +1 -1
  2. edsl/agents/Invigilator.py +7 -2
  3. edsl/agents/PromptConstructionMixin.py +18 -1
  4. edsl/config.py +4 -0
  5. edsl/conjure/Conjure.py +6 -0
  6. edsl/coop/coop.py +4 -0
  7. edsl/coop/utils.py +9 -1
  8. edsl/data/CacheHandler.py +3 -4
  9. edsl/enums.py +2 -0
  10. edsl/inference_services/DeepInfraService.py +6 -91
  11. edsl/inference_services/GroqService.py +18 -0
  12. edsl/inference_services/InferenceServicesCollection.py +13 -5
  13. edsl/inference_services/OpenAIService.py +64 -21
  14. edsl/inference_services/registry.py +2 -1
  15. edsl/jobs/Jobs.py +80 -33
  16. edsl/jobs/buckets/TokenBucket.py +24 -5
  17. edsl/jobs/interviews/Interview.py +122 -75
  18. edsl/jobs/interviews/InterviewExceptionEntry.py +101 -0
  19. edsl/jobs/interviews/InterviewTaskBuildingMixin.py +58 -52
  20. edsl/jobs/interviews/interview_exception_tracking.py +68 -10
  21. edsl/jobs/runners/JobsRunnerAsyncio.py +112 -81
  22. edsl/jobs/runners/JobsRunnerStatusData.py +0 -237
  23. edsl/jobs/runners/JobsRunnerStatusMixin.py +291 -35
  24. edsl/jobs/tasks/QuestionTaskCreator.py +1 -5
  25. edsl/jobs/tasks/TaskCreators.py +8 -2
  26. edsl/jobs/tasks/TaskHistory.py +145 -1
  27. edsl/language_models/LanguageModel.py +135 -75
  28. edsl/language_models/ModelList.py +8 -2
  29. edsl/language_models/registry.py +16 -0
  30. edsl/questions/QuestionFunctional.py +34 -2
  31. edsl/questions/QuestionMultipleChoice.py +58 -8
  32. edsl/questions/QuestionNumerical.py +0 -1
  33. edsl/questions/descriptors.py +42 -2
  34. edsl/results/DatasetExportMixin.py +258 -75
  35. edsl/results/Result.py +53 -5
  36. edsl/results/Results.py +66 -27
  37. edsl/results/ResultsToolsMixin.py +1 -1
  38. edsl/scenarios/Scenario.py +14 -0
  39. edsl/scenarios/ScenarioList.py +59 -21
  40. edsl/scenarios/ScenarioListExportMixin.py +16 -5
  41. edsl/scenarios/ScenarioListPdfMixin.py +3 -0
  42. edsl/study/Study.py +2 -2
  43. edsl/surveys/Survey.py +35 -1
  44. {edsl-0.1.30.dev4.dist-info → edsl-0.1.31.dist-info}/METADATA +4 -2
  45. {edsl-0.1.30.dev4.dist-info → edsl-0.1.31.dist-info}/RECORD +47 -45
  46. {edsl-0.1.30.dev4.dist-info → edsl-0.1.31.dist-info}/WHEEL +1 -1
  47. {edsl-0.1.30.dev4.dist-info → edsl-0.1.31.dist-info}/LICENSE +0 -0
edsl/results/Results.py CHANGED
@@ -604,6 +604,38 @@ class Results(UserList, Mixins, Base):
604
604
  self = self.add_column(key, values)
605
605
  return self
606
606
 
607
+ @staticmethod
608
+ def _create_evaluator(
609
+ result: Result, functions_dict: Optional[dict] = None
610
+ ) -> EvalWithCompoundTypes:
611
+ """Create an evaluator for the expression.
612
+
613
+ >>> from unittest.mock import Mock
614
+ >>> result = Mock()
615
+ >>> result.combined_dict = {'how_feeling': 'OK'}
616
+
617
+ >>> evaluator = Results._create_evaluator(result = result, functions_dict = {})
618
+ >>> evaluator.eval("how_feeling == 'OK'")
619
+ True
620
+
621
+ >>> result.combined_dict = {'answer': {'how_feeling': 'OK'}}
622
+ >>> evaluator = Results._create_evaluator(result = result, functions_dict = {})
623
+ >>> evaluator.eval("answer.how_feeling== 'OK'")
624
+ True
625
+
626
+ Note that you need to refer to the answer dictionary in the expression.
627
+
628
+ >>> evaluator.eval("how_feeling== 'OK'")
629
+ Traceback (most recent call last):
630
+ ...
631
+ simpleeval.NameNotDefined: 'how_feeling' is not defined for expression 'how_feeling== 'OK''
632
+ """
633
+ if functions_dict is None:
634
+ functions_dict = {}
635
+ return EvalWithCompoundTypes(
636
+ names=result.combined_dict, functions=functions_dict
637
+ )
638
+
607
639
  def mutate(
608
640
  self, new_var_string: str, functions_dict: Optional[dict] = None
609
641
  ) -> Results:
@@ -636,13 +668,8 @@ class Results(UserList, Mixins, Base):
636
668
  # create the evaluator
637
669
  functions_dict = functions_dict or {}
638
670
 
639
- def create_evaluator(result: Result) -> EvalWithCompoundTypes:
640
- return EvalWithCompoundTypes(
641
- names=result.combined_dict, functions=functions_dict
642
- )
643
-
644
671
  def new_result(old_result: "Result", var_name: str) -> "Result":
645
- evaluator = create_evaluator(old_result)
672
+ evaluator = self._create_evaluator(old_result, functions_dict)
646
673
  value = evaluator.eval(expression)
647
674
  new_result = old_result.copy()
648
675
  new_result["answer"][var_name] = value
@@ -742,6 +769,9 @@ class Results(UserList, Mixins, Base):
742
769
  >>> results = Results.example()
743
770
  >>> results.select('how_feeling')
744
771
  Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}])
772
+
773
+ >>> results.select('how_feeling', 'model', 'how_feeling')
774
+ Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}, {'model.model': ['gpt-4-1106-preview', 'gpt-4-1106-preview', 'gpt-4-1106-preview', 'gpt-4-1106-preview']}, {'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}])
745
775
  """
746
776
 
747
777
  if len(self) == 0:
@@ -799,10 +829,19 @@ class Results(UserList, Mixins, Base):
799
829
  # Return the index of this key in the list_of_keys
800
830
  return items_in_order.index(single_key)
801
831
 
802
- sorted(new_data, key=sort_by_key_order)
832
+ # sorted(new_data, key=sort_by_key_order)
803
833
  from edsl.results.Dataset import Dataset
804
834
 
805
- return Dataset(new_data)
835
+ sorted_new_data = []
836
+
837
+ # WORKS but slow
838
+ for key in items_in_order:
839
+ for d in new_data:
840
+ if key in d:
841
+ sorted_new_data.append(d)
842
+ break
843
+
844
+ return Dataset(sorted_new_data)
806
845
 
807
846
  def sort_by(self, *columns: str, reverse: bool = False) -> Results:
808
847
  import warnings
@@ -917,29 +956,29 @@ class Results(UserList, Mixins, Base):
917
956
  "You must use '==' instead of '=' in the filter expression."
918
957
  )
919
958
 
920
- def create_evaluator(result):
921
- """Create an evaluator for the given result.
922
- The 'combined_dict' is a mapping of all values for that Result object.
923
- """
924
- return EvalWithCompoundTypes(names=result.combined_dict)
925
-
926
959
  try:
927
960
  # iterates through all the results and evaluates the expression
928
- new_data = [
929
- result
930
- for result in self.data
931
- if create_evaluator(result).eval(expression)
932
- ]
961
+ new_data = []
962
+ for result in self.data:
963
+ evaluator = self._create_evaluator(result)
964
+ result.check_expression(expression) # check expression
965
+ if evaluator.eval(expression):
966
+ new_data.append(result)
967
+
968
+ except ValueError as e:
969
+ raise ResultsFilterError(
970
+ f"Error in filter. Exception:{e}",
971
+ f"The expression you provided was: {expression}",
972
+ "See https://docs.expectedparrot.com/en/latest/results.html#filtering-results for more details.",
973
+ )
933
974
  except Exception as e:
934
975
  raise ResultsFilterError(
935
- f"""Error in filter. Exception:{e}.
936
- The expression you provided was: {expression}.
937
- Please make sure that the expression is a valid Python expression that evaluates to a boolean.
938
- For example, 'how_feeling == "Great"' is a valid expression, as is 'how_feeling in ["Great", "Terrible"]'.
939
- However, 'how_feeling = "Great"' is not a valid expression.
940
-
941
- See https://docs.expectedparrot.com/en/latest/results.html#filtering-results for more details.
942
- """
976
+ f"""Error in filter. Exception:{e}.""",
977
+ f"""The expression you provided was: {expression}.""",
978
+ """Please make sure that the expression is a valid Python expression that evaluates to a boolean.""",
979
+ """For example, 'how_feeling == "Great"' is a valid expression, as is 'how_feeling in ["Great", "Terrible"]'., """,
980
+ """However, 'how_feeling = "Great"' is not a valid expression.""",
981
+ """See https://docs.expectedparrot.com/en/latest/results.html#filtering-results for more details.""",
943
982
  )
944
983
 
945
984
  if len(new_data) == 0:
@@ -37,12 +37,12 @@ class ResultsToolsMixin:
37
37
  print_exceptions=False,
38
38
  ) -> dict:
39
39
  from edsl import ScenarioList
40
+ from edsl import QuestionCheckBox
40
41
 
41
42
  values = self.select(field).to_list()
42
43
  scenarios = ScenarioList.from_list("field", values).add_value(
43
44
  "context", context
44
45
  )
45
-
46
46
  q = QuestionCheckBox(
47
47
  question_text="""
48
48
  {{ context }}
@@ -182,6 +182,19 @@ class Scenario(Base, UserDict, ScenarioImageMixin, ScenarioHtmlMixin):
182
182
  new_scenario[key] = self[key]
183
183
  return new_scenario
184
184
 
185
+ @classmethod
186
+ def from_url(cls, url: str, field_name: Optional[str] = "text") -> "Scenario":
187
+ """Creates a scenario from a URL.
188
+
189
+ :param url: The URL to create the scenario from.
190
+ :param field_name: The field name to use for the text.
191
+
192
+ """
193
+ import requests
194
+
195
+ text = requests.get(url).text
196
+ return cls({"url": url, field_name: text})
197
+
185
198
  @classmethod
186
199
  def from_image(cls, image_path: str) -> str:
187
200
  """Creates a scenario with a base64 encoding of an image.
@@ -207,6 +220,7 @@ class Scenario(Base, UserDict, ScenarioImageMixin, ScenarioHtmlMixin):
207
220
  @classmethod
208
221
  def from_pdf(cls, pdf_path):
209
222
  import fitz # PyMuPDF
223
+ from edsl import Scenario
210
224
 
211
225
  # Ensure the file exists
212
226
  if not os.path.exists(pdf_path):
@@ -1,12 +1,14 @@
1
1
  """A list of Scenarios to be used in a survey."""
2
2
 
3
3
  from __future__ import annotations
4
+ from typing import Any, Optional, Union, List, Callable
4
5
  import csv
5
6
  import random
6
7
  from collections import UserList, Counter
7
8
  from collections.abc import Iterable
9
+
8
10
  from simpleeval import EvalWithCompoundTypes
9
- from typing import Any, Optional, Union, List
11
+
10
12
  from edsl.Base import Base
11
13
  from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
12
14
  from edsl.scenarios.Scenario import Scenario
@@ -58,7 +60,13 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
58
60
  return f"ScenarioList({self.data})"
59
61
 
60
62
  def __mul__(self, other: ScenarioList) -> ScenarioList:
61
- """Takes the cross product of two ScenarioLists."""
63
+ """Takes the cross product of two ScenarioLists.
64
+
65
+ >>> s1 = ScenarioList.from_list("a", [1, 2])
66
+ >>> s2 = ScenarioList.from_list("b", [3, 4])
67
+ >>> s1 * s2
68
+ ScenarioList([Scenario({'a': 1, 'b': 3}), Scenario({'a': 1, 'b': 4}), Scenario({'a': 2, 'b': 3}), Scenario({'a': 2, 'b': 4})])
69
+ """
62
70
  from itertools import product
63
71
 
64
72
  new_sl = []
@@ -79,7 +87,12 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
79
87
  return self.__mul__(other)
80
88
 
81
89
  def shuffle(self, seed: Optional[str] = "edsl") -> ScenarioList:
82
- """Shuffle the ScenarioList."""
90
+ """Shuffle the ScenarioList.
91
+
92
+ >>> s = ScenarioList.from_list("a", [1,2,3,4])
93
+ >>> s.shuffle()
94
+ ScenarioList([Scenario({'a': 3}), Scenario({'a': 4}), Scenario({'a': 1}), Scenario({'a': 2})])
95
+ """
83
96
  random.seed(seed)
84
97
  random.shuffle(self.data)
85
98
  return self
@@ -107,10 +120,14 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
107
120
  return dict(Counter([scenario[field] for scenario in self]))
108
121
 
109
122
  def sample(self, n: int, seed="edsl") -> ScenarioList:
110
- """Return a random sample from the ScenarioList"""
123
+ """Return a random sample from the ScenarioList
111
124
 
112
- if seed != "edsl":
113
- random.seed(seed)
125
+ >>> s = ScenarioList.from_list("a", [1,2,3,4,5,6])
126
+ >>> s.sample(3)
127
+ ScenarioList([Scenario({'a': 2}), Scenario({'a': 1}), Scenario({'a': 3})])
128
+ """
129
+
130
+ random.seed(seed)
114
131
 
115
132
  return ScenarioList(random.sample(self.data, n))
116
133
 
@@ -136,7 +153,9 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
136
153
  new_scenarios.append(new_scenario)
137
154
  return ScenarioList(new_scenarios)
138
155
 
139
- def mutate(self, new_var_string: str, functions_dict: dict = None) -> ScenarioList:
156
+ def mutate(
157
+ self, new_var_string: str, functions_dict: Optional[dict[str, Callable]] = None
158
+ ) -> ScenarioList:
140
159
  """
141
160
  Return a new ScenarioList with a new variable added.
142
161
 
@@ -145,6 +164,7 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
145
164
  >>> s = ScenarioList([Scenario({'a': 1, 'b': 2}), Scenario({'a': 1, 'b': 1})])
146
165
  >>> s.mutate("c = a + b")
147
166
  ScenarioList([Scenario({'a': 1, 'b': 2, 'c': 3}), Scenario({'a': 1, 'b': 1, 'c': 2})])
167
+
148
168
  """
149
169
  if "=" not in new_var_string:
150
170
  raise Exception(
@@ -222,6 +242,16 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
222
242
 
223
243
  return ScenarioList(new_data)
224
244
 
245
+ def from_urls(self, urls: list[str], field_name: Optional[str] = "text") -> ScenarioList:
246
+ """Create a ScenarioList from a list of URLs.
247
+
248
+ :param urls: A list of URLs.
249
+ :param field_name: The name of the field to store the text from the URLs.
250
+
251
+
252
+ """
253
+ return ScenarioList([Scenario.from_url(url, field_name) for url in urls])
254
+
225
255
  def select(self, *fields) -> ScenarioList:
226
256
  """
227
257
  Selects scenarios with only the references fields.
@@ -264,11 +294,19 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
264
294
  return cls([Scenario({name: value}) for value in values])
265
295
 
266
296
  def to_dataset(self) -> "Dataset":
297
+ """
298
+ >>> s = ScenarioList.from_list("a", [1,2,3])
299
+ >>> s.to_dataset()
300
+ Dataset([{'a': [1, 2, 3]}])
301
+ >>> s = ScenarioList.from_list("a", [1,2,3]).add_list("b", [4,5,6])
302
+ >>> s.to_dataset()
303
+ Dataset([{'a': [1, 2, 3]}, {'b': [4, 5, 6]}])
304
+ """
267
305
  from edsl.results.Dataset import Dataset
268
306
 
269
307
  keys = self[0].keys()
270
- data = {key: [scenario[key] for scenario in self.data] for key in keys}
271
- return Dataset([data])
308
+ data = [{key: [scenario[key] for scenario in self.data]} for key in keys]
309
+ return Dataset(data)
272
310
 
273
311
  def add_list(self, name, values) -> ScenarioList:
274
312
  """Add a list of values to a ScenarioList.
@@ -286,7 +324,7 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
286
324
  self.append(Scenario({name: value}))
287
325
  return self
288
326
 
289
- def add_value(self, name, value):
327
+ def add_value(self, name: str, value: Any) -> ScenarioList:
290
328
  """Add a value to all scenarios in a ScenarioList.
291
329
 
292
330
  Example:
@@ -340,7 +378,7 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
340
378
  """
341
379
  return cls([Scenario(row) for row in df.to_dict(orient="records")])
342
380
 
343
- def to_key_value(self, field, value=None) -> Union[dict, set]:
381
+ def to_key_value(self, field: str, value=None) -> Union[dict, set]:
344
382
  """Return the set of values in the field.
345
383
 
346
384
  Example:
@@ -459,16 +497,16 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
459
497
  table.add_row(str(i), s.rich_print())
460
498
  return table
461
499
 
462
- def print(
463
- self,
464
- format: Optional[str] = None,
465
- max_rows: Optional[int] = None,
466
- pretty_labels: Optional[dict] = None,
467
- filename: str = None,
468
- ):
469
- from edsl.utilities.interface import print_scenario_list
470
-
471
- print_scenario_list(self[:max_rows])
500
+ # def print(
501
+ # self,
502
+ # format: Optional[str] = None,
503
+ # max_rows: Optional[int] = None,
504
+ # pretty_labels: Optional[dict] = None,
505
+ # filename: str = None,
506
+ # ):
507
+ # from edsl.utilities.interface import print_scenario_list
508
+
509
+ # print_scenario_list(self[:max_rows])
472
510
 
473
511
  def __getitem__(self, key: Union[int, slice]) -> Any:
474
512
  """Return the item at the given index.
@@ -5,7 +5,7 @@ from edsl.results.DatasetExportMixin import DatasetExportMixin
5
5
 
6
6
 
7
7
  def to_dataset(func):
8
- """Convert the Results object to a Dataset object before calling the function."""
8
+ """Convert the object to a Dataset object before calling the function."""
9
9
 
10
10
  @wraps(func)
11
11
  def wrapper(self, *args, **kwargs):
@@ -20,13 +20,24 @@ def to_dataset(func):
20
20
  return wrapper
21
21
 
22
22
 
23
- def decorate_all_methods(cls):
24
- for attr_name, attr_value in cls.__dict__.items():
25
- if callable(attr_value):
23
+ def decorate_methods_from_mixin(cls, mixin_cls):
24
+ for attr_name, attr_value in mixin_cls.__dict__.items():
25
+ if callable(attr_value) and not attr_name.startswith("__"):
26
26
  setattr(cls, attr_name, to_dataset(attr_value))
27
27
  return cls
28
28
 
29
29
 
30
- @decorate_all_methods
30
+ # def decorate_all_methods(cls):
31
+ # for attr_name, attr_value in cls.__dict__.items():
32
+ # if callable(attr_value):
33
+ # setattr(cls, attr_name, to_dataset(attr_value))
34
+ # return cls
35
+
36
+
37
+ # @decorate_all_methods
31
38
  class ScenarioListExportMixin(DatasetExportMixin):
32
39
  """Mixin class for exporting Results objects."""
40
+
41
+ def __init_subclass__(cls, **kwargs):
42
+ super().__init_subclass__(**kwargs)
43
+ decorate_methods_from_mixin(cls, DatasetExportMixin)
@@ -43,6 +43,9 @@ class ScenarioListPdfMixin:
43
43
 
44
44
  @staticmethod
45
45
  def extract_text_from_pdf(pdf_path):
46
+ from edsl import Scenario
47
+
48
+ # TODO: Add test case
46
49
  # Ensure the file exists
47
50
  if not os.path.exists(pdf_path):
48
51
  raise FileNotFoundError(f"The file {pdf_path} does not exist.")
edsl/study/Study.py CHANGED
@@ -461,13 +461,13 @@ class Study:
461
461
  else:
462
462
  self.objects[oe.hash] = oe
463
463
 
464
- def push(self, refresh=False) -> None:
464
+ def push(self) -> dict:
465
465
  """Push the objects to coop."""
466
466
 
467
467
  from edsl import Coop
468
468
 
469
469
  coop = Coop()
470
- coop.create(self, description=self.description)
470
+ return coop.create(self, description=self.description)
471
471
 
472
472
  @classmethod
473
473
  def pull(cls, uuid: Optional[Union[str, UUID]] = None, url: Optional[str] = None):
edsl/surveys/Survey.py CHANGED
@@ -106,6 +106,39 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
106
106
 
107
107
  return dict_hash(self._to_dict())
108
108
 
109
+ def __add__(self, other: Survey) -> Survey:
110
+ """Combine two surveys.
111
+
112
+ :param other: The other survey to combine with this one.
113
+ >>> s1 = Survey.example()
114
+ >>> from edsl import QuestionFreeText
115
+ >>> s2 = Survey([QuestionFreeText(question_text="What is your name?", question_name="yo")])
116
+ >>> s3 = s1 + s2
117
+ Traceback (most recent call last):
118
+ ...
119
+ ValueError: ('Cannot combine two surveys with non-default rules.', "Please use the 'clear_non_default_rules' method to remove non-default rules from the survey.")
120
+ >>> s3 = s1.clear_non_default_rules() + s2
121
+ >>> len(s3.questions)
122
+ 4
123
+
124
+ """
125
+ if (
126
+ len(self.rule_collection.non_default_rules) > 0
127
+ or len(other.rule_collection.non_default_rules) > 0
128
+ ):
129
+ raise ValueError(
130
+ "Cannot combine two surveys with non-default rules.",
131
+ "Please use the 'clear_non_default_rules' method to remove non-default rules from the survey.",
132
+ )
133
+
134
+ return Survey(questions=self.questions + other.questions)
135
+
136
+ def clear_non_default_rules(self) -> Survey:
137
+ s = Survey()
138
+ for question in self.questions:
139
+ s.add_question(question)
140
+ return s
141
+
109
142
  @property
110
143
  def parameters(self):
111
144
  return set.union(*[q.parameters for q in self.questions])
@@ -1151,4 +1184,5 @@ def main():
1151
1184
  if __name__ == "__main__":
1152
1185
  import doctest
1153
1186
 
1154
- doctest.testmod(optionflags=doctest.ELLIPSIS | doctest.SKIP)
1187
+ # doctest.testmod(optionflags=doctest.ELLIPSIS | doctest.SKIP)
1188
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edsl
3
- Version: 0.1.30.dev4
3
+ Version: 0.1.31
4
4
  Summary: Create and analyze LLM-based surveys
5
5
  Home-page: https://www.expectedparrot.com/
6
6
  License: MIT
@@ -19,10 +19,11 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
19
  Requires-Dist: aiohttp (>=3.9.1,<4.0.0)
20
20
  Requires-Dist: anthropic (>=0.23.1,<0.24.0)
21
21
  Requires-Dist: black[jupyter] (>=24.4.2,<25.0.0)
22
+ Requires-Dist: groq (>=0.9.0,<0.10.0)
22
23
  Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
23
24
  Requires-Dist: jupyter (>=1.0.0,<2.0.0)
24
25
  Requires-Dist: markdown2 (>=2.4.11,<3.0.0)
25
- Requires-Dist: matplotlib (>=3.8.4,<4.0.0)
26
+ Requires-Dist: matplotlib (>=3.8,<3.9)
26
27
  Requires-Dist: nest-asyncio (>=1.5.9,<2.0.0)
27
28
  Requires-Dist: numpy (>=1.22,<2.0)
28
29
  Requires-Dist: openai (>=1.4.0,<2.0.0)
@@ -35,6 +36,7 @@ Requires-Dist: python-docx (>=1.1.0,<2.0.0)
35
36
  Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
36
37
  Requires-Dist: restrictedpython (>=7.1,<8.0)
37
38
  Requires-Dist: rich (>=13.7.0,<14.0.0)
39
+ Requires-Dist: setuptools (<72.0)
38
40
  Requires-Dist: simpleeval (>=0.9.13,<0.10.0)
39
41
  Requires-Dist: sqlalchemy (>=2.0.23,<3.0.0)
40
42
  Requires-Dist: tenacity (>=8.2.3,<9.0.0)