edsl 0.1.39__py3-none-any.whl → 0.1.39.dev1__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 (212) hide show
  1. edsl/Base.py +116 -197
  2. edsl/__init__.py +7 -15
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +147 -351
  5. edsl/agents/AgentList.py +73 -211
  6. edsl/agents/Invigilator.py +50 -101
  7. edsl/agents/InvigilatorBase.py +70 -62
  8. edsl/agents/PromptConstructor.py +225 -143
  9. edsl/agents/__init__.py +1 -0
  10. edsl/agents/prompt_helpers.py +3 -3
  11. edsl/auto/AutoStudy.py +5 -18
  12. edsl/auto/StageBase.py +40 -53
  13. edsl/auto/StageQuestions.py +1 -2
  14. edsl/auto/utilities.py +6 -0
  15. edsl/config.py +2 -22
  16. edsl/conversation/car_buying.py +1 -2
  17. edsl/coop/PriceFetcher.py +1 -1
  18. edsl/coop/coop.py +47 -125
  19. edsl/coop/utils.py +14 -14
  20. edsl/data/Cache.py +27 -45
  21. edsl/data/CacheEntry.py +15 -12
  22. edsl/data/CacheHandler.py +12 -31
  23. edsl/data/RemoteCacheSync.py +46 -154
  24. edsl/data/__init__.py +3 -4
  25. edsl/data_transfer_models.py +1 -2
  26. edsl/enums.py +0 -27
  27. edsl/exceptions/__init__.py +50 -50
  28. edsl/exceptions/agents.py +0 -12
  29. edsl/exceptions/questions.py +6 -24
  30. edsl/exceptions/scenarios.py +0 -7
  31. edsl/inference_services/AnthropicService.py +19 -38
  32. edsl/inference_services/AwsBedrock.py +2 -0
  33. edsl/inference_services/AzureAI.py +2 -0
  34. edsl/inference_services/GoogleService.py +12 -7
  35. edsl/inference_services/InferenceServiceABC.py +85 -18
  36. edsl/inference_services/InferenceServicesCollection.py +79 -120
  37. edsl/inference_services/MistralAIService.py +3 -0
  38. edsl/inference_services/OpenAIService.py +35 -47
  39. edsl/inference_services/PerplexityService.py +3 -0
  40. edsl/inference_services/TestService.py +10 -11
  41. edsl/inference_services/TogetherAIService.py +3 -5
  42. edsl/jobs/Answers.py +14 -1
  43. edsl/jobs/Jobs.py +431 -356
  44. edsl/jobs/JobsChecks.py +10 -35
  45. edsl/jobs/JobsPrompts.py +4 -6
  46. edsl/jobs/JobsRemoteInferenceHandler.py +133 -205
  47. edsl/jobs/buckets/BucketCollection.py +3 -44
  48. edsl/jobs/buckets/TokenBucket.py +21 -53
  49. edsl/jobs/interviews/Interview.py +408 -143
  50. edsl/jobs/runners/JobsRunnerAsyncio.py +403 -88
  51. edsl/jobs/runners/JobsRunnerStatus.py +165 -133
  52. edsl/jobs/tasks/QuestionTaskCreator.py +19 -21
  53. edsl/jobs/tasks/TaskHistory.py +18 -38
  54. edsl/jobs/tasks/task_status_enum.py +2 -0
  55. edsl/language_models/KeyLookup.py +30 -0
  56. edsl/language_models/LanguageModel.py +236 -194
  57. edsl/language_models/ModelList.py +19 -28
  58. edsl/language_models/__init__.py +2 -1
  59. edsl/language_models/registry.py +190 -0
  60. edsl/language_models/repair.py +2 -2
  61. edsl/language_models/unused/ReplicateBase.py +83 -0
  62. edsl/language_models/utilities.py +4 -5
  63. edsl/notebooks/Notebook.py +14 -19
  64. edsl/prompts/Prompt.py +39 -29
  65. edsl/questions/{answer_validator_mixin.py → AnswerValidatorMixin.py} +2 -47
  66. edsl/questions/QuestionBase.py +214 -68
  67. edsl/questions/{question_base_gen_mixin.py → QuestionBaseGenMixin.py} +50 -57
  68. edsl/questions/QuestionBasePromptsMixin.py +3 -7
  69. edsl/questions/QuestionBudget.py +1 -1
  70. edsl/questions/QuestionCheckBox.py +3 -3
  71. edsl/questions/QuestionExtract.py +7 -5
  72. edsl/questions/QuestionFreeText.py +3 -2
  73. edsl/questions/QuestionList.py +18 -10
  74. edsl/questions/QuestionMultipleChoice.py +23 -67
  75. edsl/questions/QuestionNumerical.py +4 -2
  76. edsl/questions/QuestionRank.py +17 -7
  77. edsl/questions/{response_validator_abc.py → ResponseValidatorABC.py} +26 -40
  78. edsl/questions/SimpleAskMixin.py +3 -4
  79. edsl/questions/__init__.py +1 -2
  80. edsl/questions/derived/QuestionLinearScale.py +3 -6
  81. edsl/questions/derived/QuestionTopK.py +1 -1
  82. edsl/questions/descriptors.py +3 -17
  83. edsl/questions/question_registry.py +1 -1
  84. edsl/results/CSSParameterizer.py +1 -1
  85. edsl/results/Dataset.py +7 -170
  86. edsl/results/DatasetExportMixin.py +305 -168
  87. edsl/results/DatasetTree.py +8 -28
  88. edsl/results/Result.py +206 -298
  89. edsl/results/Results.py +131 -149
  90. edsl/results/ResultsDBMixin.py +238 -0
  91. edsl/results/ResultsExportMixin.py +0 -2
  92. edsl/results/{results_selector.py → Selector.py} +13 -23
  93. edsl/results/TableDisplay.py +171 -98
  94. edsl/results/__init__.py +1 -1
  95. edsl/scenarios/FileStore.py +239 -150
  96. edsl/scenarios/Scenario.py +193 -90
  97. edsl/scenarios/ScenarioHtmlMixin.py +3 -4
  98. edsl/scenarios/{scenario_join.py → ScenarioJoin.py} +6 -10
  99. edsl/scenarios/ScenarioList.py +244 -415
  100. edsl/scenarios/ScenarioListExportMixin.py +7 -0
  101. edsl/scenarios/ScenarioListPdfMixin.py +37 -15
  102. edsl/scenarios/__init__.py +2 -1
  103. edsl/study/ObjectEntry.py +1 -1
  104. edsl/study/SnapShot.py +1 -1
  105. edsl/study/Study.py +12 -5
  106. edsl/surveys/Rule.py +4 -5
  107. edsl/surveys/RuleCollection.py +27 -25
  108. edsl/surveys/Survey.py +791 -270
  109. edsl/surveys/SurveyCSS.py +8 -20
  110. edsl/surveys/{SurveyFlowVisualization.py → SurveyFlowVisualizationMixin.py} +9 -11
  111. edsl/surveys/__init__.py +2 -4
  112. edsl/surveys/descriptors.py +2 -6
  113. edsl/surveys/instructions/ChangeInstruction.py +2 -1
  114. edsl/surveys/instructions/Instruction.py +13 -4
  115. edsl/surveys/instructions/InstructionCollection.py +6 -11
  116. edsl/templates/error_reporting/interview_details.html +1 -1
  117. edsl/templates/error_reporting/report.html +1 -1
  118. edsl/tools/plotting.py +1 -1
  119. edsl/utilities/utilities.py +23 -35
  120. {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/METADATA +10 -12
  121. edsl-0.1.39.dev1.dist-info/RECORD +277 -0
  122. {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/WHEEL +1 -1
  123. edsl/agents/QuestionInstructionPromptBuilder.py +0 -128
  124. edsl/agents/QuestionTemplateReplacementsBuilder.py +0 -137
  125. edsl/agents/question_option_processor.py +0 -172
  126. edsl/coop/CoopFunctionsMixin.py +0 -15
  127. edsl/coop/ExpectedParrotKeyHandler.py +0 -125
  128. edsl/exceptions/inference_services.py +0 -5
  129. edsl/inference_services/AvailableModelCacheHandler.py +0 -184
  130. edsl/inference_services/AvailableModelFetcher.py +0 -215
  131. edsl/inference_services/ServiceAvailability.py +0 -135
  132. edsl/inference_services/data_structures.py +0 -134
  133. edsl/jobs/AnswerQuestionFunctionConstructor.py +0 -223
  134. edsl/jobs/FetchInvigilator.py +0 -47
  135. edsl/jobs/InterviewTaskManager.py +0 -98
  136. edsl/jobs/InterviewsConstructor.py +0 -50
  137. edsl/jobs/JobsComponentConstructor.py +0 -189
  138. edsl/jobs/JobsRemoteInferenceLogger.py +0 -239
  139. edsl/jobs/RequestTokenEstimator.py +0 -30
  140. edsl/jobs/async_interview_runner.py +0 -138
  141. edsl/jobs/buckets/TokenBucketAPI.py +0 -211
  142. edsl/jobs/buckets/TokenBucketClient.py +0 -191
  143. edsl/jobs/check_survey_scenario_compatibility.py +0 -85
  144. edsl/jobs/data_structures.py +0 -120
  145. edsl/jobs/decorators.py +0 -35
  146. edsl/jobs/jobs_status_enums.py +0 -9
  147. edsl/jobs/loggers/HTMLTableJobLogger.py +0 -304
  148. edsl/jobs/results_exceptions_handler.py +0 -98
  149. edsl/language_models/ComputeCost.py +0 -63
  150. edsl/language_models/PriceManager.py +0 -127
  151. edsl/language_models/RawResponseHandler.py +0 -106
  152. edsl/language_models/ServiceDataSources.py +0 -0
  153. edsl/language_models/key_management/KeyLookup.py +0 -63
  154. edsl/language_models/key_management/KeyLookupBuilder.py +0 -273
  155. edsl/language_models/key_management/KeyLookupCollection.py +0 -38
  156. edsl/language_models/key_management/__init__.py +0 -0
  157. edsl/language_models/key_management/models.py +0 -131
  158. edsl/language_models/model.py +0 -256
  159. edsl/notebooks/NotebookToLaTeX.py +0 -142
  160. edsl/questions/ExceptionExplainer.py +0 -77
  161. edsl/questions/HTMLQuestion.py +0 -103
  162. edsl/questions/QuestionMatrix.py +0 -265
  163. edsl/questions/data_structures.py +0 -20
  164. edsl/questions/loop_processor.py +0 -149
  165. edsl/questions/response_validator_factory.py +0 -34
  166. edsl/questions/templates/matrix/__init__.py +0 -1
  167. edsl/questions/templates/matrix/answering_instructions.jinja +0 -5
  168. edsl/questions/templates/matrix/question_presentation.jinja +0 -20
  169. edsl/results/MarkdownToDocx.py +0 -122
  170. edsl/results/MarkdownToPDF.py +0 -111
  171. edsl/results/TextEditor.py +0 -50
  172. edsl/results/file_exports.py +0 -252
  173. edsl/results/smart_objects.py +0 -96
  174. edsl/results/table_data_class.py +0 -12
  175. edsl/results/table_renderers.py +0 -118
  176. edsl/scenarios/ConstructDownloadLink.py +0 -109
  177. edsl/scenarios/DocumentChunker.py +0 -102
  178. edsl/scenarios/DocxScenario.py +0 -16
  179. edsl/scenarios/PdfExtractor.py +0 -40
  180. edsl/scenarios/directory_scanner.py +0 -96
  181. edsl/scenarios/file_methods.py +0 -85
  182. edsl/scenarios/handlers/__init__.py +0 -13
  183. edsl/scenarios/handlers/csv.py +0 -49
  184. edsl/scenarios/handlers/docx.py +0 -76
  185. edsl/scenarios/handlers/html.py +0 -37
  186. edsl/scenarios/handlers/json.py +0 -111
  187. edsl/scenarios/handlers/latex.py +0 -5
  188. edsl/scenarios/handlers/md.py +0 -51
  189. edsl/scenarios/handlers/pdf.py +0 -68
  190. edsl/scenarios/handlers/png.py +0 -39
  191. edsl/scenarios/handlers/pptx.py +0 -105
  192. edsl/scenarios/handlers/py.py +0 -294
  193. edsl/scenarios/handlers/sql.py +0 -313
  194. edsl/scenarios/handlers/sqlite.py +0 -149
  195. edsl/scenarios/handlers/txt.py +0 -33
  196. edsl/scenarios/scenario_selector.py +0 -156
  197. edsl/surveys/ConstructDAG.py +0 -92
  198. edsl/surveys/EditSurvey.py +0 -221
  199. edsl/surveys/InstructionHandler.py +0 -100
  200. edsl/surveys/MemoryManagement.py +0 -72
  201. edsl/surveys/RuleManager.py +0 -172
  202. edsl/surveys/Simulator.py +0 -75
  203. edsl/surveys/SurveyToApp.py +0 -141
  204. edsl/utilities/PrettyList.py +0 -56
  205. edsl/utilities/is_notebook.py +0 -18
  206. edsl/utilities/is_valid_variable_name.py +0 -11
  207. edsl/utilities/remove_edsl_version.py +0 -24
  208. edsl-0.1.39.dist-info/RECORD +0 -358
  209. /edsl/questions/{register_questions_meta.py → RegisterQuestionsMeta.py} +0 -0
  210. /edsl/results/{results_fetch_mixin.py → ResultsFetchMixin.py} +0 -0
  211. /edsl/results/{results_tools_mixin.py → ResultsToolsMixin.py} +0 -0
  212. {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/LICENSE +0 -0
@@ -1,47 +0,0 @@
1
- from typing import List, Dict, Any, Optional, TYPE_CHECKING
2
-
3
- if TYPE_CHECKING:
4
- from edsl.questions.QuestionBase import QuestionBase
5
- from edsl.agents.InvigilatorBase import InvigilatorBase
6
- from edsl.language_models.key_management.KeyLookup import KeyLookup
7
- from edsl.jobs.interviews.Interview import Interview
8
-
9
-
10
- class FetchInvigilator:
11
- def __init__(
12
- self,
13
- interview: "Interview",
14
- current_answers: Optional[Dict[str, Any]] = None,
15
- key_lookup: Optional["KeyLookup"] = None,
16
- ):
17
- self.interview = interview
18
- if current_answers is None:
19
- self.current_answers = self.interview.answers
20
- else:
21
- self.current_answers = current_answers
22
- self.key_lookup = key_lookup
23
-
24
- def get_invigilator(self, question: "QuestionBase") -> "InvigilatorBase":
25
- """Return an invigilator for the given question.
26
-
27
- :param question: the question to be answered
28
- :param debug: whether to use debug mode, in which case `InvigilatorDebug` is used.
29
- """
30
-
31
- invigilator = self.interview.agent.create_invigilator(
32
- question=question,
33
- scenario=self.interview.scenario,
34
- model=self.interview.model,
35
- survey=self.interview.survey,
36
- memory_plan=self.interview.survey.memory_plan,
37
- current_answers=self.current_answers, # not yet known
38
- iteration=self.interview.iteration,
39
- cache=self.interview.cache,
40
- raise_validation_errors=self.interview.raise_validation_errors,
41
- key_lookup=self.key_lookup,
42
- )
43
- """Return an invigilator for the given question."""
44
- return invigilator
45
-
46
- def __call__(self, question):
47
- return self.get_invigilator(question)
@@ -1,98 +0,0 @@
1
- from __future__ import annotations
2
- import asyncio
3
- from typing import Any, Type, List, Generator, Optional, Union, TYPE_CHECKING
4
-
5
- if TYPE_CHECKING:
6
- from edsl.questions import QuestionBase
7
- from edsl.jobs.tokens.InterviewTokenUsage import InterviewTokenUsage
8
- from edsl.jobs.interviews.InterviewStatusDictionary import InterviewStatusDictionary
9
- from edsl.jobs.interviews.InterviewStatusLog import InterviewStatusLog
10
-
11
-
12
- class InterviewTaskManager:
13
- """Handles creation and management of interview tasks."""
14
-
15
- def __init__(self, survey, iteration=0):
16
- from edsl.jobs.tasks.TaskCreators import TaskCreators
17
- from edsl.jobs.interviews.InterviewStatusLog import InterviewStatusLog
18
-
19
- self.survey = survey
20
- self.iteration = iteration
21
- self.task_creators = TaskCreators()
22
- self.to_index = {
23
- question_name: index
24
- for index, question_name in enumerate(self.survey.question_names)
25
- }
26
- self._task_status_log_dict = InterviewStatusLog()
27
-
28
- def build_question_tasks(
29
- self, answer_func, token_estimator, model_buckets
30
- ) -> list[asyncio.Task]:
31
- """Create tasks for all questions with proper dependencies."""
32
- tasks = []
33
- for question in self.survey.questions:
34
- dependencies = self._get_task_dependencies(tasks, question)
35
- task = self._create_single_task(
36
- question=question,
37
- dependencies=dependencies,
38
- answer_func=answer_func,
39
- token_estimator=token_estimator,
40
- model_buckets=model_buckets,
41
- )
42
- tasks.append(task)
43
- return tuple(tasks)
44
-
45
- def _get_task_dependencies(
46
- self, existing_tasks: list[asyncio.Task], question: "QuestionBase"
47
- ) -> list[asyncio.Task]:
48
- """Get tasks that must be completed before the given question."""
49
- dag = self.survey.dag(textify=True)
50
- parents = dag.get(question.question_name, [])
51
- return [existing_tasks[self.to_index[parent_name]] for parent_name in parents]
52
-
53
- def _create_single_task(
54
- self,
55
- question: "QuestionBase",
56
- dependencies: list[asyncio.Task],
57
- answer_func,
58
- token_estimator,
59
- model_buckets,
60
- ) -> asyncio.Task:
61
- """Create a single question task with its dependencies."""
62
- from edsl.jobs.tasks.QuestionTaskCreator import QuestionTaskCreator
63
-
64
- task_creator = QuestionTaskCreator(
65
- question=question,
66
- answer_question_func=answer_func,
67
- token_estimator=token_estimator,
68
- model_buckets=model_buckets,
69
- iteration=self.iteration,
70
- )
71
-
72
- for dependency in dependencies:
73
- task_creator.add_dependency(dependency)
74
-
75
- self.task_creators[question.question_name] = task_creator
76
- return task_creator.generate_task()
77
-
78
- @property
79
- def task_status_logs(self) -> "InterviewStatusLog":
80
- """Return the task status logs for the interview.
81
-
82
- The keys are the question names; the values are the lists of status log changes for each task.
83
- """
84
- for task_creator in self.task_creators.values():
85
- self._task_status_log_dict[
86
- task_creator.question.question_name
87
- ] = task_creator.status_log
88
- return self._task_status_log_dict
89
-
90
- @property
91
- def token_usage(self) -> "InterviewTokenUsage":
92
- """Determine how many tokens were used for the interview."""
93
- return self.task_creators.token_usage
94
-
95
- @property
96
- def interview_status(self) -> "InterviewStatusDictionary":
97
- """Return a dictionary mapping task status codes to counts."""
98
- return self.task_creators.interview_status
@@ -1,50 +0,0 @@
1
- from typing import Generator, TYPE_CHECKING
2
- from itertools import product
3
-
4
- if TYPE_CHECKING:
5
- from edsl.jobs.interviews.Interview import Interview
6
-
7
-
8
- class InterviewsConstructor:
9
- def __init__(self, jobs: "Jobs", cache: "Cache"):
10
- self.jobs = jobs
11
- self.cache = cache
12
-
13
- def create_interviews(self) -> Generator["Interview", None, None]:
14
- """
15
- Generate interviews.
16
-
17
- Note that this sets the agents, model and scenarios if they have not been set. This is a side effect of the method.
18
- This is useful because a user can create a job without setting the agents, models, or scenarios, and the job will still run,
19
- with us filling in defaults.
20
-
21
- """
22
- from edsl.jobs.interviews.Interview import Interview
23
-
24
- agent_index = {
25
- hash(agent): index for index, agent in enumerate(self.jobs.agents)
26
- }
27
- model_index = {
28
- hash(model): index for index, model in enumerate(self.jobs.models)
29
- }
30
- scenario_index = {
31
- hash(scenario): index for index, scenario in enumerate(self.jobs.scenarios)
32
- }
33
-
34
- for agent, scenario, model in product(
35
- self.jobs.agents, self.jobs.scenarios, self.jobs.models
36
- ):
37
- yield Interview(
38
- survey=self.jobs.survey.draw(),
39
- agent=agent,
40
- scenario=scenario,
41
- model=model,
42
- cache=self.cache,
43
- skip_retry=self.jobs.run_config.parameters.skip_retry,
44
- raise_validation_errors=self.jobs.run_config.parameters.raise_validation_errors,
45
- indices={
46
- "agent": agent_index[hash(agent)],
47
- "model": model_index[hash(model)],
48
- "scenario": scenario_index[hash(scenario)],
49
- },
50
- )
@@ -1,189 +0,0 @@
1
- from typing import Union, Sequence, TYPE_CHECKING
2
-
3
- if TYPE_CHECKING:
4
- from edsl.agents.Agent import Agent
5
- from edsl.language_models.LanguageModel import LanguageModel
6
- from edsl.scenarios.Scenario import Scenario
7
- from edsl.jobs.Jobs import Jobs
8
-
9
-
10
- class JobsComponentConstructor:
11
- "Handles the creation of Agents, Scenarios, and LanguageModels in a job."
12
-
13
- def __init__(self, jobs: "Jobs"):
14
- self.jobs = jobs
15
-
16
- def by(
17
- self,
18
- *args: Union[
19
- "Agent",
20
- "Scenario",
21
- "LanguageModel",
22
- Sequence[Union["Agent", "Scenario", "LanguageModel"]],
23
- ],
24
- ) -> "Jobs":
25
- """
26
- Add Agents, Scenarios and LanguageModels to a job.
27
-
28
- :param args: objects or a sequence (list, tuple, ...) of objects of the same type
29
-
30
- If no objects of this type exist in the Jobs instance, it stores the new objects as a list in the corresponding attribute.
31
- Otherwise, it combines the new objects with existing objects using the object's `__add__` method.
32
-
33
- This 'by' is intended to create a fluent interface.
34
-
35
- >>> from edsl.surveys import Survey
36
- >>> from edsl.questions import QuestionFreeText
37
- >>> q = QuestionFreeText(question_name="name", question_text="What is your name?")
38
- >>> from edsl.jobs import Jobs
39
- >>> j = Jobs(survey = Survey(questions=[q]))
40
- >>> j
41
- Jobs(survey=Survey(...), agents=AgentList([]), models=ModelList([]), scenarios=ScenarioList([]))
42
- >>> from edsl import Agent; a = Agent(traits = {"status": "Sad"})
43
- >>> j.by(a).agents
44
- AgentList([Agent(traits = {'status': 'Sad'})])
45
-
46
-
47
- Notes:
48
- - all objects must implement the 'get_value', 'set_value', and `__add__` methods
49
- - agents: traits of new agents are combined with traits of existing agents. New and existing agents should not have overlapping traits, and do not increase the # agents in the instance
50
- - scenarios: traits of new scenarios are combined with traits of old existing. New scenarios will overwrite overlapping traits, and do not increase the number of scenarios in the instance
51
- - models: new models overwrite old models.
52
- """
53
- from edsl.results.Dataset import Dataset
54
-
55
- if isinstance(
56
- args[0], Dataset
57
- ): # let the user use a Dataset as if it were a ScenarioList
58
- args = args[0].to_scenario_list()
59
-
60
- passed_objects = self._turn_args_to_list(
61
- args
62
- ) # objects can also be passed comma-separated
63
-
64
- current_objects, objects_key = self._get_current_objects_of_this_type(
65
- passed_objects[0]
66
- )
67
-
68
- if not current_objects:
69
- new_objects = passed_objects
70
- else:
71
- new_objects = self._merge_objects(passed_objects, current_objects)
72
-
73
- setattr(self.jobs, objects_key, new_objects) # update the job object
74
- return self.jobs
75
-
76
- @staticmethod
77
- def _turn_args_to_list(args):
78
- """Return a list of the first argument if it is a sequence, otherwise returns a list of all the arguments.
79
-
80
- Example:
81
-
82
- >>> JobsComponentConstructor._turn_args_to_list([1,2,3])
83
- [1, 2, 3]
84
-
85
- """
86
-
87
- def did_user_pass_a_sequence(args):
88
- """Return True if the user passed a sequence, False otherwise.
89
-
90
- Example:
91
-
92
- >>> did_user_pass_a_sequence([1,2,3])
93
- True
94
-
95
- >>> did_user_pass_a_sequence(1)
96
- False
97
- """
98
- return len(args) == 1 and isinstance(args[0], Sequence)
99
-
100
- if did_user_pass_a_sequence(args):
101
- container_class = JobsComponentConstructor._get_container_class(args[0][0])
102
- return container_class(args[0])
103
- else:
104
- container_class = JobsComponentConstructor._get_container_class(args[0])
105
- return container_class(args)
106
-
107
- def _get_current_objects_of_this_type(
108
- self, object: Union["Agent", "Scenario", "LanguageModel"]
109
- ) -> tuple[list, str]:
110
- from edsl.agents.Agent import Agent
111
- from edsl.scenarios.Scenario import Scenario
112
- from edsl.language_models.LanguageModel import LanguageModel
113
-
114
- """Return the current objects of the same type as the first argument.
115
-
116
- >>> from edsl.jobs import Jobs
117
- >>> j = JobsComponentConstructor(Jobs.example())
118
- >>> j._get_current_objects_of_this_type(j.agents[0])
119
- (AgentList([Agent(traits = {'status': 'Joyful'}), Agent(traits = {'status': 'Sad'})]), 'agents')
120
- """
121
- class_to_key = {
122
- Agent: "agents",
123
- Scenario: "scenarios",
124
- LanguageModel: "models",
125
- }
126
- for class_type in class_to_key:
127
- if isinstance(object, class_type) or issubclass(
128
- object.__class__, class_type
129
- ):
130
- key = class_to_key[class_type]
131
- break
132
- else:
133
- raise ValueError(
134
- f"First argument must be an Agent, Scenario, or LanguageModel, not {object}"
135
- )
136
- current_objects = getattr(self.jobs, key, None)
137
- return current_objects, key
138
-
139
- @staticmethod
140
- def _get_empty_container_object(object):
141
- from edsl.agents.AgentList import AgentList
142
- from edsl.scenarios.ScenarioList import ScenarioList
143
-
144
- return {"Agent": AgentList([]), "Scenario": ScenarioList([])}.get(
145
- object.__class__.__name__, []
146
- )
147
-
148
- @staticmethod
149
- def _merge_objects(passed_objects, current_objects) -> list:
150
- """
151
- Combine all the existing objects with the new objects.
152
-
153
- For example, if the user passes in 3 agents,
154
- and there are 2 existing agents, this will create 6 new agents
155
- >>> from edsl.jobs import Jobs
156
- >>> JobsComponentConstructor(Jobs(survey = []))._merge_objects([1,2,3], [4,5,6])
157
- [5, 6, 7, 6, 7, 8, 7, 8, 9]
158
- """
159
- new_objects = JobsComponentConstructor._get_empty_container_object(
160
- passed_objects[0]
161
- )
162
- for current_object in current_objects:
163
- for new_object in passed_objects:
164
- new_objects.append(current_object + new_object)
165
- return new_objects
166
-
167
- @staticmethod
168
- def _get_container_class(object):
169
- from edsl.agents.AgentList import AgentList
170
- from edsl.agents.Agent import Agent
171
- from edsl.scenarios.Scenario import Scenario
172
- from edsl.scenarios.ScenarioList import ScenarioList
173
- from edsl.language_models.ModelList import ModelList
174
-
175
- if isinstance(object, Agent):
176
- return AgentList
177
- elif isinstance(object, Scenario):
178
- return ScenarioList
179
- elif isinstance(object, ModelList):
180
- return ModelList
181
- else:
182
- return list
183
-
184
-
185
- if __name__ == "__main__":
186
- """Run the module's doctests."""
187
- import doctest
188
-
189
- doctest.testmod(optionflags=doctest.ELLIPSIS)
@@ -1,239 +0,0 @@
1
- import re
2
- import sys
3
- import uuid
4
- from abc import ABC, abstractmethod
5
- from typing import Optional, Union, Literal, TYPE_CHECKING, List, Dict
6
- from datetime import datetime
7
- from dataclasses import dataclass
8
- from edsl.exceptions.coop import CoopServerResponseError
9
-
10
- from edsl.jobs.jobs_status_enums import JobsStatus
11
-
12
- if TYPE_CHECKING:
13
- from edsl.results.Results import Results
14
-
15
-
16
- @dataclass
17
- class LogMessage:
18
- text: str
19
- status: str
20
- timestamp: datetime
21
- status: JobsStatus
22
-
23
-
24
- @dataclass
25
- class JobsInfo:
26
- job_uuid: str = None
27
- progress_bar_url: str = None
28
- error_report_url: str = None
29
- results_uuid: str = None
30
- results_url: str = None
31
-
32
- pretty_names = {
33
- "job_uuid": "Job UUID",
34
- "progress_bar_url": "Progress Bar URL",
35
- "error_report_url": "Error Report URL",
36
- "results_uuid": "Results UUID",
37
- "results_url": "Results URL",
38
- }
39
-
40
-
41
- class JobLogger(ABC):
42
- def __init__(self, verbose: bool = False):
43
- self.verbose = verbose
44
- self.jobs_info = JobsInfo()
45
-
46
- def add_info(
47
- self,
48
- information_type: Literal[
49
- "job_uuid",
50
- "progress_bar_url",
51
- "error_report_url",
52
- "results_uuid",
53
- "results_url",
54
- ],
55
- value: str,
56
- ):
57
- """Add information to the logger
58
-
59
- >>> j = StdOutJobLogger()
60
- >>> j.add_info("job_uuid", "1234")
61
- >>> j.jobs_info.job_uuid
62
- '1234'
63
- """
64
- if information_type not in self.jobs_info.__annotations__:
65
- raise ValueError(f"Information type {information_type} not supported")
66
- setattr(self.jobs_info, information_type, value)
67
-
68
- @abstractmethod
69
- def update(self, message: str, status: str = "running"):
70
- pass
71
-
72
-
73
- class HTMLTableJobLogger(JobLogger):
74
- def __init__(self, verbose=True, **kwargs):
75
- from IPython.display import display, HTML
76
-
77
- super().__init__(verbose=verbose)
78
- self.display_handle = display(HTML(""), display_id=True)
79
- self.current_message = None
80
- self.log_id = str(uuid.uuid4())
81
- self.is_expanded = True
82
- self.spinner_chars = ["◐", "◓", "◑", "◒"] # Rotating spinner characters
83
- self.spinner_idx = 0
84
-
85
- def _get_table_row(self, key: str, value: str) -> str:
86
- """Generate a table row with key-value pair"""
87
- return f"""
88
- <tr>
89
- <td style="padding: 8px; border: 1px solid #ddd; font-weight: bold;">{key}</td>
90
- <td style="padding: 8px; border: 1px solid #ddd;">{value if value else 'None'}</td>
91
- </tr>
92
- """
93
-
94
- def _linkify(self, text: str) -> str:
95
- """Convert URLs in text to clickable links"""
96
- url_pattern = r'(https?://[^\s<>"]+|www\.[^\s<>"]+)'
97
- return re.sub(
98
- url_pattern,
99
- r'<a href="\1" target="_blank" style="color: #3b82f6; text-decoration: underline;">\1</a>',
100
- text,
101
- )
102
-
103
- def _get_spinner(self, status: JobsStatus) -> str:
104
- """Get the current spinner frame if status is running"""
105
- if status == JobsStatus.RUNNING:
106
- spinner = self.spinner_chars[self.spinner_idx]
107
- self.spinner_idx = (self.spinner_idx + 1) % len(self.spinner_chars)
108
- return f'<span style="margin-right: 8px;">{spinner}</span>'
109
- elif status == JobsStatus.COMPLETED:
110
- return '<span style="margin-right: 8px; color: #22c55e;">✓</span>'
111
- elif status == JobsStatus.FAILED:
112
- return '<span style="margin-right: 8px; color: #ef4444;">✗</span>'
113
- return ""
114
-
115
- def _get_html(self, status: JobsStatus = JobsStatus.RUNNING) -> str:
116
- """Generate the complete HTML display"""
117
- # Generate table rows for each JobsInfo field
118
- info_rows = ""
119
- for field, _ in self.jobs_info.__annotations__.items():
120
- if field != "pretty_names": # Skip the pretty_names dictionary
121
- value = getattr(self.jobs_info, field)
122
- value = self._linkify(str(value)) if value else None
123
- pretty_name = self.jobs_info.pretty_names.get(
124
- field, field.replace("_", " ").title()
125
- )
126
- info_rows += self._get_table_row(pretty_name, value)
127
-
128
- # Add current message section with spinner
129
- message_html = ""
130
- if self.current_message:
131
- spinner = self._get_spinner(status)
132
- message_html = f"""
133
- <div style="margin-top: 10px; padding: 8px; background-color: #f8f9fa; border: 1px solid #ddd; border-radius: 4px;">
134
- {spinner}<strong>Current Status:</strong> {self._linkify(self.current_message)}
135
- </div>
136
- """
137
-
138
- display_style = "block" if self.is_expanded else "none"
139
- arrow = "▼" if self.is_expanded else "▶"
140
-
141
- return f"""
142
- <div style="font-family: system-ui; max-width: 800px; margin: 10px 0;">
143
- <div onclick="document.getElementById('content-{self.log_id}').style.display = document.getElementById('content-{self.log_id}').style.display === 'none' ? 'block' : 'none';
144
- document.getElementById('arrow-{self.log_id}').innerHTML = document.getElementById('content-{self.log_id}').style.display === 'none' ? '▶' : '▼';"
145
- style="padding: 10px; background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;">
146
- <span id="arrow-{self.log_id}">{arrow}</span> Job Status ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})
147
- </div>
148
- <div id="content-{self.log_id}" style="display: {display_style};">
149
- <table style="width: 100%; border-collapse: collapse; background: white; border: 1px solid #ddd;">
150
- {info_rows}
151
- </table>
152
- {message_html}
153
- </div>
154
- </div>
155
- """
156
-
157
- def update(self, message: str, status: JobsStatus = JobsStatus.RUNNING):
158
- """Update the display with new message and current JobsInfo state"""
159
- from IPython.display import HTML
160
-
161
- self.current_message = message
162
- if self.verbose:
163
- self.display_handle.update(HTML(self._get_html(status)))
164
- else:
165
- return None
166
-
167
-
168
- class StdOutJobLogger(JobLogger):
169
- def __init__(self, verbose=True, **kwargs):
170
- super().__init__(verbose=verbose) # Properly call parent's __init__
171
- self.messages: List[LogMessage] = []
172
-
173
- def update(self, message: str, status: JobsStatus = JobsStatus.RUNNING):
174
- log_msg = LogMessage(text=message, status=status, timestamp=datetime.now())
175
- self.messages.append(log_msg)
176
- if self.verbose:
177
- sys.stdout.write(f"│ {message}\n")
178
- sys.stdout.flush()
179
- else:
180
- return None
181
-
182
-
183
- class JupyterJobLogger(JobLogger):
184
- def __init__(self, verbose=True, **kwargs):
185
- from IPython.display import display, HTML
186
-
187
- super().__init__(verbose=verbose)
188
- self.messages = []
189
- self.log_id = str(uuid.uuid4())
190
- self.is_expanded = True
191
- self.display_handle = display(HTML(""), display_id=True)
192
-
193
- def _linkify(self, text):
194
- url_pattern = r'(https?://[^\s<>"]+|www\.[^\s<>"]+)'
195
- return re.sub(
196
- url_pattern,
197
- r'<a href="\1" target="_blank" style="color: #3b82f6; text-decoration: underline;">\1</a>',
198
- text,
199
- )
200
-
201
- def _get_html(self):
202
- messages_html = "\n".join(
203
- [
204
- f'<div style="border-left: 3px solid {msg["color"]}; padding: 5px 10px; margin: 5px 0;">{self._linkify(msg["text"])}</div>'
205
- for msg in self.messages
206
- ]
207
- )
208
-
209
- display_style = "block" if self.is_expanded else "none"
210
- arrow = "▼" if self.is_expanded else "▶"
211
-
212
- return f"""
213
- <div style="border: 1px solid #ccc; margin: 10px 0; max-width: 800px;">
214
- <div onclick="document.getElementById('content-{self.log_id}').style.display = document.getElementById('content-{self.log_id}').style.display === 'none' ? 'block' : 'none';
215
- document.getElementById('arrow-{self.log_id}').innerHTML = document.getElementById('content-{self.log_id}').style.display === 'none' ? '▶' : '▼';"
216
- style="padding: 10px; background: #f5f5f5; cursor: pointer;">
217
- <span id="arrow-{self.log_id}">{arrow}</span> Remote Job Log ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})
218
- </div>
219
- <div id="content-{self.log_id}" style="padding: 10px; display: {display_style};">
220
- {messages_html}
221
- </div>
222
- </div>
223
- """
224
-
225
- def update(self, message, status: JobsStatus = JobsStatus.RUNNING):
226
- from IPython.display import HTML
227
-
228
- colors = {"running": "#3b82f6", "completed": "#22c55e", "failed": "#ef4444"}
229
- self.messages.append({"text": message, "color": colors.get(status, "#666")})
230
- if self.verbose:
231
- self.display_handle.update(HTML(self._get_html()))
232
- else:
233
- return None
234
-
235
-
236
- if __name__ == "__main__":
237
- import doctest
238
-
239
- doctest.testmod()
@@ -1,30 +0,0 @@
1
- from edsl.jobs.FetchInvigilator import FetchInvigilator
2
-
3
-
4
- class RequestTokenEstimator:
5
- """Estimate the number of tokens that will be required to run the focal task."""
6
-
7
- def __init__(self, interview):
8
- self.interview = interview
9
-
10
- def __call__(self, question) -> float:
11
- """Estimate the number of tokens that will be required to run the focal task."""
12
- from edsl.scenarios.FileStore import FileStore
13
-
14
- invigilator = FetchInvigilator(self.interview)(question=question)
15
-
16
- # TODO: There should be a way to get a more accurate estimate.
17
- combined_text = ""
18
- file_tokens = 0
19
- for prompt in invigilator.get_prompts().values():
20
- if hasattr(prompt, "text"):
21
- combined_text += prompt.text
22
- elif isinstance(prompt, str):
23
- combined_text += prompt
24
- elif isinstance(prompt, list):
25
- for file in prompt:
26
- if isinstance(file, FileStore):
27
- file_tokens += file.size * 0.25
28
- else:
29
- raise ValueError(f"Prompt is of type {type(prompt)}")
30
- return len(combined_text) / 4.0 + file_tokens