edsl 0.1.47__py3-none-any.whl → 0.1.48__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/__init__.py +44 -39
- edsl/__version__.py +1 -1
- edsl/agents/__init__.py +4 -2
- edsl/agents/{Agent.py → agent.py} +442 -152
- edsl/agents/{AgentList.py → agent_list.py} +220 -162
- edsl/agents/descriptors.py +46 -7
- edsl/{exceptions/agents.py → agents/exceptions.py} +3 -12
- edsl/base/__init__.py +75 -0
- edsl/base/base_class.py +1303 -0
- edsl/base/data_transfer_models.py +114 -0
- edsl/base/enums.py +215 -0
- edsl/base.py +8 -0
- edsl/buckets/__init__.py +25 -0
- edsl/buckets/bucket_collection.py +324 -0
- edsl/buckets/model_buckets.py +206 -0
- edsl/buckets/token_bucket.py +502 -0
- edsl/{jobs/buckets/TokenBucketAPI.py → buckets/token_bucket_api.py} +1 -1
- edsl/buckets/token_bucket_client.py +509 -0
- edsl/caching/__init__.py +20 -0
- edsl/caching/cache.py +814 -0
- edsl/caching/cache_entry.py +427 -0
- edsl/{data/CacheHandler.py → caching/cache_handler.py} +14 -15
- edsl/caching/exceptions.py +24 -0
- edsl/caching/orm.py +30 -0
- edsl/{data/RemoteCacheSync.py → caching/remote_cache_sync.py} +3 -3
- edsl/caching/sql_dict.py +441 -0
- edsl/config/__init__.py +8 -0
- edsl/config/config_class.py +177 -0
- edsl/config.py +4 -176
- edsl/conversation/Conversation.py +7 -7
- edsl/conversation/car_buying.py +4 -4
- edsl/conversation/chips.py +6 -6
- edsl/coop/__init__.py +25 -2
- edsl/coop/coop.py +303 -67
- edsl/coop/{ExpectedParrotKeyHandler.py → ep_key_handling.py} +86 -10
- edsl/coop/exceptions.py +62 -0
- edsl/coop/price_fetcher.py +126 -0
- edsl/coop/utils.py +89 -24
- edsl/data_transfer_models.py +5 -72
- edsl/dataset/__init__.py +10 -0
- edsl/{results/Dataset.py → dataset/dataset.py} +116 -36
- edsl/{results/DatasetExportMixin.py → dataset/dataset_operations_mixin.py} +606 -122
- edsl/{results/DatasetTree.py → dataset/dataset_tree.py} +156 -75
- edsl/{results/TableDisplay.py → dataset/display/table_display.py} +18 -7
- edsl/{results → dataset/display}/table_renderers.py +58 -2
- edsl/{results → dataset}/file_exports.py +4 -5
- edsl/{results → dataset}/smart_objects.py +2 -2
- edsl/enums.py +5 -205
- edsl/inference_services/__init__.py +5 -0
- edsl/inference_services/{AvailableModelCacheHandler.py → available_model_cache_handler.py} +2 -3
- edsl/inference_services/{AvailableModelFetcher.py → available_model_fetcher.py} +8 -14
- edsl/inference_services/data_structures.py +3 -2
- edsl/{exceptions/inference_services.py → inference_services/exceptions.py} +1 -1
- edsl/inference_services/{InferenceServiceABC.py → inference_service_abc.py} +1 -1
- edsl/inference_services/{InferenceServicesCollection.py → inference_services_collection.py} +8 -7
- edsl/inference_services/registry.py +4 -41
- edsl/inference_services/{ServiceAvailability.py → service_availability.py} +5 -25
- edsl/inference_services/services/__init__.py +31 -0
- edsl/inference_services/{AnthropicService.py → services/anthropic_service.py} +3 -3
- edsl/inference_services/{AwsBedrock.py → services/aws_bedrock.py} +2 -2
- edsl/inference_services/{AzureAI.py → services/azure_ai.py} +2 -2
- edsl/inference_services/{DeepInfraService.py → services/deep_infra_service.py} +1 -3
- edsl/inference_services/{DeepSeekService.py → services/deep_seek_service.py} +2 -4
- edsl/inference_services/{GoogleService.py → services/google_service.py} +5 -4
- edsl/inference_services/{GroqService.py → services/groq_service.py} +1 -1
- edsl/inference_services/{MistralAIService.py → services/mistral_ai_service.py} +3 -3
- edsl/inference_services/{OllamaService.py → services/ollama_service.py} +1 -7
- edsl/inference_services/{OpenAIService.py → services/open_ai_service.py} +5 -6
- edsl/inference_services/{PerplexityService.py → services/perplexity_service.py} +3 -7
- edsl/inference_services/{TestService.py → services/test_service.py} +7 -6
- edsl/inference_services/{TogetherAIService.py → services/together_ai_service.py} +2 -6
- edsl/inference_services/{XAIService.py → services/xai_service.py} +1 -1
- edsl/inference_services/write_available.py +1 -2
- edsl/instructions/__init__.py +6 -0
- edsl/{surveys/instructions/Instruction.py → instructions/instruction.py} +11 -6
- edsl/{surveys/instructions/InstructionCollection.py → instructions/instruction_collection.py} +10 -5
- edsl/{surveys/InstructionHandler.py → instructions/instruction_handler.py} +3 -3
- edsl/{jobs/interviews → interviews}/ReportErrors.py +2 -2
- edsl/interviews/__init__.py +4 -0
- edsl/{jobs/AnswerQuestionFunctionConstructor.py → interviews/answering_function.py} +45 -18
- edsl/{jobs/interviews/InterviewExceptionEntry.py → interviews/exception_tracking.py} +107 -22
- edsl/interviews/interview.py +638 -0
- edsl/{jobs/interviews/InterviewStatusDictionary.py → interviews/interview_status_dictionary.py} +21 -12
- edsl/{jobs/interviews/InterviewStatusLog.py → interviews/interview_status_log.py} +16 -7
- edsl/{jobs/InterviewTaskManager.py → interviews/interview_task_manager.py} +12 -7
- edsl/{jobs/RequestTokenEstimator.py → interviews/request_token_estimator.py} +8 -3
- edsl/{jobs/interviews/InterviewStatistic.py → interviews/statistics.py} +36 -10
- edsl/invigilators/__init__.py +38 -0
- edsl/invigilators/invigilator_base.py +477 -0
- edsl/{agents/Invigilator.py → invigilators/invigilators.py} +263 -10
- edsl/invigilators/prompt_constructor.py +476 -0
- edsl/{agents → invigilators}/prompt_helpers.py +2 -1
- edsl/{agents/QuestionInstructionPromptBuilder.py → invigilators/question_instructions_prompt_builder.py} +18 -13
- edsl/{agents → invigilators}/question_option_processor.py +96 -21
- edsl/{agents/QuestionTemplateReplacementsBuilder.py → invigilators/question_template_replacements_builder.py} +64 -12
- edsl/jobs/__init__.py +7 -1
- edsl/jobs/async_interview_runner.py +99 -35
- edsl/jobs/check_survey_scenario_compatibility.py +7 -5
- edsl/jobs/data_structures.py +153 -22
- edsl/{exceptions/jobs.py → jobs/exceptions.py} +2 -1
- edsl/jobs/{FetchInvigilator.py → fetch_invigilator.py} +4 -4
- edsl/jobs/{loggers/HTMLTableJobLogger.py → html_table_job_logger.py} +6 -2
- edsl/jobs/{Jobs.py → jobs.py} +313 -167
- edsl/jobs/{JobsChecks.py → jobs_checks.py} +15 -7
- edsl/jobs/{JobsComponentConstructor.py → jobs_component_constructor.py} +19 -17
- edsl/jobs/{InterviewsConstructor.py → jobs_interview_constructor.py} +10 -5
- edsl/jobs/jobs_pricing_estimation.py +347 -0
- edsl/jobs/{JobsRemoteInferenceLogger.py → jobs_remote_inference_logger.py} +4 -3
- edsl/jobs/jobs_runner_asyncio.py +282 -0
- edsl/jobs/{JobsRemoteInferenceHandler.py → remote_inference.py} +19 -22
- edsl/jobs/results_exceptions_handler.py +2 -2
- edsl/key_management/__init__.py +28 -0
- edsl/key_management/key_lookup.py +161 -0
- edsl/{language_models/key_management/KeyLookupBuilder.py → key_management/key_lookup_builder.py} +118 -47
- edsl/key_management/key_lookup_collection.py +82 -0
- edsl/key_management/models.py +218 -0
- edsl/language_models/__init__.py +7 -2
- edsl/language_models/{ComputeCost.py → compute_cost.py} +18 -3
- edsl/{exceptions/language_models.py → language_models/exceptions.py} +2 -1
- edsl/language_models/language_model.py +1080 -0
- edsl/language_models/model.py +10 -25
- edsl/language_models/{ModelList.py → model_list.py} +9 -14
- edsl/language_models/{RawResponseHandler.py → raw_response_handler.py} +1 -1
- edsl/language_models/{RegisterLanguageModelsMeta.py → registry.py} +1 -1
- edsl/language_models/repair.py +4 -4
- edsl/language_models/utilities.py +4 -4
- edsl/notebooks/__init__.py +3 -1
- edsl/notebooks/{Notebook.py → notebook.py} +7 -8
- edsl/prompts/__init__.py +1 -1
- edsl/{exceptions/prompts.py → prompts/exceptions.py} +3 -1
- edsl/prompts/{Prompt.py → prompt.py} +101 -95
- edsl/questions/HTMLQuestion.py +1 -1
- edsl/questions/__init__.py +154 -25
- edsl/questions/answer_validator_mixin.py +1 -1
- edsl/questions/compose_questions.py +4 -3
- edsl/questions/derived/question_likert_five.py +166 -0
- edsl/questions/derived/{QuestionLinearScale.py → question_linear_scale.py} +4 -4
- edsl/questions/derived/{QuestionTopK.py → question_top_k.py} +4 -4
- edsl/questions/derived/{QuestionYesNo.py → question_yes_no.py} +4 -5
- edsl/questions/descriptors.py +24 -30
- edsl/questions/loop_processor.py +65 -19
- edsl/questions/question_base.py +881 -0
- edsl/questions/question_base_gen_mixin.py +15 -16
- edsl/questions/{QuestionBasePromptsMixin.py → question_base_prompts_mixin.py} +2 -2
- edsl/questions/{QuestionBudget.py → question_budget.py} +3 -4
- edsl/questions/{QuestionCheckBox.py → question_check_box.py} +16 -16
- edsl/questions/{QuestionDict.py → question_dict.py} +39 -5
- edsl/questions/{QuestionExtract.py → question_extract.py} +9 -9
- edsl/questions/question_free_text.py +282 -0
- edsl/questions/{QuestionFunctional.py → question_functional.py} +6 -5
- edsl/questions/{QuestionList.py → question_list.py} +6 -7
- edsl/questions/{QuestionMatrix.py → question_matrix.py} +6 -5
- edsl/questions/{QuestionMultipleChoice.py → question_multiple_choice.py} +126 -21
- edsl/questions/{QuestionNumerical.py → question_numerical.py} +5 -5
- edsl/questions/{QuestionRank.py → question_rank.py} +6 -6
- edsl/questions/question_registry.py +4 -9
- edsl/questions/register_questions_meta.py +8 -4
- edsl/questions/response_validator_abc.py +17 -16
- edsl/results/__init__.py +4 -1
- edsl/{exceptions/results.py → results/exceptions.py} +1 -1
- edsl/results/report.py +197 -0
- edsl/results/{Result.py → result.py} +131 -45
- edsl/results/{Results.py → results.py} +365 -220
- edsl/results/results_selector.py +344 -25
- edsl/scenarios/__init__.py +30 -3
- edsl/scenarios/{ConstructDownloadLink.py → construct_download_link.py} +7 -0
- edsl/scenarios/directory_scanner.py +156 -13
- edsl/scenarios/document_chunker.py +186 -0
- edsl/scenarios/exceptions.py +101 -0
- edsl/scenarios/file_methods.py +2 -3
- edsl/scenarios/{FileStore.py → file_store.py} +275 -189
- edsl/scenarios/handlers/__init__.py +14 -14
- edsl/scenarios/handlers/{csv.py → csv_file_store.py} +1 -2
- edsl/scenarios/handlers/{docx.py → docx_file_store.py} +8 -7
- edsl/scenarios/handlers/{html.py → html_file_store.py} +1 -2
- edsl/scenarios/handlers/{jpeg.py → jpeg_file_store.py} +1 -1
- edsl/scenarios/handlers/{json.py → json_file_store.py} +1 -1
- edsl/scenarios/handlers/latex_file_store.py +5 -0
- edsl/scenarios/handlers/{md.py → md_file_store.py} +1 -1
- edsl/scenarios/handlers/{pdf.py → pdf_file_store.py} +2 -2
- edsl/scenarios/handlers/{png.py → png_file_store.py} +1 -1
- edsl/scenarios/handlers/{pptx.py → pptx_file_store.py} +8 -7
- edsl/scenarios/handlers/{py.py → py_file_store.py} +1 -3
- edsl/scenarios/handlers/{sql.py → sql_file_store.py} +2 -1
- edsl/scenarios/handlers/{sqlite.py → sqlite_file_store.py} +2 -3
- edsl/scenarios/handlers/{txt.py → txt_file_store.py} +1 -1
- edsl/scenarios/scenario.py +928 -0
- edsl/scenarios/scenario_join.py +18 -5
- edsl/scenarios/{ScenarioList.py → scenario_list.py} +294 -106
- edsl/scenarios/{ScenarioListPdfMixin.py → scenario_list_pdf_tools.py} +16 -15
- edsl/scenarios/scenario_selector.py +5 -1
- edsl/study/ObjectEntry.py +2 -2
- edsl/study/SnapShot.py +5 -5
- edsl/study/Study.py +18 -19
- edsl/study/__init__.py +6 -4
- edsl/surveys/__init__.py +7 -4
- edsl/surveys/dag/__init__.py +2 -0
- edsl/surveys/{ConstructDAG.py → dag/construct_dag.py} +3 -3
- edsl/surveys/{DAG.py → dag/dag.py} +13 -10
- edsl/surveys/descriptors.py +1 -1
- edsl/surveys/{EditSurvey.py → edit_survey.py} +9 -9
- edsl/{exceptions/surveys.py → surveys/exceptions.py} +1 -2
- edsl/surveys/memory/__init__.py +3 -0
- edsl/surveys/{MemoryPlan.py → memory/memory_plan.py} +10 -9
- edsl/surveys/rules/__init__.py +3 -0
- edsl/surveys/{Rule.py → rules/rule.py} +103 -43
- edsl/surveys/{RuleCollection.py → rules/rule_collection.py} +21 -30
- edsl/surveys/{RuleManager.py → rules/rule_manager.py} +19 -13
- edsl/surveys/survey.py +1743 -0
- edsl/surveys/{SurveyExportMixin.py → survey_export.py} +22 -27
- edsl/surveys/{SurveyFlowVisualization.py → survey_flow_visualization.py} +11 -2
- edsl/surveys/{Simulator.py → survey_simulator.py} +10 -3
- edsl/tasks/__init__.py +32 -0
- edsl/{jobs/tasks/QuestionTaskCreator.py → tasks/question_task_creator.py} +115 -57
- edsl/tasks/task_creators.py +135 -0
- edsl/{jobs/tasks/TaskHistory.py → tasks/task_history.py} +86 -47
- edsl/{jobs/tasks → tasks}/task_status_enum.py +91 -7
- edsl/tasks/task_status_log.py +85 -0
- edsl/tokens/__init__.py +2 -0
- edsl/tokens/interview_token_usage.py +53 -0
- edsl/utilities/PrettyList.py +1 -1
- edsl/utilities/SystemInfo.py +25 -22
- edsl/utilities/__init__.py +29 -21
- edsl/utilities/gcp_bucket/__init__.py +2 -0
- edsl/utilities/gcp_bucket/cloud_storage.py +99 -96
- edsl/utilities/interface.py +44 -536
- edsl/{results/MarkdownToPDF.py → utilities/markdown_to_pdf.py} +13 -5
- edsl/utilities/repair_functions.py +1 -1
- {edsl-0.1.47.dist-info → edsl-0.1.48.dist-info}/METADATA +1 -1
- edsl-0.1.48.dist-info/RECORD +347 -0
- edsl/Base.py +0 -493
- edsl/BaseDiff.py +0 -260
- edsl/agents/InvigilatorBase.py +0 -260
- edsl/agents/PromptConstructor.py +0 -318
- edsl/coop/PriceFetcher.py +0 -54
- edsl/data/Cache.py +0 -582
- edsl/data/CacheEntry.py +0 -238
- edsl/data/SQLiteDict.py +0 -292
- edsl/data/__init__.py +0 -5
- edsl/data/orm.py +0 -10
- edsl/exceptions/cache.py +0 -5
- edsl/exceptions/coop.py +0 -14
- edsl/exceptions/data.py +0 -14
- edsl/exceptions/scenarios.py +0 -29
- edsl/jobs/Answers.py +0 -43
- edsl/jobs/JobsPrompts.py +0 -354
- edsl/jobs/buckets/BucketCollection.py +0 -134
- edsl/jobs/buckets/ModelBuckets.py +0 -65
- edsl/jobs/buckets/TokenBucket.py +0 -283
- edsl/jobs/buckets/TokenBucketClient.py +0 -191
- edsl/jobs/interviews/Interview.py +0 -395
- edsl/jobs/interviews/InterviewExceptionCollection.py +0 -99
- edsl/jobs/interviews/InterviewStatisticsCollection.py +0 -25
- edsl/jobs/runners/JobsRunnerAsyncio.py +0 -163
- edsl/jobs/runners/JobsRunnerStatusData.py +0 -0
- edsl/jobs/tasks/TaskCreators.py +0 -64
- edsl/jobs/tasks/TaskStatusLog.py +0 -23
- edsl/jobs/tokens/InterviewTokenUsage.py +0 -27
- edsl/language_models/LanguageModel.py +0 -635
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/key_management/KeyLookup.py +0 -63
- edsl/language_models/key_management/KeyLookupCollection.py +0 -38
- edsl/language_models/key_management/models.py +0 -137
- edsl/questions/QuestionBase.py +0 -544
- edsl/questions/QuestionFreeText.py +0 -130
- edsl/questions/derived/QuestionLikertFive.py +0 -76
- edsl/results/ResultsExportMixin.py +0 -45
- edsl/results/TextEditor.py +0 -50
- edsl/results/results_fetch_mixin.py +0 -33
- edsl/results/results_tools_mixin.py +0 -98
- edsl/scenarios/DocumentChunker.py +0 -104
- edsl/scenarios/Scenario.py +0 -548
- edsl/scenarios/ScenarioHtmlMixin.py +0 -65
- edsl/scenarios/ScenarioListExportMixin.py +0 -45
- edsl/scenarios/handlers/latex.py +0 -5
- edsl/shared.py +0 -1
- edsl/surveys/Survey.py +0 -1301
- edsl/surveys/SurveyQualtricsImport.py +0 -284
- edsl/surveys/SurveyToApp.py +0 -141
- edsl/surveys/instructions/__init__.py +0 -0
- edsl/tools/__init__.py +0 -1
- edsl/tools/clusters.py +0 -192
- edsl/tools/embeddings.py +0 -27
- edsl/tools/embeddings_plotting.py +0 -118
- edsl/tools/plotting.py +0 -112
- edsl/tools/summarize.py +0 -18
- edsl/utilities/data/Registry.py +0 -6
- edsl/utilities/data/__init__.py +0 -1
- edsl/utilities/data/scooter_results.json +0 -1
- edsl-0.1.47.dist-info/RECORD +0 -354
- /edsl/coop/{CoopFunctionsMixin.py → coop_functions.py} +0 -0
- /edsl/{results → dataset/display}/CSSParameterizer.py +0 -0
- /edsl/{language_models/key_management → dataset/display}/__init__.py +0 -0
- /edsl/{results → dataset/display}/table_data_class.py +0 -0
- /edsl/{results → dataset/display}/table_display.css +0 -0
- /edsl/{results/ResultsGGMixin.py → dataset/r/ggplot.py} +0 -0
- /edsl/{results → dataset}/tree_explore.py +0 -0
- /edsl/{surveys/instructions/ChangeInstruction.py → instructions/change_instruction.py} +0 -0
- /edsl/{jobs/interviews → interviews}/interview_status_enum.py +0 -0
- /edsl/jobs/{runners/JobsRunnerStatus.py → jobs_runner_status.py} +0 -0
- /edsl/language_models/{PriceManager.py → price_manager.py} +0 -0
- /edsl/language_models/{fake_openai_call.py → unused/fake_openai_call.py} +0 -0
- /edsl/language_models/{fake_openai_service.py → unused/fake_openai_service.py} +0 -0
- /edsl/notebooks/{NotebookToLaTeX.py → notebook_to_latex.py} +0 -0
- /edsl/{exceptions/questions.py → questions/exceptions.py} +0 -0
- /edsl/questions/{SimpleAskMixin.py → simple_ask_mixin.py} +0 -0
- /edsl/surveys/{Memory.py → memory/memory.py} +0 -0
- /edsl/surveys/{MemoryManagement.py → memory/memory_management.py} +0 -0
- /edsl/surveys/{SurveyCSS.py → survey_css.py} +0 -0
- /edsl/{jobs/tokens/TokenUsage.py → tokens/token_usage.py} +0 -0
- /edsl/{results/MarkdownToDocx.py → utilities/markdown_to_docx.py} +0 -0
- /edsl/{TemplateLoader.py → utilities/template_loader.py} +0 -0
- {edsl-0.1.47.dist-info → edsl-0.1.48.dist-info}/LICENSE +0 -0
- {edsl-0.1.47.dist-info → edsl-0.1.48.dist-info}/WHEEL +0 -0
@@ -2,6 +2,52 @@ from jinja2 import Environment, meta
|
|
2
2
|
from typing import List, Optional, Union
|
3
3
|
|
4
4
|
|
5
|
+
def extract_template_variables(ast) -> List[Union[str, tuple]]:
|
6
|
+
"""
|
7
|
+
Extract variable expressions from a Jinja2 AST.
|
8
|
+
|
9
|
+
Args:
|
10
|
+
ast: Jinja2 AST
|
11
|
+
|
12
|
+
Returns:
|
13
|
+
List[Union[str, tuple]]: List of variable names or tuples for dotted paths
|
14
|
+
"""
|
15
|
+
from jinja2 import nodes
|
16
|
+
from jinja2.visitor import NodeVisitor
|
17
|
+
|
18
|
+
variables = []
|
19
|
+
|
20
|
+
class VariableVisitor(NodeVisitor):
|
21
|
+
def visit_Name(self, node):
|
22
|
+
variables.append(node.name)
|
23
|
+
|
24
|
+
def visit_Getattr(self, node):
|
25
|
+
# For dotted access like scenario.question_options
|
26
|
+
parts = []
|
27
|
+
current = node
|
28
|
+
|
29
|
+
# Handle the leaf attribute
|
30
|
+
parts.append(node.attr)
|
31
|
+
|
32
|
+
# Walk up the chain to collect all parts
|
33
|
+
while isinstance(current.node, nodes.Getattr):
|
34
|
+
current = current.node
|
35
|
+
parts.append(current.attr)
|
36
|
+
|
37
|
+
# Add the root name
|
38
|
+
if isinstance(current.node, nodes.Name):
|
39
|
+
parts.append(current.node.name)
|
40
|
+
|
41
|
+
# Reverse to get the correct order
|
42
|
+
parts.reverse()
|
43
|
+
variables.append(tuple(parts))
|
44
|
+
|
45
|
+
visitor = VariableVisitor()
|
46
|
+
visitor.visit(ast)
|
47
|
+
|
48
|
+
return variables
|
49
|
+
|
50
|
+
|
5
51
|
class QuestionOptionProcessor:
|
6
52
|
"""
|
7
53
|
Class that manages the processing of question options.
|
@@ -16,7 +62,11 @@ class QuestionOptionProcessor:
|
|
16
62
|
return cls(scenario, prior_answers_dict)
|
17
63
|
|
18
64
|
def __init__(self, scenario: 'Scenario', prior_answers_dict: dict):
|
19
|
-
|
65
|
+
# This handles cases where the question has {{ scenario.key }} - eventually
|
66
|
+
# we might not allow 'naked' scenario keys w/o the scenario prefix
|
67
|
+
#new_scenario = scenario.copy()
|
68
|
+
#new_scenario.update({'scenario': new_scenario})
|
69
|
+
self.scenario = scenario
|
20
70
|
self.prior_answers_dict = prior_answers_dict
|
21
71
|
|
22
72
|
@staticmethod
|
@@ -25,18 +75,23 @@ class QuestionOptionProcessor:
|
|
25
75
|
return [f"<< Option {i} - Placeholder >>" for i in range(1, 4)]
|
26
76
|
|
27
77
|
@staticmethod
|
28
|
-
def _parse_template_variable(template_str: str) -> str:
|
78
|
+
def _parse_template_variable(template_str: str) -> Union[str, tuple]:
|
29
79
|
"""
|
30
80
|
Extract the variable name from a template string.
|
81
|
+
If the variable contains dots (e.g., scenario.question_options),
|
82
|
+
returns a tuple of the path components.
|
31
83
|
|
32
84
|
Args:
|
33
85
|
template_str (str): Jinja template string
|
34
86
|
|
35
87
|
Returns:
|
36
|
-
str: Name of the first undefined variable in the template
|
88
|
+
Union[str, tuple]: Name of the first undefined variable in the template,
|
89
|
+
or a tuple of path components if the variable contains dots
|
37
90
|
|
38
91
|
>>> QuestionOptionProcessor._parse_template_variable("Here are some {{ options }}")
|
39
92
|
'options'
|
93
|
+
>>> QuestionOptionProcessor._parse_template_variable("Here are some {{ scenario.question_options }}")
|
94
|
+
('scenario', 'question_options')
|
40
95
|
>>> QuestionOptionProcessor._parse_template_variable("Here are some {{ options }} and {{ other }}")
|
41
96
|
Traceback (most recent call last):
|
42
97
|
...
|
@@ -48,11 +103,13 @@ class QuestionOptionProcessor:
|
|
48
103
|
"""
|
49
104
|
env = Environment()
|
50
105
|
parsed_content = env.parse(template_str)
|
51
|
-
undeclared_variables =
|
106
|
+
undeclared_variables = extract_template_variables(parsed_content)
|
107
|
+
|
52
108
|
if not undeclared_variables:
|
53
109
|
raise ValueError("No variables found in template string")
|
54
110
|
if len(undeclared_variables) > 1:
|
55
111
|
raise ValueError("Multiple variables found in template string")
|
112
|
+
|
56
113
|
return undeclared_variables[0]
|
57
114
|
|
58
115
|
@staticmethod
|
@@ -128,7 +185,7 @@ class QuestionOptionProcessor:
|
|
128
185
|
|
129
186
|
The case where options are provided as a template string:
|
130
187
|
|
131
|
-
>>> question_data = {"question_options": "{{ options }}"}
|
188
|
+
>>> question_data = {"question_options": "{{ scenario.options }}"}
|
132
189
|
>>> processor.get_question_options(question_data)
|
133
190
|
['Option 1', 'Option 2']
|
134
191
|
|
@@ -140,7 +197,7 @@ class QuestionOptionProcessor:
|
|
140
197
|
>>> q0.answer = ["Option 1", "Option 2"]
|
141
198
|
>>> mpc.prior_answers_dict = lambda: {'q0': q0}
|
142
199
|
>>> processor = QuestionOptionProcessor.from_prompt_constructor(mpc)
|
143
|
-
>>> question_data = {"question_options": "{{ q0 }}"}
|
200
|
+
>>> question_data = {"question_options": "{{ q0.answer }}"}
|
144
201
|
>>> processor.get_question_options(question_data)
|
145
202
|
['Option 1', 'Option 2']
|
146
203
|
|
@@ -156,21 +213,39 @@ class QuestionOptionProcessor:
|
|
156
213
|
return options_entry if options_entry else self._get_default_options()
|
157
214
|
|
158
215
|
# Parse template to get variable name
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
216
|
+
raw_option_key = self._parse_template_variable(options_entry)
|
217
|
+
|
218
|
+
source_type = None
|
219
|
+
|
220
|
+
if isinstance(raw_option_key, tuple):
|
221
|
+
if raw_option_key[0] == 'scenario':
|
222
|
+
source_type = 'scenario'
|
223
|
+
option_key = raw_option_key[-1]
|
224
|
+
else:
|
225
|
+
source_type = 'prior_answers'
|
226
|
+
option_key = raw_option_key[0]
|
227
|
+
#breakpoint()
|
228
|
+
else:
|
229
|
+
option_key = raw_option_key
|
230
|
+
|
231
|
+
#breakpoint()
|
232
|
+
|
233
|
+
if source_type == 'scenario':
|
234
|
+
# Try getting options from scenario
|
235
|
+
scenario_options = self._get_options_from_scenario(
|
236
|
+
self.scenario, option_key
|
237
|
+
)
|
238
|
+
if scenario_options:
|
239
|
+
return scenario_options
|
240
|
+
|
241
|
+
if source_type == 'prior_answers':
|
242
|
+
|
243
|
+
# Try getting options from prior answers
|
244
|
+
prior_answer_options = self._get_options_from_prior_answers(
|
245
|
+
self.prior_answers_dict, option_key
|
246
|
+
)
|
247
|
+
if prior_answer_options:
|
248
|
+
return prior_answer_options
|
174
249
|
|
175
250
|
return self._get_default_options()
|
176
251
|
|
@@ -1,11 +1,12 @@
|
|
1
1
|
from jinja2 import Environment, meta, TemplateSyntaxError
|
2
2
|
from typing import Any, Set, TYPE_CHECKING
|
3
3
|
|
4
|
+
from ..agents import Agent
|
5
|
+
from ..scenarios import Scenario
|
6
|
+
|
4
7
|
if TYPE_CHECKING:
|
5
|
-
from
|
6
|
-
from
|
7
|
-
from edsl.questions.QuestionBase import QuestionBase
|
8
|
-
from edsl.agents.Agent import Agent
|
8
|
+
from .prompt_constructor import PromptConstructor
|
9
|
+
from ..questions import QuestionBase
|
9
10
|
|
10
11
|
|
11
12
|
class QuestionTemplateReplacementsBuilder:
|
@@ -33,17 +34,29 @@ class QuestionTemplateReplacementsBuilder:
|
|
33
34
|
|
34
35
|
def question_file_keys(self):
|
35
36
|
"""
|
36
|
-
>>> from
|
37
|
+
>>> from ..questions import QuestionMultipleChoice
|
38
|
+
>>> from ..scenarios import Scenario
|
37
39
|
>>> q = QuestionMultipleChoice(question_text="Do you like school?", question_name = "q0", question_options = ["yes", "no"])
|
38
40
|
>>> qtrb = QuestionTemplateReplacementsBuilder(scenario = {"file1": "file1"}, question = q, prior_answers_dict = {'q0': 'q0'}, agent = "agent")
|
39
41
|
>>> qtrb.question_file_keys()
|
40
42
|
[]
|
41
|
-
>>> from
|
43
|
+
>>> from ..scenarios import FileStore
|
42
44
|
>>> fs = FileStore.example()
|
45
|
+
>>> # Test direct key reference
|
43
46
|
>>> q = QuestionMultipleChoice(question_text="What do you think of this file: {{ file1 }}", question_name = "q0", question_options = ["good", "bad"])
|
44
47
|
>>> qtrb = QuestionTemplateReplacementsBuilder(scenario = Scenario({"file1": fs}), question = q, prior_answers_dict = {'q0': 'q0'}, agent = "agent")
|
45
48
|
>>> qtrb.question_file_keys()
|
46
49
|
['file1']
|
50
|
+
>>> # Test scenario.key reference
|
51
|
+
>>> q = QuestionMultipleChoice(question_text="What do you think of this file: {{ scenario.file2 }}", question_name = "q0", question_options = ["good", "bad"])
|
52
|
+
>>> qtrb = QuestionTemplateReplacementsBuilder(scenario = Scenario({"file2": fs}), question = q, prior_answers_dict = {'q0': 'q0'}, agent = "agent")
|
53
|
+
>>> qtrb.question_file_keys()
|
54
|
+
['file2']
|
55
|
+
>>> # Test both formats in the same question
|
56
|
+
>>> q = QuestionMultipleChoice(question_text="Compare {{ file1 }} with {{ scenario.file2 }}", question_name = "q0", question_options = ["good", "bad"])
|
57
|
+
>>> qtrb = QuestionTemplateReplacementsBuilder(scenario = Scenario({"file1": fs, "file2": fs}), question = q, prior_answers_dict = {'q0': 'q0'}, agent = "agent")
|
58
|
+
>>> sorted(qtrb.question_file_keys())
|
59
|
+
['file1', 'file2']
|
47
60
|
"""
|
48
61
|
question_text = self.question.question_text
|
49
62
|
file_keys = self._find_file_keys(self.scenario)
|
@@ -76,8 +89,8 @@ class QuestionTemplateReplacementsBuilder:
|
|
76
89
|
"""We need to find all the keys in the scenario that refer to FileStore objects.
|
77
90
|
These will be used to append to the prompt a list of files that are part of the scenario.
|
78
91
|
|
79
|
-
>>> from
|
80
|
-
>>> from
|
92
|
+
>>> from ..scenarios import Scenario
|
93
|
+
>>> from ..scenarios import FileStore
|
81
94
|
>>> import tempfile
|
82
95
|
>>> with tempfile.NamedTemporaryFile() as f:
|
83
96
|
... _ = f.write(b"Hello, world!")
|
@@ -87,7 +100,7 @@ class QuestionTemplateReplacementsBuilder:
|
|
87
100
|
... QuestionTemplateReplacementsBuilder._find_file_keys(scenario)
|
88
101
|
['fs_file']
|
89
102
|
"""
|
90
|
-
from
|
103
|
+
from ..scenarios import FileStore
|
91
104
|
|
92
105
|
file_entries = []
|
93
106
|
for key, value in scenario.items():
|
@@ -100,10 +113,11 @@ class QuestionTemplateReplacementsBuilder:
|
|
100
113
|
question_text: str, scenario_file_keys: list
|
101
114
|
) -> list:
|
102
115
|
"""
|
103
|
-
Extracts the file keys from a question text
|
116
|
+
Extracts the file keys from a question text, handling both direct references ({{ file_key }})
|
117
|
+
and scenario-prefixed references ({{ scenario.file_key }}).
|
104
118
|
|
105
119
|
>>> from edsl import Scenario
|
106
|
-
>>> from edsl.scenarios
|
120
|
+
>>> from edsl.scenarios import FileStore
|
107
121
|
>>> import tempfile
|
108
122
|
>>> with tempfile.NamedTemporaryFile() as f:
|
109
123
|
... _ = f.write(b"Hello, world!")
|
@@ -112,15 +126,53 @@ class QuestionTemplateReplacementsBuilder:
|
|
112
126
|
... scenario = Scenario({"fs_file": fs, 'a': 1})
|
113
127
|
... QuestionTemplateReplacementsBuilder._extract_file_keys_from_question_text("{{ fs_file }}", ['fs_file'])
|
114
128
|
['fs_file']
|
129
|
+
>>> with tempfile.NamedTemporaryFile() as f:
|
130
|
+
... _ = f.write(b"Hello, world!")
|
131
|
+
... _ = f.seek(0)
|
132
|
+
... fs = FileStore(f.name)
|
133
|
+
... scenario = Scenario({"print": fs, 'a': 1})
|
134
|
+
... QuestionTemplateReplacementsBuilder._extract_file_keys_from_question_text("{{ scenario.print }}", ['print'])
|
135
|
+
['print']
|
136
|
+
>>> with tempfile.NamedTemporaryFile() as f:
|
137
|
+
... _ = f.write(b"Hello, world!")
|
138
|
+
... _ = f.seek(0)
|
139
|
+
... fs = FileStore(f.name)
|
140
|
+
... scenario = Scenario({"file1": fs, "file2": fs})
|
141
|
+
... sorted(QuestionTemplateReplacementsBuilder._extract_file_keys_from_question_text("Compare {{ file1 }} with {{ scenario.file2 }}", ['file1', 'file2']))
|
142
|
+
['file1', 'file2']
|
115
143
|
"""
|
116
144
|
variables = QuestionTemplateReplacementsBuilder.get_jinja2_variables(
|
117
145
|
question_text
|
118
146
|
)
|
119
147
|
question_file_keys = []
|
148
|
+
|
149
|
+
# Direct references: {{ file_key }}
|
120
150
|
for var in variables:
|
121
151
|
if var in scenario_file_keys:
|
122
152
|
question_file_keys.append(var)
|
123
|
-
|
153
|
+
|
154
|
+
# Scenario-prefixed references: {{ scenario.file_key }}
|
155
|
+
for var in variables:
|
156
|
+
if var == "scenario":
|
157
|
+
# If we find a scenario variable, we need to check for nested references
|
158
|
+
# Create a modified template with just {{ scenario.* }} expressions to isolate them
|
159
|
+
scenario_template = "".join([
|
160
|
+
"{% for key, value in scenario.items() %}{{ key }}{% endfor %}"
|
161
|
+
])
|
162
|
+
try:
|
163
|
+
# This is a check to make sure there's scenario.something syntax in the template
|
164
|
+
if "scenario." in question_text:
|
165
|
+
# Extract dot-notation scenario references by parsing the template
|
166
|
+
import re
|
167
|
+
scenario_refs = re.findall(r'{{\s*scenario\.(\w+)\s*}}', question_text)
|
168
|
+
for key in scenario_refs:
|
169
|
+
if key in scenario_file_keys:
|
170
|
+
question_file_keys.append(key)
|
171
|
+
except:
|
172
|
+
# If there's any issue parsing, just continue with what we have
|
173
|
+
pass
|
174
|
+
|
175
|
+
return list(set(question_file_keys)) # Remove duplicates
|
124
176
|
|
125
177
|
def _scenario_replacements(
|
126
178
|
self, replacement_string: str = "<see file {key}>"
|
edsl/jobs/__init__.py
CHANGED
@@ -1,43 +1,94 @@
|
|
1
|
+
"""
|
2
|
+
Asynchronous interview runner module for conducting interviews concurrently.
|
3
|
+
|
4
|
+
This module provides functionality to run multiple interviews in parallel
|
5
|
+
with controlled concurrency, supporting both error handling and result collection.
|
6
|
+
"""
|
7
|
+
|
1
8
|
from collections.abc import AsyncGenerator
|
2
|
-
from typing import List,
|
9
|
+
from typing import List, Generator, Tuple, TYPE_CHECKING
|
3
10
|
from dataclasses import dataclass
|
4
11
|
import asyncio
|
5
|
-
from contextlib import asynccontextmanager
|
6
12
|
from edsl.data_transfer_models import EDSLResultObjectInput
|
7
13
|
|
8
|
-
from
|
9
|
-
from
|
10
|
-
from
|
14
|
+
from ..results import Result
|
15
|
+
from ..interviews import Interview
|
16
|
+
from ..config import Config
|
11
17
|
config = Config()
|
12
18
|
|
13
19
|
if TYPE_CHECKING:
|
14
|
-
from
|
20
|
+
from ..jobs import Jobs
|
15
21
|
|
22
|
+
from .data_structures import RunConfig
|
16
23
|
|
17
24
|
@dataclass
|
18
25
|
class InterviewResult:
|
26
|
+
"""Container for the result of an interview along with metadata.
|
27
|
+
|
28
|
+
Attributes:
|
29
|
+
result: The Result object containing the interview answers
|
30
|
+
interview: The Interview object used to conduct the interview
|
31
|
+
order: The original position of this interview in the processing queue
|
32
|
+
"""
|
19
33
|
result: Result
|
20
34
|
interview: Interview
|
21
35
|
order: int
|
22
36
|
|
23
37
|
|
24
|
-
from edsl.jobs.data_structures import RunConfig
|
25
|
-
|
26
|
-
|
27
38
|
class AsyncInterviewRunner:
|
39
|
+
"""
|
40
|
+
Runs interviews asynchronously with controlled concurrency.
|
41
|
+
|
42
|
+
This class manages the parallel execution of multiple interviews while
|
43
|
+
respecting concurrency limits and handling errors appropriately.
|
44
|
+
|
45
|
+
Examples:
|
46
|
+
>>> from unittest.mock import MagicMock, AsyncMock
|
47
|
+
>>> mock_jobs = MagicMock()
|
48
|
+
>>> mock_run_config = MagicMock()
|
49
|
+
>>> mock_run_config.parameters.n = 1
|
50
|
+
>>> mock_run_config.environment.cache = None
|
51
|
+
>>> runner = AsyncInterviewRunner(mock_jobs, mock_run_config)
|
52
|
+
>>> isinstance(runner._initialized, asyncio.Event)
|
53
|
+
True
|
54
|
+
"""
|
55
|
+
|
28
56
|
MAX_CONCURRENT = int(config.EDSL_MAX_CONCURRENT_TASKS)
|
29
57
|
|
30
58
|
def __init__(self, jobs: "Jobs", run_config: RunConfig):
|
59
|
+
"""
|
60
|
+
Initialize the AsyncInterviewRunner.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
jobs: The Jobs object that generates interviews
|
64
|
+
run_config: Configuration for running the interviews
|
65
|
+
"""
|
31
66
|
self.jobs = jobs
|
32
67
|
self.run_config = run_config
|
33
68
|
self._initialized = asyncio.Event()
|
34
69
|
|
35
70
|
def _expand_interviews(self) -> Generator["Interview", None, None]:
|
36
|
-
"""
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
71
|
+
"""
|
72
|
+
Create multiple copies of each interview based on the run configuration.
|
73
|
+
|
74
|
+
This method expands interviews for repeated runs and ensures each has
|
75
|
+
the proper cache configuration.
|
76
|
+
|
77
|
+
Yields:
|
78
|
+
Interview objects ready to be conducted
|
79
|
+
|
80
|
+
Examples:
|
81
|
+
>>> from unittest.mock import MagicMock
|
82
|
+
>>> mock_jobs = MagicMock()
|
83
|
+
>>> mock_interview = MagicMock()
|
84
|
+
>>> mock_jobs.generate_interviews.return_value = [mock_interview]
|
85
|
+
>>> mock_run_config = MagicMock()
|
86
|
+
>>> mock_run_config.parameters.n = 2
|
87
|
+
>>> mock_run_config.environment.cache = "mock_cache"
|
88
|
+
>>> runner = AsyncInterviewRunner(mock_jobs, mock_run_config)
|
89
|
+
>>> interviews = list(runner._expand_interviews())
|
90
|
+
>>> len(interviews)
|
91
|
+
2
|
41
92
|
"""
|
42
93
|
for interview in self.jobs.generate_interviews():
|
43
94
|
for iteration in range(self.run_config.parameters.n):
|
@@ -52,21 +103,22 @@ class AsyncInterviewRunner:
|
|
52
103
|
async def _conduct_interview(
|
53
104
|
self, interview: "Interview"
|
54
105
|
) -> Tuple["Result", "Interview"]:
|
55
|
-
"""Conducts an interview and returns the result object, along with the associated interview.
|
56
|
-
|
57
|
-
We return the interview because it is not populated with exceptions, if any.
|
58
|
-
|
59
|
-
:param interview: the interview to conduct
|
60
|
-
:return: the result of the interview
|
61
|
-
|
62
|
-
'extracted_answers' is a dictionary of the answers to the questions in the interview.
|
63
|
-
This is not the same as the generated_tokens---it can include substantial cleaning and processing / validation.
|
64
106
|
"""
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
107
|
+
Asynchronously conduct a single interview.
|
108
|
+
|
109
|
+
This method performs the interview and creates a Result object with
|
110
|
+
the extracted answers and model responses.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
interview: The interview to conduct
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
Tuple containing the Result object and the Interview object
|
117
|
+
|
118
|
+
Notes:
|
119
|
+
'extracted_answers' contains the processed and validated answers
|
120
|
+
from the interview, which may differ from the raw model output.
|
121
|
+
"""
|
70
122
|
extracted_answers: dict[str, str]
|
71
123
|
model_response_objects: List[EDSLResultObjectInput]
|
72
124
|
|
@@ -83,10 +135,20 @@ class AsyncInterviewRunner:
|
|
83
135
|
async def run(
|
84
136
|
self,
|
85
137
|
) -> AsyncGenerator[tuple[Result, Interview], None]:
|
86
|
-
"""
|
87
|
-
|
88
|
-
|
89
|
-
|
138
|
+
"""
|
139
|
+
Run all interviews asynchronously and yield results as they complete.
|
140
|
+
|
141
|
+
This method processes interviews in chunks based on MAX_CONCURRENT,
|
142
|
+
maintaining controlled concurrency while yielding results as soon as
|
143
|
+
they become available.
|
144
|
+
|
145
|
+
Yields:
|
146
|
+
Tuples of (Result, Interview) as interviews complete
|
147
|
+
|
148
|
+
Notes:
|
149
|
+
- Uses structured concurrency patterns for proper resource management
|
150
|
+
- Handles exceptions according to the run configuration
|
151
|
+
- Ensures task cleanup even in case of failures
|
90
152
|
"""
|
91
153
|
interviews = list(self._expand_interviews())
|
92
154
|
self._initialized.set()
|
@@ -102,10 +164,8 @@ class AsyncInterviewRunner:
|
|
102
164
|
result.order = idx
|
103
165
|
return InterviewResult(result, interview, idx)
|
104
166
|
except Exception as e:
|
105
|
-
# breakpoint()
|
106
167
|
if self.run_config.parameters.stop_on_exception:
|
107
168
|
raise
|
108
|
-
# logger.error(f"Task failed with error: {e}")
|
109
169
|
return None
|
110
170
|
|
111
171
|
# Process interviews in chunks
|
@@ -130,7 +190,6 @@ class AsyncInterviewRunner:
|
|
130
190
|
except Exception as e:
|
131
191
|
if self.run_config.parameters.stop_on_exception:
|
132
192
|
raise
|
133
|
-
# logger.error(f"Chunk processing failed with error: {e}")
|
134
193
|
continue
|
135
194
|
|
136
195
|
finally:
|
@@ -138,3 +197,8 @@ class AsyncInterviewRunner:
|
|
138
197
|
for task in tasks:
|
139
198
|
if not task.done():
|
140
199
|
task.cancel()
|
200
|
+
|
201
|
+
|
202
|
+
if __name__ == "__main__":
|
203
|
+
import doctest
|
204
|
+
doctest.testmod()
|
@@ -1,5 +1,7 @@
|
|
1
1
|
import warnings
|
2
2
|
from typing import TYPE_CHECKING
|
3
|
+
from edsl.scenarios import ScenarioList
|
4
|
+
from edsl.surveys import Survey
|
3
5
|
|
4
6
|
if TYPE_CHECKING:
|
5
7
|
from edsl.surveys.Survey import Survey
|
@@ -15,10 +17,10 @@ class CheckSurveyScenarioCompatibility:
|
|
15
17
|
def check(self, strict: bool = False, warn: bool = False) -> None:
|
16
18
|
"""Check if the parameters in the survey and scenarios are consistent.
|
17
19
|
|
18
|
-
>>> from edsl.jobs
|
19
|
-
>>> from edsl.questions
|
20
|
-
>>> from edsl.surveys
|
21
|
-
>>> from edsl.scenarios
|
20
|
+
>>> from edsl.jobs import Jobs
|
21
|
+
>>> from edsl.questions import QuestionFreeText
|
22
|
+
>>> from edsl.surveys import Survey
|
23
|
+
>>> from edsl.scenarios import Scenario
|
22
24
|
>>> q = QuestionFreeText(question_text = "{{poo}}", question_name = "ugly_question")
|
23
25
|
>>> j = Jobs(survey = Survey(questions=[q]))
|
24
26
|
>>> cs = CheckSurveyScenarioCompatibility(j.survey, j.scenarios)
|
@@ -39,7 +41,7 @@ class CheckSurveyScenarioCompatibility:
|
|
39
41
|
|
40
42
|
>>> q = QuestionFreeText(question_text = "Hello", question_name = "ugly_question")
|
41
43
|
>>> s = Scenario({'ugly_question': "B"})
|
42
|
-
>>> from edsl.scenarios
|
44
|
+
>>> from edsl.scenarios import ScenarioList
|
43
45
|
>>> cs = CheckSurveyScenarioCompatibility(Survey(questions=[q]), ScenarioList([s]))
|
44
46
|
>>> cs.check()
|
45
47
|
Traceback (most recent call last):
|