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.
- edsl/Base.py +116 -197
- edsl/__init__.py +7 -15
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +147 -351
- edsl/agents/AgentList.py +73 -211
- edsl/agents/Invigilator.py +50 -101
- edsl/agents/InvigilatorBase.py +70 -62
- edsl/agents/PromptConstructor.py +225 -143
- edsl/agents/__init__.py +1 -0
- edsl/agents/prompt_helpers.py +3 -3
- edsl/auto/AutoStudy.py +5 -18
- edsl/auto/StageBase.py +40 -53
- edsl/auto/StageQuestions.py +1 -2
- edsl/auto/utilities.py +6 -0
- edsl/config.py +2 -22
- edsl/conversation/car_buying.py +1 -2
- edsl/coop/PriceFetcher.py +1 -1
- edsl/coop/coop.py +47 -125
- edsl/coop/utils.py +14 -14
- edsl/data/Cache.py +27 -45
- edsl/data/CacheEntry.py +15 -12
- edsl/data/CacheHandler.py +12 -31
- edsl/data/RemoteCacheSync.py +46 -154
- edsl/data/__init__.py +3 -4
- edsl/data_transfer_models.py +1 -2
- edsl/enums.py +0 -27
- edsl/exceptions/__init__.py +50 -50
- edsl/exceptions/agents.py +0 -12
- edsl/exceptions/questions.py +6 -24
- edsl/exceptions/scenarios.py +0 -7
- edsl/inference_services/AnthropicService.py +19 -38
- edsl/inference_services/AwsBedrock.py +2 -0
- edsl/inference_services/AzureAI.py +2 -0
- edsl/inference_services/GoogleService.py +12 -7
- edsl/inference_services/InferenceServiceABC.py +85 -18
- edsl/inference_services/InferenceServicesCollection.py +79 -120
- edsl/inference_services/MistralAIService.py +3 -0
- edsl/inference_services/OpenAIService.py +35 -47
- edsl/inference_services/PerplexityService.py +3 -0
- edsl/inference_services/TestService.py +10 -11
- edsl/inference_services/TogetherAIService.py +3 -5
- edsl/jobs/Answers.py +14 -1
- edsl/jobs/Jobs.py +431 -356
- edsl/jobs/JobsChecks.py +10 -35
- edsl/jobs/JobsPrompts.py +4 -6
- edsl/jobs/JobsRemoteInferenceHandler.py +133 -205
- edsl/jobs/buckets/BucketCollection.py +3 -44
- edsl/jobs/buckets/TokenBucket.py +21 -53
- edsl/jobs/interviews/Interview.py +408 -143
- edsl/jobs/runners/JobsRunnerAsyncio.py +403 -88
- edsl/jobs/runners/JobsRunnerStatus.py +165 -133
- edsl/jobs/tasks/QuestionTaskCreator.py +19 -21
- edsl/jobs/tasks/TaskHistory.py +18 -38
- edsl/jobs/tasks/task_status_enum.py +2 -0
- edsl/language_models/KeyLookup.py +30 -0
- edsl/language_models/LanguageModel.py +236 -194
- edsl/language_models/ModelList.py +19 -28
- edsl/language_models/__init__.py +2 -1
- edsl/language_models/registry.py +190 -0
- edsl/language_models/repair.py +2 -2
- edsl/language_models/unused/ReplicateBase.py +83 -0
- edsl/language_models/utilities.py +4 -5
- edsl/notebooks/Notebook.py +14 -19
- edsl/prompts/Prompt.py +39 -29
- edsl/questions/{answer_validator_mixin.py → AnswerValidatorMixin.py} +2 -47
- edsl/questions/QuestionBase.py +214 -68
- edsl/questions/{question_base_gen_mixin.py → QuestionBaseGenMixin.py} +50 -57
- edsl/questions/QuestionBasePromptsMixin.py +3 -7
- edsl/questions/QuestionBudget.py +1 -1
- edsl/questions/QuestionCheckBox.py +3 -3
- edsl/questions/QuestionExtract.py +7 -5
- edsl/questions/QuestionFreeText.py +3 -2
- edsl/questions/QuestionList.py +18 -10
- edsl/questions/QuestionMultipleChoice.py +23 -67
- edsl/questions/QuestionNumerical.py +4 -2
- edsl/questions/QuestionRank.py +17 -7
- edsl/questions/{response_validator_abc.py → ResponseValidatorABC.py} +26 -40
- edsl/questions/SimpleAskMixin.py +3 -4
- edsl/questions/__init__.py +1 -2
- edsl/questions/derived/QuestionLinearScale.py +3 -6
- edsl/questions/derived/QuestionTopK.py +1 -1
- edsl/questions/descriptors.py +3 -17
- edsl/questions/question_registry.py +1 -1
- edsl/results/CSSParameterizer.py +1 -1
- edsl/results/Dataset.py +7 -170
- edsl/results/DatasetExportMixin.py +305 -168
- edsl/results/DatasetTree.py +8 -28
- edsl/results/Result.py +206 -298
- edsl/results/Results.py +131 -149
- edsl/results/ResultsDBMixin.py +238 -0
- edsl/results/ResultsExportMixin.py +0 -2
- edsl/results/{results_selector.py → Selector.py} +13 -23
- edsl/results/TableDisplay.py +171 -98
- edsl/results/__init__.py +1 -1
- edsl/scenarios/FileStore.py +239 -150
- edsl/scenarios/Scenario.py +193 -90
- edsl/scenarios/ScenarioHtmlMixin.py +3 -4
- edsl/scenarios/{scenario_join.py → ScenarioJoin.py} +6 -10
- edsl/scenarios/ScenarioList.py +244 -415
- edsl/scenarios/ScenarioListExportMixin.py +7 -0
- edsl/scenarios/ScenarioListPdfMixin.py +37 -15
- edsl/scenarios/__init__.py +2 -1
- edsl/study/ObjectEntry.py +1 -1
- edsl/study/SnapShot.py +1 -1
- edsl/study/Study.py +12 -5
- edsl/surveys/Rule.py +4 -5
- edsl/surveys/RuleCollection.py +27 -25
- edsl/surveys/Survey.py +791 -270
- edsl/surveys/SurveyCSS.py +8 -20
- edsl/surveys/{SurveyFlowVisualization.py → SurveyFlowVisualizationMixin.py} +9 -11
- edsl/surveys/__init__.py +2 -4
- edsl/surveys/descriptors.py +2 -6
- edsl/surveys/instructions/ChangeInstruction.py +2 -1
- edsl/surveys/instructions/Instruction.py +13 -4
- edsl/surveys/instructions/InstructionCollection.py +6 -11
- edsl/templates/error_reporting/interview_details.html +1 -1
- edsl/templates/error_reporting/report.html +1 -1
- edsl/tools/plotting.py +1 -1
- edsl/utilities/utilities.py +23 -35
- {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/METADATA +10 -12
- edsl-0.1.39.dev1.dist-info/RECORD +277 -0
- {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/WHEEL +1 -1
- edsl/agents/QuestionInstructionPromptBuilder.py +0 -128
- edsl/agents/QuestionTemplateReplacementsBuilder.py +0 -137
- edsl/agents/question_option_processor.py +0 -172
- edsl/coop/CoopFunctionsMixin.py +0 -15
- edsl/coop/ExpectedParrotKeyHandler.py +0 -125
- edsl/exceptions/inference_services.py +0 -5
- edsl/inference_services/AvailableModelCacheHandler.py +0 -184
- edsl/inference_services/AvailableModelFetcher.py +0 -215
- edsl/inference_services/ServiceAvailability.py +0 -135
- edsl/inference_services/data_structures.py +0 -134
- edsl/jobs/AnswerQuestionFunctionConstructor.py +0 -223
- edsl/jobs/FetchInvigilator.py +0 -47
- edsl/jobs/InterviewTaskManager.py +0 -98
- edsl/jobs/InterviewsConstructor.py +0 -50
- edsl/jobs/JobsComponentConstructor.py +0 -189
- edsl/jobs/JobsRemoteInferenceLogger.py +0 -239
- edsl/jobs/RequestTokenEstimator.py +0 -30
- edsl/jobs/async_interview_runner.py +0 -138
- edsl/jobs/buckets/TokenBucketAPI.py +0 -211
- edsl/jobs/buckets/TokenBucketClient.py +0 -191
- edsl/jobs/check_survey_scenario_compatibility.py +0 -85
- edsl/jobs/data_structures.py +0 -120
- edsl/jobs/decorators.py +0 -35
- edsl/jobs/jobs_status_enums.py +0 -9
- edsl/jobs/loggers/HTMLTableJobLogger.py +0 -304
- edsl/jobs/results_exceptions_handler.py +0 -98
- edsl/language_models/ComputeCost.py +0 -63
- edsl/language_models/PriceManager.py +0 -127
- edsl/language_models/RawResponseHandler.py +0 -106
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/key_management/KeyLookup.py +0 -63
- edsl/language_models/key_management/KeyLookupBuilder.py +0 -273
- edsl/language_models/key_management/KeyLookupCollection.py +0 -38
- edsl/language_models/key_management/__init__.py +0 -0
- edsl/language_models/key_management/models.py +0 -131
- edsl/language_models/model.py +0 -256
- edsl/notebooks/NotebookToLaTeX.py +0 -142
- edsl/questions/ExceptionExplainer.py +0 -77
- edsl/questions/HTMLQuestion.py +0 -103
- edsl/questions/QuestionMatrix.py +0 -265
- edsl/questions/data_structures.py +0 -20
- edsl/questions/loop_processor.py +0 -149
- edsl/questions/response_validator_factory.py +0 -34
- edsl/questions/templates/matrix/__init__.py +0 -1
- edsl/questions/templates/matrix/answering_instructions.jinja +0 -5
- edsl/questions/templates/matrix/question_presentation.jinja +0 -20
- edsl/results/MarkdownToDocx.py +0 -122
- edsl/results/MarkdownToPDF.py +0 -111
- edsl/results/TextEditor.py +0 -50
- edsl/results/file_exports.py +0 -252
- edsl/results/smart_objects.py +0 -96
- edsl/results/table_data_class.py +0 -12
- edsl/results/table_renderers.py +0 -118
- edsl/scenarios/ConstructDownloadLink.py +0 -109
- edsl/scenarios/DocumentChunker.py +0 -102
- edsl/scenarios/DocxScenario.py +0 -16
- edsl/scenarios/PdfExtractor.py +0 -40
- edsl/scenarios/directory_scanner.py +0 -96
- edsl/scenarios/file_methods.py +0 -85
- edsl/scenarios/handlers/__init__.py +0 -13
- edsl/scenarios/handlers/csv.py +0 -49
- edsl/scenarios/handlers/docx.py +0 -76
- edsl/scenarios/handlers/html.py +0 -37
- edsl/scenarios/handlers/json.py +0 -111
- edsl/scenarios/handlers/latex.py +0 -5
- edsl/scenarios/handlers/md.py +0 -51
- edsl/scenarios/handlers/pdf.py +0 -68
- edsl/scenarios/handlers/png.py +0 -39
- edsl/scenarios/handlers/pptx.py +0 -105
- edsl/scenarios/handlers/py.py +0 -294
- edsl/scenarios/handlers/sql.py +0 -313
- edsl/scenarios/handlers/sqlite.py +0 -149
- edsl/scenarios/handlers/txt.py +0 -33
- edsl/scenarios/scenario_selector.py +0 -156
- edsl/surveys/ConstructDAG.py +0 -92
- edsl/surveys/EditSurvey.py +0 -221
- edsl/surveys/InstructionHandler.py +0 -100
- edsl/surveys/MemoryManagement.py +0 -72
- edsl/surveys/RuleManager.py +0 -172
- edsl/surveys/Simulator.py +0 -75
- edsl/surveys/SurveyToApp.py +0 -141
- edsl/utilities/PrettyList.py +0 -56
- edsl/utilities/is_notebook.py +0 -18
- edsl/utilities/is_valid_variable_name.py +0 -11
- edsl/utilities/remove_edsl_version.py +0 -24
- edsl-0.1.39.dist-info/RECORD +0 -358
- /edsl/questions/{register_questions_meta.py → RegisterQuestionsMeta.py} +0 -0
- /edsl/results/{results_fetch_mixin.py → ResultsFetchMixin.py} +0 -0
- /edsl/results/{results_tools_mixin.py → ResultsToolsMixin.py} +0 -0
- {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/LICENSE +0 -0
edsl/jobs/FetchInvigilator.py
DELETED
@@ -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
|