edsl 0.1.49__py3-none-any.whl → 0.1.51__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 +124 -53
- edsl/__version__.py +1 -1
- edsl/agents/agent.py +21 -21
- edsl/agents/agent_list.py +2 -5
- edsl/agents/exceptions.py +119 -5
- edsl/base/__init__.py +10 -35
- edsl/base/base_class.py +71 -36
- edsl/base/base_exception.py +204 -0
- edsl/base/data_transfer_models.py +1 -1
- edsl/base/exceptions.py +94 -0
- edsl/buckets/__init__.py +15 -1
- edsl/buckets/bucket_collection.py +3 -4
- edsl/buckets/exceptions.py +107 -0
- edsl/buckets/model_buckets.py +1 -2
- edsl/buckets/token_bucket.py +11 -6
- edsl/buckets/token_bucket_api.py +27 -12
- edsl/buckets/token_bucket_client.py +9 -7
- edsl/caching/cache.py +12 -4
- edsl/caching/cache_entry.py +10 -9
- edsl/caching/exceptions.py +113 -7
- edsl/caching/remote_cache_sync.py +6 -7
- edsl/caching/sql_dict.py +20 -14
- edsl/cli.py +43 -0
- edsl/config/__init__.py +1 -1
- edsl/config/config_class.py +32 -6
- edsl/conversation/Conversation.py +8 -4
- edsl/conversation/car_buying.py +1 -3
- edsl/conversation/exceptions.py +58 -0
- edsl/conversation/mug_negotiation.py +2 -8
- edsl/coop/__init__.py +28 -6
- edsl/coop/coop.py +120 -29
- edsl/coop/coop_functions.py +1 -1
- edsl/coop/ep_key_handling.py +1 -1
- edsl/coop/exceptions.py +188 -9
- edsl/coop/price_fetcher.py +5 -8
- edsl/coop/utils.py +4 -6
- edsl/dataset/__init__.py +5 -4
- edsl/dataset/dataset.py +177 -86
- edsl/dataset/dataset_operations_mixin.py +98 -76
- edsl/dataset/dataset_tree.py +11 -7
- edsl/dataset/display/table_display.py +0 -2
- edsl/dataset/display/table_renderers.py +6 -4
- edsl/dataset/exceptions.py +125 -0
- edsl/dataset/file_exports.py +18 -11
- edsl/dataset/r/ggplot.py +13 -6
- edsl/display/__init__.py +27 -0
- edsl/display/core.py +147 -0
- edsl/display/plugin.py +189 -0
- edsl/display/utils.py +52 -0
- edsl/inference_services/__init__.py +9 -1
- edsl/inference_services/available_model_cache_handler.py +1 -1
- edsl/inference_services/available_model_fetcher.py +5 -6
- edsl/inference_services/data_structures.py +10 -7
- edsl/inference_services/exceptions.py +132 -1
- edsl/inference_services/inference_service_abc.py +2 -2
- edsl/inference_services/inference_services_collection.py +2 -6
- edsl/inference_services/registry.py +4 -3
- edsl/inference_services/service_availability.py +4 -3
- edsl/inference_services/services/anthropic_service.py +4 -1
- edsl/inference_services/services/aws_bedrock.py +13 -12
- edsl/inference_services/services/azure_ai.py +12 -10
- edsl/inference_services/services/deep_infra_service.py +1 -4
- edsl/inference_services/services/deep_seek_service.py +1 -5
- edsl/inference_services/services/google_service.py +7 -3
- edsl/inference_services/services/groq_service.py +1 -1
- edsl/inference_services/services/mistral_ai_service.py +4 -2
- edsl/inference_services/services/ollama_service.py +1 -1
- edsl/inference_services/services/open_ai_service.py +7 -5
- edsl/inference_services/services/perplexity_service.py +6 -2
- edsl/inference_services/services/test_service.py +8 -7
- edsl/inference_services/services/together_ai_service.py +2 -3
- edsl/inference_services/services/xai_service.py +1 -1
- edsl/instructions/__init__.py +1 -1
- edsl/instructions/change_instruction.py +7 -5
- edsl/instructions/exceptions.py +61 -0
- edsl/instructions/instruction.py +6 -2
- edsl/instructions/instruction_collection.py +6 -4
- edsl/instructions/instruction_handler.py +12 -15
- edsl/interviews/ReportErrors.py +0 -3
- edsl/interviews/__init__.py +9 -2
- edsl/interviews/answering_function.py +11 -13
- edsl/interviews/exception_tracking.py +15 -8
- edsl/interviews/exceptions.py +79 -0
- edsl/interviews/interview.py +33 -30
- edsl/interviews/interview_status_dictionary.py +4 -2
- edsl/interviews/interview_status_log.py +2 -1
- edsl/interviews/interview_task_manager.py +5 -5
- edsl/interviews/request_token_estimator.py +5 -2
- edsl/interviews/statistics.py +3 -4
- edsl/invigilators/__init__.py +7 -1
- edsl/invigilators/exceptions.py +79 -0
- edsl/invigilators/invigilator_base.py +0 -1
- edsl/invigilators/invigilators.py +9 -13
- edsl/invigilators/prompt_constructor.py +1 -5
- edsl/invigilators/prompt_helpers.py +8 -4
- edsl/invigilators/question_instructions_prompt_builder.py +1 -1
- edsl/invigilators/question_option_processor.py +9 -5
- edsl/invigilators/question_template_replacements_builder.py +3 -2
- edsl/jobs/__init__.py +42 -5
- edsl/jobs/async_interview_runner.py +25 -23
- edsl/jobs/check_survey_scenario_compatibility.py +11 -10
- edsl/jobs/data_structures.py +8 -5
- edsl/jobs/exceptions.py +177 -8
- edsl/jobs/fetch_invigilator.py +1 -1
- edsl/jobs/jobs.py +74 -69
- edsl/jobs/jobs_checks.py +6 -7
- edsl/jobs/jobs_component_constructor.py +4 -4
- edsl/jobs/jobs_pricing_estimation.py +4 -3
- edsl/jobs/jobs_remote_inference_logger.py +5 -4
- edsl/jobs/jobs_runner_asyncio.py +3 -4
- edsl/jobs/jobs_runner_status.py +8 -9
- edsl/jobs/remote_inference.py +27 -24
- edsl/jobs/results_exceptions_handler.py +10 -7
- edsl/key_management/__init__.py +3 -1
- edsl/key_management/exceptions.py +62 -0
- edsl/key_management/key_lookup.py +1 -1
- edsl/key_management/key_lookup_builder.py +37 -14
- edsl/key_management/key_lookup_collection.py +2 -0
- edsl/language_models/__init__.py +1 -1
- edsl/language_models/exceptions.py +302 -14
- edsl/language_models/language_model.py +9 -8
- edsl/language_models/model.py +4 -4
- edsl/language_models/model_list.py +1 -1
- edsl/language_models/price_manager.py +1 -1
- edsl/language_models/raw_response_handler.py +14 -9
- edsl/language_models/registry.py +17 -21
- edsl/language_models/repair.py +0 -6
- edsl/language_models/unused/fake_openai_service.py +0 -1
- edsl/load_plugins.py +69 -0
- edsl/logger.py +146 -0
- edsl/notebooks/__init__.py +24 -1
- edsl/notebooks/exceptions.py +82 -0
- edsl/notebooks/notebook.py +7 -3
- edsl/notebooks/notebook_to_latex.py +1 -2
- edsl/plugins/__init__.py +63 -0
- edsl/plugins/built_in/export_example.py +50 -0
- edsl/plugins/built_in/pig_latin.py +67 -0
- edsl/plugins/cli.py +372 -0
- edsl/plugins/cli_typer.py +283 -0
- edsl/plugins/exceptions.py +31 -0
- edsl/plugins/hookspec.py +51 -0
- edsl/plugins/plugin_host.py +128 -0
- edsl/plugins/plugin_manager.py +633 -0
- edsl/plugins/plugins_registry.py +168 -0
- edsl/prompts/__init__.py +24 -1
- edsl/prompts/exceptions.py +107 -5
- edsl/prompts/prompt.py +15 -7
- edsl/questions/HTMLQuestion.py +5 -11
- edsl/questions/Quick.py +0 -1
- edsl/questions/__init__.py +6 -4
- edsl/questions/answer_validator_mixin.py +318 -323
- edsl/questions/compose_questions.py +3 -3
- edsl/questions/descriptors.py +11 -50
- edsl/questions/exceptions.py +278 -22
- edsl/questions/loop_processor.py +7 -5
- edsl/questions/prompt_templates/question_list.jinja +3 -0
- edsl/questions/question_base.py +46 -19
- edsl/questions/question_base_gen_mixin.py +2 -2
- edsl/questions/question_base_prompts_mixin.py +13 -7
- edsl/questions/question_budget.py +503 -98
- edsl/questions/question_check_box.py +660 -160
- edsl/questions/question_dict.py +345 -194
- edsl/questions/question_extract.py +401 -61
- edsl/questions/question_free_text.py +80 -14
- edsl/questions/question_functional.py +119 -9
- edsl/questions/{derived/question_likert_five.py → question_likert_five.py} +2 -2
- edsl/questions/{derived/question_linear_scale.py → question_linear_scale.py} +3 -4
- edsl/questions/question_list.py +275 -28
- edsl/questions/question_matrix.py +643 -96
- edsl/questions/question_multiple_choice.py +219 -51
- edsl/questions/question_numerical.py +361 -32
- edsl/questions/question_rank.py +401 -124
- edsl/questions/question_registry.py +7 -5
- edsl/questions/{derived/question_top_k.py → question_top_k.py} +3 -3
- edsl/questions/{derived/question_yes_no.py → question_yes_no.py} +3 -4
- edsl/questions/register_questions_meta.py +2 -2
- edsl/questions/response_validator_abc.py +13 -15
- edsl/questions/response_validator_factory.py +10 -12
- edsl/questions/templates/dict/answering_instructions.jinja +1 -0
- edsl/questions/templates/rank/question_presentation.jinja +1 -1
- edsl/results/__init__.py +1 -1
- edsl/results/exceptions.py +141 -7
- edsl/results/report.py +1 -2
- edsl/results/result.py +11 -9
- edsl/results/results.py +480 -321
- edsl/results/results_selector.py +8 -4
- edsl/scenarios/PdfExtractor.py +2 -2
- edsl/scenarios/construct_download_link.py +69 -35
- edsl/scenarios/directory_scanner.py +33 -14
- edsl/scenarios/document_chunker.py +1 -1
- edsl/scenarios/exceptions.py +238 -14
- edsl/scenarios/file_methods.py +1 -1
- edsl/scenarios/file_store.py +7 -3
- edsl/scenarios/handlers/__init__.py +17 -0
- edsl/scenarios/handlers/docx_file_store.py +0 -5
- edsl/scenarios/handlers/pdf_file_store.py +0 -1
- edsl/scenarios/handlers/pptx_file_store.py +0 -5
- edsl/scenarios/handlers/py_file_store.py +0 -1
- edsl/scenarios/handlers/sql_file_store.py +1 -4
- edsl/scenarios/handlers/sqlite_file_store.py +0 -1
- edsl/scenarios/handlers/txt_file_store.py +1 -1
- edsl/scenarios/scenario.py +1 -3
- edsl/scenarios/scenario_list.py +179 -27
- edsl/scenarios/scenario_list_pdf_tools.py +1 -0
- edsl/scenarios/scenario_selector.py +0 -1
- edsl/surveys/__init__.py +3 -4
- edsl/surveys/dag/__init__.py +4 -2
- edsl/surveys/descriptors.py +1 -1
- edsl/surveys/edit_survey.py +1 -0
- edsl/surveys/exceptions.py +165 -9
- edsl/surveys/memory/__init__.py +5 -3
- edsl/surveys/memory/memory_management.py +1 -0
- edsl/surveys/memory/memory_plan.py +6 -15
- edsl/surveys/rules/__init__.py +5 -3
- edsl/surveys/rules/rule.py +1 -2
- edsl/surveys/rules/rule_collection.py +1 -1
- edsl/surveys/survey.py +12 -24
- edsl/surveys/survey_css.py +3 -3
- edsl/surveys/survey_export.py +6 -3
- edsl/surveys/survey_flow_visualization.py +10 -1
- edsl/surveys/survey_simulator.py +2 -1
- edsl/tasks/__init__.py +23 -1
- edsl/tasks/exceptions.py +72 -0
- edsl/tasks/question_task_creator.py +3 -3
- edsl/tasks/task_creators.py +1 -3
- edsl/tasks/task_history.py +8 -10
- edsl/tasks/task_status_log.py +1 -2
- edsl/tokens/__init__.py +29 -1
- edsl/tokens/exceptions.py +37 -0
- edsl/tokens/interview_token_usage.py +3 -2
- edsl/tokens/token_usage.py +4 -3
- edsl/utilities/__init__.py +21 -1
- edsl/utilities/decorators.py +1 -2
- edsl/utilities/markdown_to_docx.py +2 -2
- edsl/utilities/markdown_to_pdf.py +1 -1
- edsl/utilities/repair_functions.py +0 -1
- edsl/utilities/restricted_python.py +0 -1
- edsl/utilities/template_loader.py +2 -3
- edsl/utilities/utilities.py +8 -29
- {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/METADATA +32 -2
- edsl-0.1.51.dist-info/RECORD +365 -0
- edsl-0.1.51.dist-info/entry_points.txt +3 -0
- edsl/dataset/smart_objects.py +0 -96
- edsl/exceptions/BaseException.py +0 -21
- edsl/exceptions/__init__.py +0 -54
- edsl/exceptions/configuration.py +0 -16
- edsl/exceptions/general.py +0 -34
- edsl/questions/derived/__init__.py +0 -0
- edsl/study/ObjectEntry.py +0 -173
- edsl/study/ProofOfWork.py +0 -113
- edsl/study/SnapShot.py +0 -80
- edsl/study/Study.py +0 -520
- edsl/study/__init__.py +0 -6
- edsl/utilities/interface.py +0 -135
- edsl-0.1.49.dist-info/RECORD +0 -347
- {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/LICENSE +0 -0
- {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/WHEEL +0 -0
@@ -1,15 +1,52 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from
|
4
|
-
|
3
|
+
from typing import Any, Optional, Union
|
4
|
+
import re
|
5
|
+
from pydantic import BaseModel, model_validator, ValidationError
|
5
6
|
|
6
|
-
from pydantic import BaseModel, Field, field_validator
|
7
|
-
|
8
|
-
from .exceptions import QuestionAnswerValidationError
|
9
7
|
from .question_base import QuestionBase
|
10
8
|
from .descriptors import NumericalOrNoneDescriptor
|
11
9
|
from .decorators import inject_exception
|
12
10
|
from .response_validator_abc import ResponseValidatorABC
|
11
|
+
from .exceptions import QuestionAnswerValidationError
|
12
|
+
|
13
|
+
|
14
|
+
class NumericalResponse(BaseModel):
|
15
|
+
"""
|
16
|
+
Pydantic model for validating numerical responses.
|
17
|
+
|
18
|
+
This model defines the structure and validation rules for responses to
|
19
|
+
numerical questions. It ensures that responses contain a valid number
|
20
|
+
and that the number falls within any specified range constraints.
|
21
|
+
|
22
|
+
Attributes:
|
23
|
+
answer: The numerical response (int or float)
|
24
|
+
comment: Optional comment provided with the answer
|
25
|
+
generated_tokens: Optional raw LLM output for token tracking
|
26
|
+
|
27
|
+
Examples:
|
28
|
+
>>> # Valid response with just answer
|
29
|
+
>>> response = NumericalResponse(answer=42)
|
30
|
+
>>> response.answer
|
31
|
+
42
|
32
|
+
|
33
|
+
>>> # Valid response with comment
|
34
|
+
>>> response = NumericalResponse(answer=3.14, comment="Pi approximation")
|
35
|
+
>>> response.answer
|
36
|
+
3.14
|
37
|
+
>>> response.comment
|
38
|
+
'Pi approximation'
|
39
|
+
|
40
|
+
>>> # Invalid non-numeric answer
|
41
|
+
>>> try:
|
42
|
+
... NumericalResponse(answer="not a number")
|
43
|
+
... except Exception as e:
|
44
|
+
... print("Validation error occurred")
|
45
|
+
Validation error occurred
|
46
|
+
"""
|
47
|
+
answer: Union[int, float]
|
48
|
+
comment: Optional[str] = None
|
49
|
+
generated_tokens: Optional[Any] = None
|
13
50
|
|
14
51
|
|
15
52
|
def create_numeric_response(
|
@@ -17,23 +54,120 @@ def create_numeric_response(
|
|
17
54
|
max_value: Optional[float] = None,
|
18
55
|
permissive=False,
|
19
56
|
):
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
57
|
+
"""Create a constrained numerical response model with range validation.
|
58
|
+
|
59
|
+
Examples:
|
60
|
+
>>> # Create model with constraints
|
61
|
+
>>> ConstrainedModel = create_numeric_response(min_value=0, max_value=100)
|
62
|
+
>>> response = ConstrainedModel(answer=42)
|
63
|
+
>>> response.answer
|
64
|
+
42
|
65
|
+
|
66
|
+
>>> # Test min value constraint failure
|
67
|
+
>>> try:
|
68
|
+
... ConstrainedModel(answer=-5)
|
69
|
+
... except Exception as e:
|
70
|
+
... "must be greater than or equal to" in str(e)
|
71
|
+
True
|
72
|
+
|
73
|
+
>>> # Test max value constraint failure
|
74
|
+
>>> try:
|
75
|
+
... ConstrainedModel(answer=150)
|
76
|
+
... except Exception as e:
|
77
|
+
... "must be less than or equal to" in str(e)
|
78
|
+
True
|
79
|
+
|
80
|
+
>>> # Permissive mode ignores constraints
|
81
|
+
>>> PermissiveModel = create_numeric_response(min_value=0, max_value=100, permissive=True)
|
82
|
+
>>> response = PermissiveModel(answer=150)
|
83
|
+
>>> response.answer
|
84
|
+
150
|
85
|
+
"""
|
86
|
+
if permissive:
|
87
|
+
return NumericalResponse
|
88
|
+
|
89
|
+
class ConstrainedNumericResponse(NumericalResponse):
|
90
|
+
"""Numerical response model with added range constraints."""
|
91
|
+
|
92
|
+
@model_validator(mode='after')
|
93
|
+
def validate_range_constraints(self):
|
94
|
+
"""Validate that the number meets range constraints."""
|
95
|
+
if min_value is not None and self.answer < min_value:
|
96
|
+
validation_error = ValidationError.from_exception_data(
|
97
|
+
title='ConstrainedNumericResponse',
|
98
|
+
line_errors=[{
|
99
|
+
'type': 'value_error',
|
100
|
+
'loc': ('answer',),
|
101
|
+
'msg': f'Answer must be greater than or equal to {min_value}',
|
102
|
+
'input': self.answer,
|
103
|
+
'ctx': {'error': 'Value too small'}
|
104
|
+
}]
|
105
|
+
)
|
106
|
+
raise QuestionAnswerValidationError(
|
107
|
+
message=f"Answer {self.answer} must be greater than or equal to {min_value}",
|
108
|
+
data=self.model_dump(),
|
109
|
+
model=self.__class__,
|
110
|
+
pydantic_error=validation_error
|
111
|
+
)
|
112
|
+
|
113
|
+
if max_value is not None and self.answer > max_value:
|
114
|
+
validation_error = ValidationError.from_exception_data(
|
115
|
+
title='ConstrainedNumericResponse',
|
116
|
+
line_errors=[{
|
117
|
+
'type': 'value_error',
|
118
|
+
'loc': ('answer',),
|
119
|
+
'msg': f'Answer must be less than or equal to {max_value}',
|
120
|
+
'input': self.answer,
|
121
|
+
'ctx': {'error': 'Value too large'}
|
122
|
+
}]
|
123
|
+
)
|
124
|
+
raise QuestionAnswerValidationError(
|
125
|
+
message=f"Answer {self.answer} must be less than or equal to {max_value}",
|
126
|
+
data=self.model_dump(),
|
127
|
+
model=self.__class__,
|
128
|
+
pydantic_error=validation_error
|
129
|
+
)
|
130
|
+
return self
|
131
|
+
|
33
132
|
return ConstrainedNumericResponse
|
34
133
|
|
35
134
|
|
36
135
|
class NumericalResponseValidator(ResponseValidatorABC):
|
136
|
+
"""
|
137
|
+
Validator for numerical question responses.
|
138
|
+
|
139
|
+
This class implements the validation and fixing logic for numerical responses.
|
140
|
+
It ensures that responses contain valid numbers within specified ranges and
|
141
|
+
provides methods to fix common issues in responses.
|
142
|
+
|
143
|
+
Attributes:
|
144
|
+
required_params: List of required parameters for validation.
|
145
|
+
valid_examples: Examples of valid responses for testing.
|
146
|
+
invalid_examples: Examples of invalid responses for testing.
|
147
|
+
|
148
|
+
Examples:
|
149
|
+
>>> from edsl import QuestionNumerical
|
150
|
+
>>> q = QuestionNumerical.example()
|
151
|
+
>>> validator = q.response_validator
|
152
|
+
|
153
|
+
>>> # Fix string to number
|
154
|
+
>>> response = {"answer": "42"}
|
155
|
+
>>> fixed = validator.fix(response)
|
156
|
+
>>> fixed
|
157
|
+
{'answer': '42'}
|
158
|
+
|
159
|
+
>>> # Extract number from text
|
160
|
+
>>> response = {"answer": "The answer is 42"}
|
161
|
+
>>> fixed = validator.fix(response)
|
162
|
+
>>> fixed
|
163
|
+
{'answer': '42'}
|
164
|
+
|
165
|
+
>>> # Preserve comments when fixing
|
166
|
+
>>> response = {"answer": "The answer is 42", "comment": "My explanation"}
|
167
|
+
>>> fixed = validator.fix(response)
|
168
|
+
>>> fixed
|
169
|
+
{'answer': '42', 'comment': 'My explanation'}
|
170
|
+
"""
|
37
171
|
required_params = ["min_value", "max_value", "permissive"]
|
38
172
|
|
39
173
|
valid_examples = [
|
@@ -48,30 +182,76 @@ class NumericalResponseValidator(ResponseValidatorABC):
|
|
48
182
|
]
|
49
183
|
|
50
184
|
def fix(self, response, verbose=False):
|
185
|
+
"""
|
186
|
+
Fix common issues in numerical responses.
|
187
|
+
|
188
|
+
This method attempts to extract valid numbers from text responses,
|
189
|
+
handle formatting issues, and ensure the response contains a valid number.
|
190
|
+
|
191
|
+
Args:
|
192
|
+
response: The response dictionary to fix.
|
193
|
+
verbose: If True, print information about the fixing process.
|
194
|
+
|
195
|
+
Returns:
|
196
|
+
A fixed version of the response dictionary.
|
197
|
+
|
198
|
+
Notes:
|
199
|
+
- Attempts to extract numbers using regex pattern matching
|
200
|
+
- Removes commas from numbers (e.g., "1,000" → "1000")
|
201
|
+
- Preserves any comment in the original response
|
202
|
+
"""
|
51
203
|
response_text = str(response).lower()
|
52
|
-
import re
|
53
204
|
|
54
205
|
if verbose:
|
55
|
-
print(f"
|
206
|
+
print(f"Invalid generated tokens was: {response_text}")
|
207
|
+
|
56
208
|
pattern = r"\b\d+(?:\.\d+)?\b"
|
57
209
|
match = re.search(pattern, response_text.replace(",", ""))
|
58
210
|
solution = match.group(0) if match else response.get("answer")
|
211
|
+
|
59
212
|
if verbose:
|
60
213
|
print("Proposed solution is: ", solution)
|
214
|
+
|
61
215
|
if "comment" in response:
|
62
216
|
return {"answer": solution, "comment": response["comment"]}
|
63
217
|
else:
|
64
218
|
return {"answer": solution}
|
65
219
|
|
66
220
|
def _check_constraints(self, pydantic_edsl_answer: BaseModel):
|
221
|
+
"""Method preserved for compatibility, constraints handled in Pydantic model."""
|
67
222
|
pass
|
68
223
|
|
69
224
|
|
70
225
|
class QuestionNumerical(QuestionBase):
|
71
|
-
"""
|
72
|
-
|
73
|
-
|
74
|
-
|
226
|
+
"""
|
227
|
+
A question that prompts the agent to answer with a numerical value.
|
228
|
+
|
229
|
+
QuestionNumerical is designed for responses that must be numbers, with optional
|
230
|
+
range constraints to ensure values fall within acceptable bounds. It's useful for
|
231
|
+
age questions, ratings, measurements, and any scenario requiring numerical answers.
|
232
|
+
|
233
|
+
Attributes:
|
234
|
+
question_type (str): Identifier for this question type, set to "numerical".
|
235
|
+
min_value: Optional lower bound for acceptable answers.
|
236
|
+
max_value: Optional upper bound for acceptable answers.
|
237
|
+
_response_model: Initially None, set by create_response_model().
|
238
|
+
response_validator_class: Class used to validate and fix responses.
|
239
|
+
|
240
|
+
Examples:
|
241
|
+
>>> # Basic self-check passes
|
242
|
+
>>> QuestionNumerical.self_check()
|
243
|
+
|
244
|
+
>>> # Create age question with range constraints
|
245
|
+
>>> q = QuestionNumerical(
|
246
|
+
... question_name="age",
|
247
|
+
... question_text="How old are you in years?",
|
248
|
+
... min_value=0,
|
249
|
+
... max_value=120
|
250
|
+
... )
|
251
|
+
>>> q.min_value
|
252
|
+
0
|
253
|
+
>>> q.max_value
|
254
|
+
120
|
75
255
|
"""
|
76
256
|
|
77
257
|
question_type = "numerical"
|
@@ -92,12 +272,36 @@ class QuestionNumerical(QuestionBase):
|
|
92
272
|
answering_instructions: Optional[str] = None,
|
93
273
|
permissive: bool = False,
|
94
274
|
):
|
95
|
-
"""
|
96
|
-
|
97
|
-
|
98
|
-
:
|
99
|
-
|
100
|
-
|
275
|
+
"""
|
276
|
+
Initialize a new numerical question.
|
277
|
+
|
278
|
+
Args:
|
279
|
+
question_name: Identifier for the question, used in results and templates.
|
280
|
+
Must be a valid Python variable name.
|
281
|
+
question_text: The actual text of the question to be asked.
|
282
|
+
min_value: Optional minimum value for the answer (inclusive).
|
283
|
+
max_value: Optional maximum value for the answer (inclusive).
|
284
|
+
include_comment: Whether to allow comments with the answer.
|
285
|
+
question_presentation: Optional custom presentation template.
|
286
|
+
answering_instructions: Optional additional instructions.
|
287
|
+
permissive: If True, ignore min/max constraints during validation.
|
288
|
+
|
289
|
+
Examples:
|
290
|
+
>>> q = QuestionNumerical(
|
291
|
+
... question_name="temperature",
|
292
|
+
... question_text="What is the temperature in Celsius?",
|
293
|
+
... min_value=-273.15 # Absolute zero
|
294
|
+
... )
|
295
|
+
>>> q.question_name
|
296
|
+
'temperature'
|
297
|
+
|
298
|
+
>>> # Question with both min and max
|
299
|
+
>>> q = QuestionNumerical(
|
300
|
+
... question_name="rating",
|
301
|
+
... question_text="Rate from 1 to 10",
|
302
|
+
... min_value=1,
|
303
|
+
... max_value=10
|
304
|
+
... )
|
101
305
|
"""
|
102
306
|
self.question_name = question_name
|
103
307
|
self.question_text = question_text
|
@@ -110,7 +314,53 @@ class QuestionNumerical(QuestionBase):
|
|
110
314
|
self.permissive = permissive
|
111
315
|
|
112
316
|
def create_response_model(self):
|
317
|
+
"""
|
318
|
+
Create a response model with the appropriate constraints.
|
319
|
+
|
320
|
+
This method creates a Pydantic model customized with the min/max constraints
|
321
|
+
specified for this question instance. If permissive=True, constraints are ignored.
|
322
|
+
|
323
|
+
Returns:
|
324
|
+
A Pydantic model class tailored to this question's constraints.
|
325
|
+
|
326
|
+
Examples:
|
327
|
+
>>> q = QuestionNumerical.example()
|
328
|
+
>>> model = q.create_response_model()
|
329
|
+
>>> model(answer=45).answer
|
330
|
+
45
|
331
|
+
"""
|
113
332
|
return create_numeric_response(self.min_value, self.max_value, self.permissive)
|
333
|
+
|
334
|
+
def _simulate_answer(self, human_readable: bool = False) -> dict:
|
335
|
+
"""
|
336
|
+
Generate a simulated valid answer respecting min/max constraints.
|
337
|
+
|
338
|
+
Overrides the base class method to ensure values are within defined bounds.
|
339
|
+
|
340
|
+
Args:
|
341
|
+
human_readable: Flag for human-readable output (not used for numerical questions)
|
342
|
+
|
343
|
+
Returns:
|
344
|
+
A dictionary with a valid numerical answer within constraints
|
345
|
+
|
346
|
+
Examples:
|
347
|
+
>>> q = QuestionNumerical(question_name="test", question_text="Test", min_value=1, max_value=10)
|
348
|
+
>>> answer = q._simulate_answer()
|
349
|
+
>>> 1 <= answer["answer"] <= 10
|
350
|
+
True
|
351
|
+
"""
|
352
|
+
from random import randint, uniform
|
353
|
+
|
354
|
+
min_val = self.min_value if self.min_value is not None else 0
|
355
|
+
max_val = self.max_value if self.max_value is not None else 100
|
356
|
+
|
357
|
+
# Generate a value within the constraints
|
358
|
+
if isinstance(min_val, int) and isinstance(max_val, int):
|
359
|
+
value = randint(int(min_val), int(max_val))
|
360
|
+
else:
|
361
|
+
value = uniform(float(min_val), float(max_val))
|
362
|
+
|
363
|
+
return {"answer": value, "comment": None, "generated_tokens": None}
|
114
364
|
|
115
365
|
################
|
116
366
|
# Answer methods
|
@@ -118,6 +368,16 @@ class QuestionNumerical(QuestionBase):
|
|
118
368
|
|
119
369
|
@property
|
120
370
|
def question_html_content(self) -> str:
|
371
|
+
"""
|
372
|
+
Generate HTML content for rendering the question in web interfaces.
|
373
|
+
|
374
|
+
This property generates HTML markup for the question when it needs to be
|
375
|
+
displayed in web interfaces or HTML contexts. For a numerical question,
|
376
|
+
this is typically an input element with type="number".
|
377
|
+
|
378
|
+
Returns:
|
379
|
+
str: HTML markup for rendering the question.
|
380
|
+
"""
|
121
381
|
from jinja2 import Template
|
122
382
|
|
123
383
|
question_html_content = Template(
|
@@ -135,7 +395,29 @@ class QuestionNumerical(QuestionBase):
|
|
135
395
|
@classmethod
|
136
396
|
@inject_exception
|
137
397
|
def example(cls, include_comment=False) -> QuestionNumerical:
|
138
|
-
"""
|
398
|
+
"""
|
399
|
+
Create an example instance of a numerical question.
|
400
|
+
|
401
|
+
This class method creates a predefined example of a numerical question
|
402
|
+
for demonstration, testing, and documentation purposes.
|
403
|
+
|
404
|
+
Args:
|
405
|
+
include_comment: Whether to include a comment field with the answer.
|
406
|
+
|
407
|
+
Returns:
|
408
|
+
QuestionNumerical: An example numerical question.
|
409
|
+
|
410
|
+
Examples:
|
411
|
+
>>> q = QuestionNumerical.example()
|
412
|
+
>>> q.question_name
|
413
|
+
'age'
|
414
|
+
>>> q.question_text
|
415
|
+
'You are a 45 year old man. How old are you in years?'
|
416
|
+
>>> q.min_value
|
417
|
+
0
|
418
|
+
>>> q.max_value
|
419
|
+
86.7
|
420
|
+
"""
|
139
421
|
return cls(
|
140
422
|
question_name="age",
|
141
423
|
question_text="You are a 45 year old man. How old are you in years?",
|
@@ -145,7 +427,54 @@ class QuestionNumerical(QuestionBase):
|
|
145
427
|
)
|
146
428
|
|
147
429
|
|
148
|
-
|
430
|
+
def main():
|
431
|
+
"""
|
432
|
+
Demonstrate the functionality of the QuestionNumerical class.
|
433
|
+
|
434
|
+
This function creates an example numerical question and demonstrates its
|
435
|
+
key features including validation, serialization, and answer simulation.
|
436
|
+
It's primarily intended for testing and development purposes.
|
437
|
+
|
438
|
+
Note:
|
439
|
+
This function will be executed when the module is run directly,
|
440
|
+
but not when imported.
|
441
|
+
"""
|
442
|
+
# Create an example question
|
443
|
+
q = QuestionNumerical.example()
|
444
|
+
print(f"Question text: {q.question_text}")
|
445
|
+
print(f"Question name: {q.question_name}")
|
446
|
+
print(f"Min value: {q.min_value}")
|
447
|
+
print(f"Max value: {q.max_value}")
|
448
|
+
|
449
|
+
# Validate an answer
|
450
|
+
valid_answer = {"answer": 42}
|
451
|
+
validated = q._validate_answer(valid_answer)
|
452
|
+
print(f"Validated answer: {validated}")
|
453
|
+
|
454
|
+
# Test constraints - this should be in range
|
455
|
+
valid_constrained = {"answer": 75}
|
456
|
+
constrained = q._validate_answer(valid_constrained)
|
457
|
+
print(f"Valid constrained answer: {constrained}")
|
458
|
+
|
459
|
+
# Simulate an answer
|
460
|
+
simulated = q._simulate_answer()
|
461
|
+
print(f"Simulated answer: {simulated}")
|
462
|
+
|
463
|
+
# Serialization demonstration
|
464
|
+
serialized = q.to_dict()
|
465
|
+
print(f"Serialized: {serialized}")
|
466
|
+
deserialized = QuestionBase.from_dict(serialized)
|
467
|
+
print(f"Deserialization successful: {deserialized.question_text == q.question_text}")
|
468
|
+
|
469
|
+
# Run doctests
|
149
470
|
import doctest
|
471
|
+
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
472
|
+
print("Doctests completed")
|
473
|
+
|
150
474
|
|
475
|
+
if __name__ == "__main__":
|
476
|
+
import doctest
|
151
477
|
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
478
|
+
|
479
|
+
# Uncomment to run demonstration
|
480
|
+
# main()
|