edsl 0.1.36.dev6__py3-none-any.whl → 0.1.37.dev2__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 +303 -303
- edsl/BaseDiff.py +260 -260
- edsl/TemplateLoader.py +24 -24
- edsl/__init__.py +48 -47
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +804 -804
- edsl/agents/AgentList.py +345 -337
- edsl/agents/Invigilator.py +222 -222
- edsl/agents/InvigilatorBase.py +305 -294
- edsl/agents/PromptConstructor.py +312 -312
- edsl/agents/__init__.py +3 -3
- edsl/agents/descriptors.py +86 -86
- edsl/agents/prompt_helpers.py +129 -129
- edsl/auto/AutoStudy.py +117 -117
- edsl/auto/StageBase.py +230 -230
- edsl/auto/StageGenerateSurvey.py +178 -178
- edsl/auto/StageLabelQuestions.py +125 -125
- edsl/auto/StagePersona.py +61 -61
- edsl/auto/StagePersonaDimensionValueRanges.py +88 -88
- edsl/auto/StagePersonaDimensionValues.py +74 -74
- edsl/auto/StagePersonaDimensions.py +69 -69
- edsl/auto/StageQuestions.py +73 -73
- edsl/auto/SurveyCreatorPipeline.py +21 -21
- edsl/auto/utilities.py +224 -224
- edsl/base/Base.py +289 -289
- edsl/config.py +149 -149
- edsl/conjure/AgentConstructionMixin.py +152 -152
- edsl/conjure/Conjure.py +62 -62
- edsl/conjure/InputData.py +659 -659
- edsl/conjure/InputDataCSV.py +48 -48
- edsl/conjure/InputDataMixinQuestionStats.py +182 -182
- edsl/conjure/InputDataPyRead.py +91 -91
- edsl/conjure/InputDataSPSS.py +8 -8
- edsl/conjure/InputDataStata.py +8 -8
- edsl/conjure/QuestionOptionMixin.py +76 -76
- edsl/conjure/QuestionTypeMixin.py +23 -23
- edsl/conjure/RawQuestion.py +65 -65
- edsl/conjure/SurveyResponses.py +7 -7
- edsl/conjure/__init__.py +9 -9
- edsl/conjure/naming_utilities.py +263 -263
- edsl/conjure/utilities.py +201 -201
- edsl/conversation/Conversation.py +238 -238
- edsl/conversation/car_buying.py +58 -58
- edsl/conversation/mug_negotiation.py +81 -81
- edsl/conversation/next_speaker_utilities.py +93 -93
- edsl/coop/PriceFetcher.py +54 -54
- edsl/coop/__init__.py +2 -2
- edsl/coop/coop.py +824 -849
- edsl/coop/utils.py +131 -131
- edsl/data/Cache.py +527 -527
- edsl/data/CacheEntry.py +228 -228
- edsl/data/CacheHandler.py +149 -149
- edsl/data/RemoteCacheSync.py +97 -84
- edsl/data/SQLiteDict.py +292 -292
- edsl/data/__init__.py +4 -4
- edsl/data/orm.py +10 -10
- edsl/data_transfer_models.py +73 -73
- edsl/enums.py +173 -173
- edsl/exceptions/__init__.py +50 -50
- edsl/exceptions/agents.py +40 -40
- edsl/exceptions/configuration.py +16 -16
- edsl/exceptions/coop.py +10 -10
- edsl/exceptions/data.py +14 -14
- edsl/exceptions/general.py +34 -34
- edsl/exceptions/jobs.py +33 -33
- edsl/exceptions/language_models.py +63 -63
- edsl/exceptions/prompts.py +15 -15
- edsl/exceptions/questions.py +91 -91
- edsl/exceptions/results.py +26 -26
- edsl/exceptions/surveys.py +34 -34
- edsl/inference_services/AnthropicService.py +87 -87
- edsl/inference_services/AwsBedrock.py +115 -115
- edsl/inference_services/AzureAI.py +217 -217
- edsl/inference_services/DeepInfraService.py +18 -18
- edsl/inference_services/GoogleService.py +156 -156
- edsl/inference_services/GroqService.py +20 -20
- edsl/inference_services/InferenceServiceABC.py +147 -147
- edsl/inference_services/InferenceServicesCollection.py +74 -72
- edsl/inference_services/MistralAIService.py +123 -123
- edsl/inference_services/OllamaService.py +18 -18
- edsl/inference_services/OpenAIService.py +224 -224
- edsl/inference_services/TestService.py +89 -89
- edsl/inference_services/TogetherAIService.py +170 -170
- edsl/inference_services/models_available_cache.py +118 -118
- edsl/inference_services/rate_limits_cache.py +25 -25
- edsl/inference_services/registry.py +39 -39
- edsl/inference_services/write_available.py +10 -10
- edsl/jobs/Answers.py +56 -56
- edsl/jobs/Jobs.py +1112 -1112
- edsl/jobs/__init__.py +1 -1
- edsl/jobs/buckets/BucketCollection.py +63 -63
- edsl/jobs/buckets/ModelBuckets.py +65 -65
- edsl/jobs/buckets/TokenBucket.py +248 -248
- edsl/jobs/interviews/Interview.py +661 -651
- edsl/jobs/interviews/InterviewExceptionCollection.py +99 -99
- edsl/jobs/interviews/InterviewExceptionEntry.py +182 -182
- edsl/jobs/interviews/InterviewStatistic.py +63 -63
- edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -25
- edsl/jobs/interviews/InterviewStatusDictionary.py +78 -78
- edsl/jobs/interviews/InterviewStatusLog.py +92 -92
- edsl/jobs/interviews/ReportErrors.py +66 -66
- edsl/jobs/interviews/interview_status_enum.py +9 -9
- edsl/jobs/runners/JobsRunnerAsyncio.py +338 -337
- edsl/jobs/runners/JobsRunnerStatus.py +332 -332
- edsl/jobs/tasks/QuestionTaskCreator.py +242 -242
- edsl/jobs/tasks/TaskCreators.py +64 -64
- edsl/jobs/tasks/TaskHistory.py +441 -441
- edsl/jobs/tasks/TaskStatusLog.py +23 -23
- edsl/jobs/tasks/task_status_enum.py +163 -163
- edsl/jobs/tokens/InterviewTokenUsage.py +27 -27
- edsl/jobs/tokens/TokenUsage.py +34 -34
- edsl/language_models/LanguageModel.py +718 -718
- edsl/language_models/ModelList.py +102 -102
- edsl/language_models/RegisterLanguageModelsMeta.py +184 -184
- edsl/language_models/__init__.py +2 -2
- edsl/language_models/fake_openai_call.py +15 -15
- edsl/language_models/fake_openai_service.py +61 -61
- edsl/language_models/registry.py +137 -137
- edsl/language_models/repair.py +156 -156
- edsl/language_models/unused/ReplicateBase.py +83 -83
- edsl/language_models/utilities.py +64 -64
- edsl/notebooks/Notebook.py +259 -259
- edsl/notebooks/__init__.py +1 -1
- edsl/prompts/Prompt.py +353 -358
- edsl/prompts/__init__.py +2 -2
- edsl/questions/AnswerValidatorMixin.py +289 -289
- edsl/questions/QuestionBase.py +616 -616
- edsl/questions/QuestionBaseGenMixin.py +161 -161
- edsl/questions/QuestionBasePromptsMixin.py +266 -266
- edsl/questions/QuestionBudget.py +227 -227
- edsl/questions/QuestionCheckBox.py +359 -359
- edsl/questions/QuestionExtract.py +183 -183
- edsl/questions/QuestionFreeText.py +114 -113
- edsl/questions/QuestionFunctional.py +159 -159
- edsl/questions/QuestionList.py +231 -231
- edsl/questions/QuestionMultipleChoice.py +286 -286
- edsl/questions/QuestionNumerical.py +153 -153
- edsl/questions/QuestionRank.py +324 -324
- edsl/questions/Quick.py +41 -41
- edsl/questions/RegisterQuestionsMeta.py +71 -71
- edsl/questions/ResponseValidatorABC.py +174 -174
- edsl/questions/SimpleAskMixin.py +73 -73
- edsl/questions/__init__.py +26 -26
- edsl/questions/compose_questions.py +98 -98
- edsl/questions/decorators.py +21 -21
- edsl/questions/derived/QuestionLikertFive.py +76 -76
- edsl/questions/derived/QuestionLinearScale.py +87 -87
- edsl/questions/derived/QuestionTopK.py +91 -91
- edsl/questions/derived/QuestionYesNo.py +82 -82
- edsl/questions/descriptors.py +418 -418
- edsl/questions/prompt_templates/question_budget.jinja +13 -13
- edsl/questions/prompt_templates/question_checkbox.jinja +32 -32
- edsl/questions/prompt_templates/question_extract.jinja +11 -11
- edsl/questions/prompt_templates/question_free_text.jinja +3 -3
- edsl/questions/prompt_templates/question_linear_scale.jinja +11 -11
- edsl/questions/prompt_templates/question_list.jinja +17 -17
- edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -33
- edsl/questions/prompt_templates/question_numerical.jinja +36 -36
- edsl/questions/question_registry.py +147 -147
- edsl/questions/settings.py +12 -12
- edsl/questions/templates/budget/answering_instructions.jinja +7 -7
- edsl/questions/templates/budget/question_presentation.jinja +7 -7
- edsl/questions/templates/checkbox/answering_instructions.jinja +10 -10
- edsl/questions/templates/checkbox/question_presentation.jinja +22 -22
- edsl/questions/templates/extract/answering_instructions.jinja +7 -7
- edsl/questions/templates/likert_five/answering_instructions.jinja +10 -10
- edsl/questions/templates/likert_five/question_presentation.jinja +11 -11
- edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -5
- edsl/questions/templates/linear_scale/question_presentation.jinja +5 -5
- edsl/questions/templates/list/answering_instructions.jinja +3 -3
- edsl/questions/templates/list/question_presentation.jinja +5 -5
- edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -9
- edsl/questions/templates/multiple_choice/question_presentation.jinja +11 -11
- edsl/questions/templates/numerical/answering_instructions.jinja +6 -6
- edsl/questions/templates/numerical/question_presentation.jinja +6 -6
- edsl/questions/templates/rank/answering_instructions.jinja +11 -11
- edsl/questions/templates/rank/question_presentation.jinja +15 -15
- edsl/questions/templates/top_k/answering_instructions.jinja +8 -8
- edsl/questions/templates/top_k/question_presentation.jinja +22 -22
- edsl/questions/templates/yes_no/answering_instructions.jinja +6 -6
- edsl/questions/templates/yes_no/question_presentation.jinja +11 -11
- edsl/results/Dataset.py +293 -293
- edsl/results/DatasetExportMixin.py +693 -693
- edsl/results/DatasetTree.py +145 -145
- edsl/results/Result.py +435 -433
- edsl/results/Results.py +1160 -1158
- edsl/results/ResultsDBMixin.py +238 -238
- edsl/results/ResultsExportMixin.py +43 -43
- edsl/results/ResultsFetchMixin.py +33 -33
- edsl/results/ResultsGGMixin.py +121 -121
- edsl/results/ResultsToolsMixin.py +98 -98
- edsl/results/Selector.py +118 -118
- edsl/results/__init__.py +2 -2
- edsl/results/tree_explore.py +115 -115
- edsl/scenarios/FileStore.py +458 -443
- edsl/scenarios/Scenario.py +510 -507
- edsl/scenarios/ScenarioHtmlMixin.py +59 -59
- edsl/scenarios/ScenarioList.py +1101 -1101
- edsl/scenarios/ScenarioListExportMixin.py +52 -52
- edsl/scenarios/ScenarioListPdfMixin.py +261 -261
- edsl/scenarios/__init__.py +4 -2
- edsl/shared.py +1 -1
- edsl/study/ObjectEntry.py +173 -173
- edsl/study/ProofOfWork.py +113 -113
- edsl/study/SnapShot.py +80 -80
- edsl/study/Study.py +528 -528
- edsl/study/__init__.py +4 -4
- edsl/surveys/DAG.py +148 -148
- edsl/surveys/Memory.py +31 -31
- edsl/surveys/MemoryPlan.py +244 -244
- edsl/surveys/Rule.py +324 -324
- edsl/surveys/RuleCollection.py +387 -387
- edsl/surveys/Survey.py +1772 -1772
- edsl/surveys/SurveyCSS.py +261 -261
- edsl/surveys/SurveyExportMixin.py +259 -259
- edsl/surveys/SurveyFlowVisualizationMixin.py +121 -121
- edsl/surveys/SurveyQualtricsImport.py +284 -284
- edsl/surveys/__init__.py +3 -3
- edsl/surveys/base.py +53 -53
- edsl/surveys/descriptors.py +56 -56
- edsl/surveys/instructions/ChangeInstruction.py +47 -47
- edsl/surveys/instructions/Instruction.py +51 -51
- edsl/surveys/instructions/InstructionCollection.py +77 -77
- edsl/templates/error_reporting/base.html +23 -23
- edsl/templates/error_reporting/exceptions_by_model.html +34 -34
- edsl/templates/error_reporting/exceptions_by_question_name.html +16 -16
- edsl/templates/error_reporting/exceptions_by_type.html +16 -16
- edsl/templates/error_reporting/interview_details.html +115 -115
- edsl/templates/error_reporting/interviews.html +9 -9
- edsl/templates/error_reporting/overview.html +4 -4
- edsl/templates/error_reporting/performance_plot.html +1 -1
- edsl/templates/error_reporting/report.css +73 -73
- edsl/templates/error_reporting/report.html +117 -117
- edsl/templates/error_reporting/report.js +25 -25
- edsl/tools/__init__.py +1 -1
- edsl/tools/clusters.py +192 -192
- edsl/tools/embeddings.py +27 -27
- edsl/tools/embeddings_plotting.py +118 -118
- edsl/tools/plotting.py +112 -112
- edsl/tools/summarize.py +18 -18
- edsl/utilities/SystemInfo.py +28 -28
- edsl/utilities/__init__.py +22 -22
- edsl/utilities/ast_utilities.py +25 -25
- edsl/utilities/data/Registry.py +6 -6
- edsl/utilities/data/__init__.py +1 -1
- edsl/utilities/data/scooter_results.json +1 -1
- edsl/utilities/decorators.py +77 -77
- edsl/utilities/gcp_bucket/cloud_storage.py +96 -96
- edsl/utilities/interface.py +627 -627
- edsl/utilities/repair_functions.py +28 -28
- edsl/utilities/restricted_python.py +70 -70
- edsl/utilities/utilities.py +391 -391
- {edsl-0.1.36.dev6.dist-info → edsl-0.1.37.dev2.dist-info}/LICENSE +21 -21
- {edsl-0.1.36.dev6.dist-info → edsl-0.1.37.dev2.dist-info}/METADATA +1 -1
- edsl-0.1.37.dev2.dist-info/RECORD +279 -0
- edsl-0.1.36.dev6.dist-info/RECORD +0 -279
- {edsl-0.1.36.dev6.dist-info → edsl-0.1.37.dev2.dist-info}/WHEEL +0 -0
edsl/questions/QuestionList.py
CHANGED
@@ -1,231 +1,231 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
import random
|
3
|
-
import textwrap
|
4
|
-
from typing import Any, Optional, Union
|
5
|
-
from edsl.questions.QuestionBase import QuestionBase
|
6
|
-
from edsl.questions.descriptors import IntegerOrNoneDescriptor
|
7
|
-
from edsl.questions.decorators import inject_exception
|
8
|
-
|
9
|
-
from pydantic import field_validator, Field
|
10
|
-
from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
|
11
|
-
from edsl.questions.ResponseValidatorABC import BaseResponse
|
12
|
-
|
13
|
-
from edsl.exceptions import QuestionAnswerValidationError
|
14
|
-
import textwrap
|
15
|
-
import json
|
16
|
-
|
17
|
-
from json_repair import repair_json
|
18
|
-
|
19
|
-
|
20
|
-
def convert_string(s):
|
21
|
-
"""Convert a string to a more appropriate type if possible.
|
22
|
-
|
23
|
-
>>> convert_string("3.14")
|
24
|
-
3.14
|
25
|
-
>>> convert_string("42")
|
26
|
-
42
|
27
|
-
>>> convert_string("hello")
|
28
|
-
'hello'
|
29
|
-
>>> convert_string('{"key": "value"}')
|
30
|
-
{'key': 'value'}
|
31
|
-
>>> convert_string("{'key': 'value'}")
|
32
|
-
{'key': 'value'}
|
33
|
-
"""
|
34
|
-
|
35
|
-
if not isinstance(s, str): # if it's not a string, return it as is
|
36
|
-
return s
|
37
|
-
|
38
|
-
# If the repair returns, continue on; otherwise, try to load it as JSON
|
39
|
-
if (repaired_json := repair_json(s)) == '""':
|
40
|
-
pass
|
41
|
-
else:
|
42
|
-
try:
|
43
|
-
return json.loads(repaired_json)
|
44
|
-
except json.JSONDecodeError:
|
45
|
-
pass
|
46
|
-
|
47
|
-
# Try to convert to float
|
48
|
-
try:
|
49
|
-
return float(s)
|
50
|
-
except ValueError:
|
51
|
-
pass
|
52
|
-
|
53
|
-
# Try to convert to int
|
54
|
-
try:
|
55
|
-
return int(s)
|
56
|
-
except ValueError:
|
57
|
-
pass
|
58
|
-
|
59
|
-
# If all conversions fail, return the original string
|
60
|
-
return s
|
61
|
-
|
62
|
-
|
63
|
-
def create_model(max_list_items: int, permissive):
|
64
|
-
from pydantic import BaseModel
|
65
|
-
|
66
|
-
if permissive or max_list_items is None:
|
67
|
-
|
68
|
-
class ListResponse(BaseModel):
|
69
|
-
answer: list[Any]
|
70
|
-
comment: Optional[str] = None
|
71
|
-
generated_tokens: Optional[str] = None
|
72
|
-
|
73
|
-
else:
|
74
|
-
|
75
|
-
class ListResponse(BaseModel):
|
76
|
-
"""
|
77
|
-
>>> nr = ListResponse(answer=["Apple", "Cherry"])
|
78
|
-
>>> nr.dict()
|
79
|
-
{'answer': ['Apple', 'Cherry'], 'comment': None, 'generated_tokens': None}
|
80
|
-
"""
|
81
|
-
|
82
|
-
answer: list[Any] = Field(..., min_items=0, max_items=max_list_items)
|
83
|
-
comment: Optional[str] = None
|
84
|
-
generated_tokens: Optional[str] = None
|
85
|
-
|
86
|
-
return ListResponse
|
87
|
-
|
88
|
-
|
89
|
-
class ListResponseValidator(ResponseValidatorABC):
|
90
|
-
required_params = ["max_list_items", "permissive"]
|
91
|
-
valid_examples = [({"answer": ["hello", "world"]}, {"max_list_items": 5})]
|
92
|
-
|
93
|
-
invalid_examples = [
|
94
|
-
(
|
95
|
-
{"answer": ["hello", "world", "this", "is", "a", "test"]},
|
96
|
-
{"max_list_items": 5},
|
97
|
-
"Too many items.",
|
98
|
-
),
|
99
|
-
]
|
100
|
-
|
101
|
-
def _check_constraints(self, response) -> None:
|
102
|
-
if (
|
103
|
-
self.max_list_items is not None
|
104
|
-
and len(response.answer) > self.max_list_items
|
105
|
-
):
|
106
|
-
raise QuestionAnswerValidationError("Too many items.")
|
107
|
-
|
108
|
-
def fix(self, response, verbose=False):
|
109
|
-
if verbose:
|
110
|
-
print(f"Fixing list response: {response}")
|
111
|
-
answer = str(response.get("answer") or response.get("generated_tokens", ""))
|
112
|
-
if len(answer.split(",")) > 0:
|
113
|
-
return (
|
114
|
-
{"answer": answer.split(",")} | {"comment": response.get("comment")}
|
115
|
-
if "comment" in response
|
116
|
-
else {}
|
117
|
-
)
|
118
|
-
|
119
|
-
def _post_process(self, edsl_answer_dict):
|
120
|
-
edsl_answer_dict["answer"] = [
|
121
|
-
convert_string(item) for item in edsl_answer_dict["answer"]
|
122
|
-
]
|
123
|
-
return edsl_answer_dict
|
124
|
-
|
125
|
-
|
126
|
-
class QuestionList(QuestionBase):
|
127
|
-
"""This question prompts the agent to answer by providing a list of items as comma-separated strings."""
|
128
|
-
|
129
|
-
question_type = "list"
|
130
|
-
max_list_items: int = IntegerOrNoneDescriptor()
|
131
|
-
_response_model = None
|
132
|
-
response_validator_class = ListResponseValidator
|
133
|
-
|
134
|
-
def __init__(
|
135
|
-
self,
|
136
|
-
question_name: str,
|
137
|
-
question_text: str,
|
138
|
-
max_list_items: Optional[int] = None,
|
139
|
-
include_comment: bool = True,
|
140
|
-
answering_instructions: Optional[str] = None,
|
141
|
-
question_presentation: Optional[str] = None,
|
142
|
-
permissive: bool = False,
|
143
|
-
):
|
144
|
-
"""Instantiate a new QuestionList.
|
145
|
-
|
146
|
-
:param question_name: The name of the question.
|
147
|
-
:param question_text: The text of the question.
|
148
|
-
:param max_list_items: The maximum number of items that can be in the answer list.
|
149
|
-
|
150
|
-
>>> QuestionList.example().self_check()
|
151
|
-
"""
|
152
|
-
self.question_name = question_name
|
153
|
-
self.question_text = question_text
|
154
|
-
self.max_list_items = max_list_items
|
155
|
-
self.permissive = permissive
|
156
|
-
|
157
|
-
self.include_comment = include_comment
|
158
|
-
self.answering_instructions = answering_instructions
|
159
|
-
self.question_presentations = question_presentation
|
160
|
-
|
161
|
-
def create_response_model(self):
|
162
|
-
return create_model(self.max_list_items, self.permissive)
|
163
|
-
|
164
|
-
@property
|
165
|
-
def question_html_content(self) -> str:
|
166
|
-
from jinja2 import Template
|
167
|
-
|
168
|
-
question_html_content = Template(
|
169
|
-
"""
|
170
|
-
<div id="question-list-container">
|
171
|
-
<div>
|
172
|
-
<textarea name="{{ question_name }}[]" rows="1" placeholder="Enter item"></textarea>
|
173
|
-
</div>
|
174
|
-
</div>
|
175
|
-
<button type="button" onclick="addNewLine()">Add another line</button>
|
176
|
-
|
177
|
-
<script>
|
178
|
-
function addNewLine() {
|
179
|
-
var container = document.getElementById('question-list-container');
|
180
|
-
var newLine = document.createElement('div');
|
181
|
-
newLine.innerHTML = '<textarea name="{{ question_name }}[]" rows="1" placeholder="Enter item"></textarea>';
|
182
|
-
container.appendChild(newLine);
|
183
|
-
}
|
184
|
-
</script>
|
185
|
-
"""
|
186
|
-
).render(question_name=self.question_name)
|
187
|
-
return question_html_content
|
188
|
-
|
189
|
-
################
|
190
|
-
# Helpful methods
|
191
|
-
################
|
192
|
-
@classmethod
|
193
|
-
@inject_exception
|
194
|
-
def example(
|
195
|
-
cls, include_comment=True, max_list_items=None, permissive=False
|
196
|
-
) -> QuestionList:
|
197
|
-
"""Return an example of a list question."""
|
198
|
-
return cls(
|
199
|
-
question_name="list_of_foods",
|
200
|
-
question_text="What are your favorite foods?",
|
201
|
-
include_comment=include_comment,
|
202
|
-
max_list_items=max_list_items,
|
203
|
-
permissive=permissive,
|
204
|
-
)
|
205
|
-
|
206
|
-
|
207
|
-
def main():
|
208
|
-
"""Create an example of a list question and demonstrate its functionality."""
|
209
|
-
from edsl.questions.QuestionList import QuestionList
|
210
|
-
|
211
|
-
q = QuestionList.example(max_list_items=5)
|
212
|
-
q.question_text
|
213
|
-
q.question_name
|
214
|
-
q.max_list_items
|
215
|
-
# validate an answer
|
216
|
-
q._validate_answer({"answer": ["pasta", "garlic", "oil", "parmesan"]})
|
217
|
-
# translate answer code
|
218
|
-
q._translate_answer_code_to_answer(["pasta", "garlic", "oil", "parmesan"])
|
219
|
-
# simulate answer
|
220
|
-
q._simulate_answer()
|
221
|
-
q._simulate_answer(human_readable=False)
|
222
|
-
q._validate_answer(q._simulate_answer(human_readable=False))
|
223
|
-
# serialization (inherits from Question)
|
224
|
-
q.to_dict()
|
225
|
-
assert q.from_dict(q.to_dict()) == q
|
226
|
-
|
227
|
-
|
228
|
-
if __name__ == "__main__":
|
229
|
-
import doctest
|
230
|
-
|
231
|
-
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
1
|
+
from __future__ import annotations
|
2
|
+
import random
|
3
|
+
import textwrap
|
4
|
+
from typing import Any, Optional, Union
|
5
|
+
from edsl.questions.QuestionBase import QuestionBase
|
6
|
+
from edsl.questions.descriptors import IntegerOrNoneDescriptor
|
7
|
+
from edsl.questions.decorators import inject_exception
|
8
|
+
|
9
|
+
from pydantic import field_validator, Field
|
10
|
+
from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
|
11
|
+
from edsl.questions.ResponseValidatorABC import BaseResponse
|
12
|
+
|
13
|
+
from edsl.exceptions import QuestionAnswerValidationError
|
14
|
+
import textwrap
|
15
|
+
import json
|
16
|
+
|
17
|
+
from json_repair import repair_json
|
18
|
+
|
19
|
+
|
20
|
+
def convert_string(s):
|
21
|
+
"""Convert a string to a more appropriate type if possible.
|
22
|
+
|
23
|
+
>>> convert_string("3.14")
|
24
|
+
3.14
|
25
|
+
>>> convert_string("42")
|
26
|
+
42
|
27
|
+
>>> convert_string("hello")
|
28
|
+
'hello'
|
29
|
+
>>> convert_string('{"key": "value"}')
|
30
|
+
{'key': 'value'}
|
31
|
+
>>> convert_string("{'key': 'value'}")
|
32
|
+
{'key': 'value'}
|
33
|
+
"""
|
34
|
+
|
35
|
+
if not isinstance(s, str): # if it's not a string, return it as is
|
36
|
+
return s
|
37
|
+
|
38
|
+
# If the repair returns, continue on; otherwise, try to load it as JSON
|
39
|
+
if (repaired_json := repair_json(s)) == '""':
|
40
|
+
pass
|
41
|
+
else:
|
42
|
+
try:
|
43
|
+
return json.loads(repaired_json)
|
44
|
+
except json.JSONDecodeError:
|
45
|
+
pass
|
46
|
+
|
47
|
+
# Try to convert to float
|
48
|
+
try:
|
49
|
+
return float(s)
|
50
|
+
except ValueError:
|
51
|
+
pass
|
52
|
+
|
53
|
+
# Try to convert to int
|
54
|
+
try:
|
55
|
+
return int(s)
|
56
|
+
except ValueError:
|
57
|
+
pass
|
58
|
+
|
59
|
+
# If all conversions fail, return the original string
|
60
|
+
return s
|
61
|
+
|
62
|
+
|
63
|
+
def create_model(max_list_items: int, permissive):
|
64
|
+
from pydantic import BaseModel
|
65
|
+
|
66
|
+
if permissive or max_list_items is None:
|
67
|
+
|
68
|
+
class ListResponse(BaseModel):
|
69
|
+
answer: list[Any]
|
70
|
+
comment: Optional[str] = None
|
71
|
+
generated_tokens: Optional[str] = None
|
72
|
+
|
73
|
+
else:
|
74
|
+
|
75
|
+
class ListResponse(BaseModel):
|
76
|
+
"""
|
77
|
+
>>> nr = ListResponse(answer=["Apple", "Cherry"])
|
78
|
+
>>> nr.dict()
|
79
|
+
{'answer': ['Apple', 'Cherry'], 'comment': None, 'generated_tokens': None}
|
80
|
+
"""
|
81
|
+
|
82
|
+
answer: list[Any] = Field(..., min_items=0, max_items=max_list_items)
|
83
|
+
comment: Optional[str] = None
|
84
|
+
generated_tokens: Optional[str] = None
|
85
|
+
|
86
|
+
return ListResponse
|
87
|
+
|
88
|
+
|
89
|
+
class ListResponseValidator(ResponseValidatorABC):
|
90
|
+
required_params = ["max_list_items", "permissive"]
|
91
|
+
valid_examples = [({"answer": ["hello", "world"]}, {"max_list_items": 5})]
|
92
|
+
|
93
|
+
invalid_examples = [
|
94
|
+
(
|
95
|
+
{"answer": ["hello", "world", "this", "is", "a", "test"]},
|
96
|
+
{"max_list_items": 5},
|
97
|
+
"Too many items.",
|
98
|
+
),
|
99
|
+
]
|
100
|
+
|
101
|
+
def _check_constraints(self, response) -> None:
|
102
|
+
if (
|
103
|
+
self.max_list_items is not None
|
104
|
+
and len(response.answer) > self.max_list_items
|
105
|
+
):
|
106
|
+
raise QuestionAnswerValidationError("Too many items.")
|
107
|
+
|
108
|
+
def fix(self, response, verbose=False):
|
109
|
+
if verbose:
|
110
|
+
print(f"Fixing list response: {response}")
|
111
|
+
answer = str(response.get("answer") or response.get("generated_tokens", ""))
|
112
|
+
if len(answer.split(",")) > 0:
|
113
|
+
return (
|
114
|
+
{"answer": answer.split(",")} | {"comment": response.get("comment")}
|
115
|
+
if "comment" in response
|
116
|
+
else {}
|
117
|
+
)
|
118
|
+
|
119
|
+
def _post_process(self, edsl_answer_dict):
|
120
|
+
edsl_answer_dict["answer"] = [
|
121
|
+
convert_string(item) for item in edsl_answer_dict["answer"]
|
122
|
+
]
|
123
|
+
return edsl_answer_dict
|
124
|
+
|
125
|
+
|
126
|
+
class QuestionList(QuestionBase):
|
127
|
+
"""This question prompts the agent to answer by providing a list of items as comma-separated strings."""
|
128
|
+
|
129
|
+
question_type = "list"
|
130
|
+
max_list_items: int = IntegerOrNoneDescriptor()
|
131
|
+
_response_model = None
|
132
|
+
response_validator_class = ListResponseValidator
|
133
|
+
|
134
|
+
def __init__(
|
135
|
+
self,
|
136
|
+
question_name: str,
|
137
|
+
question_text: str,
|
138
|
+
max_list_items: Optional[int] = None,
|
139
|
+
include_comment: bool = True,
|
140
|
+
answering_instructions: Optional[str] = None,
|
141
|
+
question_presentation: Optional[str] = None,
|
142
|
+
permissive: bool = False,
|
143
|
+
):
|
144
|
+
"""Instantiate a new QuestionList.
|
145
|
+
|
146
|
+
:param question_name: The name of the question.
|
147
|
+
:param question_text: The text of the question.
|
148
|
+
:param max_list_items: The maximum number of items that can be in the answer list.
|
149
|
+
|
150
|
+
>>> QuestionList.example().self_check()
|
151
|
+
"""
|
152
|
+
self.question_name = question_name
|
153
|
+
self.question_text = question_text
|
154
|
+
self.max_list_items = max_list_items
|
155
|
+
self.permissive = permissive
|
156
|
+
|
157
|
+
self.include_comment = include_comment
|
158
|
+
self.answering_instructions = answering_instructions
|
159
|
+
self.question_presentations = question_presentation
|
160
|
+
|
161
|
+
def create_response_model(self):
|
162
|
+
return create_model(self.max_list_items, self.permissive)
|
163
|
+
|
164
|
+
@property
|
165
|
+
def question_html_content(self) -> str:
|
166
|
+
from jinja2 import Template
|
167
|
+
|
168
|
+
question_html_content = Template(
|
169
|
+
"""
|
170
|
+
<div id="question-list-container">
|
171
|
+
<div>
|
172
|
+
<textarea name="{{ question_name }}[]" rows="1" placeholder="Enter item"></textarea>
|
173
|
+
</div>
|
174
|
+
</div>
|
175
|
+
<button type="button" onclick="addNewLine()">Add another line</button>
|
176
|
+
|
177
|
+
<script>
|
178
|
+
function addNewLine() {
|
179
|
+
var container = document.getElementById('question-list-container');
|
180
|
+
var newLine = document.createElement('div');
|
181
|
+
newLine.innerHTML = '<textarea name="{{ question_name }}[]" rows="1" placeholder="Enter item"></textarea>';
|
182
|
+
container.appendChild(newLine);
|
183
|
+
}
|
184
|
+
</script>
|
185
|
+
"""
|
186
|
+
).render(question_name=self.question_name)
|
187
|
+
return question_html_content
|
188
|
+
|
189
|
+
################
|
190
|
+
# Helpful methods
|
191
|
+
################
|
192
|
+
@classmethod
|
193
|
+
@inject_exception
|
194
|
+
def example(
|
195
|
+
cls, include_comment=True, max_list_items=None, permissive=False
|
196
|
+
) -> QuestionList:
|
197
|
+
"""Return an example of a list question."""
|
198
|
+
return cls(
|
199
|
+
question_name="list_of_foods",
|
200
|
+
question_text="What are your favorite foods?",
|
201
|
+
include_comment=include_comment,
|
202
|
+
max_list_items=max_list_items,
|
203
|
+
permissive=permissive,
|
204
|
+
)
|
205
|
+
|
206
|
+
|
207
|
+
def main():
|
208
|
+
"""Create an example of a list question and demonstrate its functionality."""
|
209
|
+
from edsl.questions.QuestionList import QuestionList
|
210
|
+
|
211
|
+
q = QuestionList.example(max_list_items=5)
|
212
|
+
q.question_text
|
213
|
+
q.question_name
|
214
|
+
q.max_list_items
|
215
|
+
# validate an answer
|
216
|
+
q._validate_answer({"answer": ["pasta", "garlic", "oil", "parmesan"]})
|
217
|
+
# translate answer code
|
218
|
+
q._translate_answer_code_to_answer(["pasta", "garlic", "oil", "parmesan"])
|
219
|
+
# simulate answer
|
220
|
+
q._simulate_answer()
|
221
|
+
q._simulate_answer(human_readable=False)
|
222
|
+
q._validate_answer(q._simulate_answer(human_readable=False))
|
223
|
+
# serialization (inherits from Question)
|
224
|
+
q.to_dict()
|
225
|
+
assert q.from_dict(q.to_dict()) == q
|
226
|
+
|
227
|
+
|
228
|
+
if __name__ == "__main__":
|
229
|
+
import doctest
|
230
|
+
|
231
|
+
doctest.testmod(optionflags=doctest.ELLIPSIS)
|