edsl 0.1.48__py3-none-any.whl → 0.1.50__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 +75 -0
- edsl/buckets/model_buckets.py +1 -2
- edsl/buckets/token_bucket.py +11 -6
- edsl/buckets/token_bucket_api.py +1 -2
- edsl/buckets/token_bucket_client.py +9 -7
- edsl/caching/cache.py +7 -2
- edsl/caching/cache_entry.py +10 -9
- edsl/caching/exceptions.py +113 -7
- edsl/caching/remote_cache_sync.py +1 -2
- edsl/caching/sql_dict.py +17 -12
- edsl/cli.py +43 -0
- edsl/config/config_class.py +30 -6
- edsl/conversation/Conversation.py +3 -2
- edsl/conversation/exceptions.py +58 -0
- edsl/conversation/mug_negotiation.py +0 -2
- edsl/coop/__init__.py +20 -1
- edsl/coop/coop.py +129 -38
- edsl/coop/exceptions.py +188 -9
- edsl/coop/price_fetcher.py +3 -6
- edsl/coop/utils.py +4 -6
- edsl/dataset/__init__.py +5 -4
- edsl/dataset/dataset.py +53 -43
- edsl/dataset/dataset_operations_mixin.py +86 -72
- edsl/dataset/dataset_tree.py +9 -5
- edsl/dataset/display/table_display.py +0 -2
- edsl/dataset/display/table_renderers.py +0 -1
- 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 +4 -5
- edsl/inference_services/data_structures.py +9 -6
- 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 +2 -1
- 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 +6 -2
- 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 +3 -2
- edsl/instructions/exceptions.py +61 -0
- edsl/instructions/instruction.py +5 -2
- edsl/instructions/instruction_collection.py +2 -1
- edsl/instructions/instruction_handler.py +4 -9
- edsl/interviews/ReportErrors.py +0 -3
- edsl/interviews/__init__.py +9 -2
- edsl/interviews/answering_function.py +11 -13
- edsl/interviews/exception_tracking.py +14 -7
- edsl/interviews/exceptions.py +79 -0
- edsl/interviews/interview.py +32 -29
- edsl/interviews/interview_status_dictionary.py +4 -2
- edsl/interviews/interview_status_log.py +2 -1
- edsl/interviews/interview_task_manager.py +3 -3
- edsl/interviews/request_token_estimator.py +3 -1
- edsl/interviews/statistics.py +2 -3
- edsl/invigilators/__init__.py +7 -1
- edsl/invigilators/exceptions.py +79 -0
- edsl/invigilators/invigilator_base.py +0 -1
- edsl/invigilators/invigilators.py +8 -12
- 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 +3 -3
- edsl/jobs/async_interview_runner.py +24 -22
- edsl/jobs/check_survey_scenario_compatibility.py +7 -6
- edsl/jobs/data_structures.py +7 -4
- edsl/jobs/exceptions.py +177 -8
- edsl/jobs/fetch_invigilator.py +1 -1
- edsl/jobs/jobs.py +72 -67
- edsl/jobs/jobs_checks.py +2 -3
- edsl/jobs/jobs_component_constructor.py +2 -2
- edsl/jobs/jobs_pricing_estimation.py +3 -2
- edsl/jobs/jobs_remote_inference_logger.py +5 -4
- edsl/jobs/jobs_runner_asyncio.py +1 -2
- edsl/jobs/jobs_runner_status.py +8 -9
- edsl/jobs/remote_inference.py +26 -23
- edsl/jobs/results_exceptions_handler.py +8 -5
- 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 +4 -7
- 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/notebook.py +1 -1
- edsl/notebooks/notebook_to_latex.py +0 -1
- 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 +2 -0
- edsl/prompts/exceptions.py +107 -5
- edsl/prompts/prompt.py +14 -6
- edsl/questions/HTMLQuestion.py +5 -11
- edsl/questions/Quick.py +0 -1
- edsl/questions/__init__.py +2 -0
- edsl/questions/answer_validator_mixin.py +318 -318
- edsl/questions/compose_questions.py +2 -2
- edsl/questions/descriptors.py +10 -49
- 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 +14 -16
- edsl/questions/question_base_gen_mixin.py +2 -2
- edsl/questions/question_base_prompts_mixin.py +9 -3
- edsl/questions/question_budget.py +9 -5
- edsl/questions/question_check_box.py +3 -5
- edsl/questions/question_dict.py +171 -194
- edsl/questions/question_extract.py +1 -1
- edsl/questions/question_free_text.py +4 -6
- edsl/questions/question_functional.py +4 -3
- edsl/questions/question_list.py +36 -9
- edsl/questions/question_matrix.py +95 -61
- edsl/questions/question_multiple_choice.py +6 -4
- edsl/questions/question_numerical.py +2 -4
- edsl/questions/question_registry.py +4 -2
- edsl/questions/register_questions_meta.py +0 -1
- edsl/questions/response_validator_abc.py +7 -13
- 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 +0 -1
- edsl/results/result.py +4 -5
- edsl/results/results.py +10 -51
- 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 +0 -1
- edsl/scenarios/scenario_list.py +152 -18
- 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_export.py +6 -3
- edsl/surveys/survey_flow_visualization.py +10 -1
- edsl/tasks/__init__.py +2 -0
- edsl/tasks/question_task_creator.py +3 -3
- edsl/tasks/task_creators.py +1 -3
- edsl/tasks/task_history.py +5 -7
- edsl/tasks/task_status_log.py +1 -2
- edsl/tokens/__init__.py +3 -1
- edsl/tokens/token_usage.py +1 -1
- 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.48.dist-info → edsl-0.1.50.dist-info}/METADATA +32 -2
- edsl-0.1.50.dist-info/RECORD +363 -0
- edsl-0.1.50.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/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.48.dist-info/RECORD +0 -347
- {edsl-0.1.48.dist-info → edsl-0.1.50.dist-info}/LICENSE +0 -0
- {edsl-0.1.48.dist-info → edsl-0.1.50.dist-info}/WHEEL +0 -0
@@ -1,5 +1,5 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
from typing import
|
2
|
+
from typing import Optional, List
|
3
3
|
|
4
4
|
from pydantic import Field, BaseModel, validator
|
5
5
|
|
@@ -40,14 +40,18 @@ def create_budget_model(
|
|
40
40
|
@validator("answer")
|
41
41
|
def validate_answer(cls, v):
|
42
42
|
if len(v) != len(question_options):
|
43
|
-
|
43
|
+
from .exceptions import QuestionAnswerValidationError
|
44
|
+
raise QuestionAnswerValidationError(f"Must provide {len(question_options)} values")
|
44
45
|
if any(x < 0 for x in v):
|
45
|
-
|
46
|
+
from .exceptions import QuestionAnswerValidationError
|
47
|
+
raise QuestionAnswerValidationError("All values must be non-negative")
|
46
48
|
total = sum(v)
|
47
49
|
if not permissive and total != budget_sum:
|
48
|
-
|
50
|
+
from .exceptions import QuestionAnswerValidationError
|
51
|
+
raise QuestionAnswerValidationError(f"Sum of numbers must equal {budget_sum}")
|
49
52
|
elif permissive and total > budget_sum:
|
50
|
-
|
53
|
+
from .exceptions import QuestionAnswerValidationError
|
54
|
+
raise QuestionAnswerValidationError(f"Sum of numbers cannot exceed {budget_sum}")
|
51
55
|
return v
|
52
56
|
|
53
57
|
class Config:
|
@@ -1,11 +1,9 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
import
|
3
|
-
from typing import Any, Optional, Union, TYPE_CHECKING
|
2
|
+
from typing import Any, Optional, TYPE_CHECKING
|
4
3
|
|
5
4
|
from jinja2 import Template
|
6
|
-
from pydantic import
|
7
|
-
from
|
8
|
-
from typing import List, Literal, Optional, Annotated
|
5
|
+
from pydantic import BaseModel, Field
|
6
|
+
from typing import List, Literal, Annotated
|
9
7
|
|
10
8
|
from .exceptions import QuestionAnswerValidationError
|
11
9
|
from ..scenarios import Scenario
|
edsl/questions/question_dict.py
CHANGED
@@ -1,6 +1,18 @@
|
|
1
|
+
"""
|
2
|
+
question_dict.py
|
3
|
+
|
4
|
+
Drop-in replacement for `QuestionDict`, with dynamic creation of a Pydantic model
|
5
|
+
to validate user responses automatically (just like QuestionNumerical).
|
6
|
+
|
7
|
+
|
8
|
+
Failure:
|
9
|
+
|
10
|
+
```python { "first_name": "Kris", "last_name": "Rosemann", "phone": "(262) 506-6064", "email": "InvestorRelations@generac.com", "title": "Senior Manager Corporate Development & Investor Relations", "external": False } ``` The first name and last name are extracted directly from the text. The phone number and email are provided in the text. The title is also given in the text. The email domain "generac.com" suggests that it is an internal email address, so "external" is set to False.
|
11
|
+
"""
|
12
|
+
|
1
13
|
from __future__ import annotations
|
2
14
|
from typing import Union, Optional, Dict, List, Any, Type
|
3
|
-
from pydantic import BaseModel, Field,
|
15
|
+
from pydantic import BaseModel, Field, create_model, ValidationError
|
4
16
|
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
|
5
17
|
from pathlib import Path
|
6
18
|
|
@@ -16,7 +28,77 @@ from .exceptions import QuestionCreationValidationError
|
|
16
28
|
from .decorators import inject_exception
|
17
29
|
|
18
30
|
|
31
|
+
def _parse_type_string(type_str: str) -> Any:
|
32
|
+
"""
|
33
|
+
Very simplistic parser that can map:
|
34
|
+
- "int" -> int
|
35
|
+
- "float" -> float
|
36
|
+
- "str" -> str
|
37
|
+
- "list[str]" -> List[str]
|
38
|
+
- ...
|
39
|
+
Expand this as needed for more advanced usage.
|
40
|
+
"""
|
41
|
+
type_str = type_str.strip().lower()
|
42
|
+
if type_str == "int":
|
43
|
+
return int
|
44
|
+
elif type_str == "float":
|
45
|
+
return float
|
46
|
+
elif type_str == "str":
|
47
|
+
return str
|
48
|
+
elif type_str == "list":
|
49
|
+
return List[Any]
|
50
|
+
elif type_str.startswith("list["):
|
51
|
+
# e.g. "list[str]" or "list[int]" etc.
|
52
|
+
inner = type_str[len("list["):-1].strip()
|
53
|
+
return List[_parse_type_string(inner)]
|
54
|
+
# If none matched, return a very permissive type or raise an error
|
55
|
+
return Any
|
56
|
+
|
57
|
+
|
58
|
+
def create_dict_response(
|
59
|
+
answer_keys: List[str],
|
60
|
+
value_types: List[str],
|
61
|
+
permissive: bool = False,
|
62
|
+
) -> Type[BaseModel]:
|
63
|
+
"""
|
64
|
+
Dynamically builds a Pydantic model that has:
|
65
|
+
- an `answer` submodel containing your required keys
|
66
|
+
- an optional `comment` field
|
67
|
+
|
68
|
+
If `permissive=False`, extra keys in `answer` are forbidden.
|
69
|
+
If `permissive=True`, extra keys in `answer` are allowed.
|
70
|
+
"""
|
71
|
+
|
72
|
+
# 1) Build the 'answer' submodel fields
|
73
|
+
# Each key is required (using `...`), with the associated type from value_types.
|
74
|
+
field_definitions = {}
|
75
|
+
for key, t_str in zip(answer_keys, value_types):
|
76
|
+
python_type = _parse_type_string(t_str)
|
77
|
+
field_definitions[key] = (python_type, Field(...))
|
78
|
+
|
79
|
+
# Use Pydantic's create_model to construct an "AnswerSubModel" with these fields
|
80
|
+
AnswerSubModel = create_model(
|
81
|
+
"AnswerSubModel",
|
82
|
+
__base__=BaseModel,
|
83
|
+
**field_definitions
|
84
|
+
)
|
85
|
+
|
86
|
+
# 2) Define the top-level model with `answer` + optional `comment`
|
87
|
+
class DictResponse(BaseModel):
|
88
|
+
answer: AnswerSubModel
|
89
|
+
comment: Optional[str] = None
|
90
|
+
generated_tokens: Optional[Any] = Field(None)
|
91
|
+
|
92
|
+
class Config:
|
93
|
+
# If permissive=False, forbid extra keys in `answer`
|
94
|
+
# If permissive=True, allow them
|
95
|
+
extra = "allow" if permissive else "forbid"
|
96
|
+
|
97
|
+
return DictResponse
|
98
|
+
|
99
|
+
|
19
100
|
class DictResponseValidator(ResponseValidatorABC):
|
101
|
+
"""Optional placeholder if you still want a validator class around it."""
|
20
102
|
required_params = ["answer_keys", "permissive"]
|
21
103
|
|
22
104
|
valid_examples = [
|
@@ -42,17 +124,19 @@ class DictResponseValidator(ResponseValidatorABC):
|
|
42
124
|
),
|
43
125
|
(
|
44
126
|
{"answer": {"ingredients": "milk"}}, # Should be a list
|
45
|
-
{"answer_keys": ["ingredients"], "value_types": ["list"]},
|
127
|
+
{"answer_keys": ["ingredients"], "value_types": ["list[str]"]},
|
46
128
|
"Key 'ingredients' should be a list, got str",
|
47
129
|
)
|
48
130
|
]
|
49
131
|
|
50
132
|
|
51
133
|
class QuestionDict(QuestionBase):
|
52
|
-
"""
|
134
|
+
"""A QuestionDict allows you to create questions that expect dictionary responses
|
135
|
+
with specific keys and value types. It dynamically builds a pydantic model
|
136
|
+
so that Pydantic automatically raises ValidationError for missing/invalid fields.
|
137
|
+
|
138
|
+
Documentation: https://docs.expectedparrot.com/en/latest/questions.html#questiondict
|
53
139
|
|
54
|
-
Documenation: https://docs.expectedparrot.com/en/latest/questions.html#questiondict
|
55
|
-
|
56
140
|
Parameters
|
57
141
|
----------
|
58
142
|
question_name : str
|
@@ -73,16 +157,8 @@ class QuestionDict(QuestionBase):
|
|
73
157
|
Additional instructions for answering
|
74
158
|
permissive : bool
|
75
159
|
If True, allows additional keys not specified in answer_keys
|
76
|
-
|
77
|
-
Examples
|
78
|
-
--------
|
79
|
-
>>> q = QuestionDict(
|
80
|
-
... question_name="tweet",
|
81
|
-
... question_text="Draft a tweet.",
|
82
|
-
... answer_keys=["text", "characters"],
|
83
|
-
... value_descriptions=["The text of the tweet", "The number of characters in the tweet"]
|
84
|
-
... )
|
85
160
|
"""
|
161
|
+
|
86
162
|
question_type = "dict"
|
87
163
|
question_text: str = QuestionTextDescriptor()
|
88
164
|
answer_keys: List[str] = AnswerKeysDescriptor()
|
@@ -92,121 +168,6 @@ class QuestionDict(QuestionBase):
|
|
92
168
|
_response_model = None
|
93
169
|
response_validator_class = DictResponseValidator
|
94
170
|
|
95
|
-
def _get_default_answer(self) -> Dict[str, Any]:
|
96
|
-
"""Get default answer based on types."""
|
97
|
-
answer = {}
|
98
|
-
if not self.value_types:
|
99
|
-
return {
|
100
|
-
"title": "Sample Recipe",
|
101
|
-
"ingredients": ["ingredient1", "ingredient2"],
|
102
|
-
"num_ingredients": 2,
|
103
|
-
"instructions": "Sample instructions"
|
104
|
-
}
|
105
|
-
|
106
|
-
for key, type_str in zip(self.answer_keys, self.value_types):
|
107
|
-
if type_str.startswith(('list[', 'list')):
|
108
|
-
if '[' in type_str:
|
109
|
-
element_type = type_str[type_str.index('[') + 1:type_str.rindex(']')].lower()
|
110
|
-
if element_type == 'str':
|
111
|
-
answer[key] = ["sample_string"]
|
112
|
-
elif element_type == 'int':
|
113
|
-
answer[key] = [1]
|
114
|
-
elif element_type == 'float':
|
115
|
-
answer[key] = [1.0]
|
116
|
-
else:
|
117
|
-
answer[key] = []
|
118
|
-
else:
|
119
|
-
answer[key] = []
|
120
|
-
else:
|
121
|
-
if type_str == 'str':
|
122
|
-
answer[key] = "sample_string"
|
123
|
-
elif type_str == 'int':
|
124
|
-
answer[key] = 1
|
125
|
-
elif type_str == 'float':
|
126
|
-
answer[key] = 1.0
|
127
|
-
else:
|
128
|
-
answer[key] = None
|
129
|
-
|
130
|
-
return answer
|
131
|
-
|
132
|
-
def create_response_model(
|
133
|
-
self,
|
134
|
-
) -> Type[BaseModel]:
|
135
|
-
"""Create a response model for dict questions."""
|
136
|
-
default_answer = self._get_default_answer()
|
137
|
-
|
138
|
-
class DictResponse(BaseModel):
|
139
|
-
answer: Dict[str, Any] = Field(
|
140
|
-
default_factory=lambda: default_answer.copy()
|
141
|
-
)
|
142
|
-
comment: Optional[str] = None
|
143
|
-
|
144
|
-
@field_validator("answer")
|
145
|
-
def validate_answer(cls, v, values, **kwargs):
|
146
|
-
# Ensure all keys exist
|
147
|
-
missing_keys = set(self.answer_keys) - set(v.keys())
|
148
|
-
if missing_keys:
|
149
|
-
raise ValueError(f"Missing required keys: {missing_keys}")
|
150
|
-
|
151
|
-
# Validate value types if not permissive
|
152
|
-
if not self.permissive and self.value_types:
|
153
|
-
for key, type_str in zip(self.answer_keys, self.value_types):
|
154
|
-
if key not in v:
|
155
|
-
continue
|
156
|
-
|
157
|
-
value = v[key]
|
158
|
-
type_str = type_str.lower() # Normalize to lowercase
|
159
|
-
|
160
|
-
# Handle list types
|
161
|
-
if type_str.startswith(('list[', 'list')):
|
162
|
-
if not isinstance(value, list):
|
163
|
-
raise ValueError(f"Key '{key}' should be a list, got {type(value).__name__}")
|
164
|
-
|
165
|
-
# If it's a parameterized list, check element types
|
166
|
-
if '[' in type_str:
|
167
|
-
element_type = type_str[type_str.index('[') + 1:type_str.rindex(']')]
|
168
|
-
element_type = element_type.lower().strip()
|
169
|
-
|
170
|
-
for i, elem in enumerate(value):
|
171
|
-
expected_type = {
|
172
|
-
'str': str,
|
173
|
-
'int': int,
|
174
|
-
'float': float,
|
175
|
-
'list': list
|
176
|
-
}.get(element_type)
|
177
|
-
|
178
|
-
if expected_type and not isinstance(elem, expected_type):
|
179
|
-
raise ValueError(
|
180
|
-
f"List element at index {i} for key '{key}' "
|
181
|
-
f"has type {type(elem).__name__}, expected {element_type}"
|
182
|
-
)
|
183
|
-
else:
|
184
|
-
# Handle basic types
|
185
|
-
expected_type = {
|
186
|
-
'str': str,
|
187
|
-
'int': int,
|
188
|
-
'float': float,
|
189
|
-
'list': list,
|
190
|
-
}.get(type_str)
|
191
|
-
|
192
|
-
if expected_type and not isinstance(value, expected_type):
|
193
|
-
raise ValueError(
|
194
|
-
f"Key '{key}' has value of type {type(value).__name__}, expected {type_str}"
|
195
|
-
)
|
196
|
-
return v
|
197
|
-
|
198
|
-
model_config = {
|
199
|
-
"json_schema_extra": {
|
200
|
-
"examples": [{
|
201
|
-
"answer": default_answer,
|
202
|
-
"comment": None
|
203
|
-
}]
|
204
|
-
}
|
205
|
-
}
|
206
|
-
|
207
|
-
DictResponse.__name__ = "DictResponse"
|
208
|
-
return DictResponse
|
209
|
-
|
210
171
|
def __init__(
|
211
172
|
self,
|
212
173
|
question_name: str,
|
@@ -243,67 +204,54 @@ class QuestionDict(QuestionBase):
|
|
243
204
|
"Length of value_descriptions must match length of answer_keys."
|
244
205
|
)
|
245
206
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
return 'list'
|
257
|
-
|
258
|
-
# Handle string inputs
|
259
|
-
if isinstance(t, str):
|
260
|
-
t = t.lower()
|
261
|
-
# Handle list types
|
262
|
-
if t.startswith(('list[', 'list')):
|
263
|
-
if '[' in t:
|
264
|
-
# Normalize the inner type
|
265
|
-
inner_type = t[t.index('[') + 1:t.rindex(']')].strip().lower()
|
266
|
-
return f"list[{inner_type}]"
|
267
|
-
return "list"
|
268
|
-
return t
|
269
|
-
|
270
|
-
# Handle List the same as list
|
271
|
-
if t_str == "<class 'List'>":
|
272
|
-
return "list"
|
273
|
-
|
274
|
-
# If it's list type
|
275
|
-
if t is list:
|
276
|
-
return "list"
|
277
|
-
|
278
|
-
# If it's a basic type
|
279
|
-
if hasattr(t, "__name__"):
|
280
|
-
return t.__name__.lower()
|
281
|
-
|
282
|
-
# If it's a typing.List
|
283
|
-
if t_str.startswith(('list[', 'list')):
|
284
|
-
return t_str.replace('typing.', '').lower()
|
285
|
-
|
286
|
-
# Handle generic types
|
287
|
-
if hasattr(t, "__origin__"):
|
288
|
-
origin = t.__origin__.__name__.lower()
|
289
|
-
args = [
|
290
|
-
arg.__name__.lower() if hasattr(arg, "__name__") else str(arg).lower()
|
291
|
-
for arg in t.__args__
|
292
|
-
]
|
293
|
-
return f"{origin}[{', '.join(args)}]"
|
207
|
+
def create_response_model(self) -> Type[BaseModel]:
|
208
|
+
"""
|
209
|
+
Build and return the Pydantic model that should parse/validate user answers.
|
210
|
+
This is similar to `QuestionNumerical.create_response_model`, but for dicts.
|
211
|
+
"""
|
212
|
+
return create_dict_response(
|
213
|
+
answer_keys=self.answer_keys,
|
214
|
+
value_types=self.value_types or [],
|
215
|
+
permissive=self.permissive
|
216
|
+
)
|
294
217
|
|
295
|
-
|
296
|
-
|
297
|
-
|
218
|
+
def _get_default_answer(self) -> Dict[str, Any]:
|
219
|
+
"""Build a default example answer based on the declared types."""
|
220
|
+
if not self.value_types:
|
221
|
+
# If user didn't specify types, return some default structure
|
222
|
+
return {
|
223
|
+
"title": "Sample Recipe",
|
224
|
+
"ingredients": ["ingredient1", "ingredient2"],
|
225
|
+
"num_ingredients": 2,
|
226
|
+
"instructions": "Sample instructions"
|
227
|
+
}
|
298
228
|
|
299
|
-
|
300
|
-
for
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
229
|
+
answer = {}
|
230
|
+
for key, type_str in zip(self.answer_keys, self.value_types):
|
231
|
+
t_str = type_str.lower()
|
232
|
+
if t_str.startswith("list["):
|
233
|
+
# e.g. list[str], list[int], etc.
|
234
|
+
inner = t_str[len("list["):-1].strip()
|
235
|
+
if inner == "str":
|
236
|
+
answer[key] = ["sample_string"]
|
237
|
+
elif inner == "int":
|
238
|
+
answer[key] = [1]
|
239
|
+
elif inner == "float":
|
240
|
+
answer[key] = [1.0]
|
241
|
+
else:
|
242
|
+
answer[key] = []
|
243
|
+
elif t_str == "str":
|
244
|
+
answer[key] = "sample_string"
|
245
|
+
elif t_str == "int":
|
246
|
+
answer[key] = 1
|
247
|
+
elif t_str == "float":
|
248
|
+
answer[key] = 1.0
|
249
|
+
elif t_str == "list":
|
250
|
+
answer[key] = []
|
251
|
+
else:
|
252
|
+
# fallback
|
253
|
+
answer[key] = None
|
254
|
+
return answer
|
307
255
|
|
308
256
|
def _render_template(self, template_name: str) -> str:
|
309
257
|
"""Render a template using Jinja."""
|
@@ -322,6 +270,34 @@ class QuestionDict(QuestionBase):
|
|
322
270
|
except TemplateNotFound:
|
323
271
|
return f"Template {template_name} not found in {template_dir}."
|
324
272
|
|
273
|
+
@staticmethod
|
274
|
+
def _normalize_value_types(value_types: Optional[List[Union[str, type]]]) -> Optional[List[str]]:
|
275
|
+
"""
|
276
|
+
Convert all value_types to string representations (e.g. "int", "list[str]", etc.).
|
277
|
+
This logic is similar to your original approach but expanded to handle
|
278
|
+
python `type` objects as well as string hints.
|
279
|
+
"""
|
280
|
+
if not value_types:
|
281
|
+
return None
|
282
|
+
|
283
|
+
def normalize_type(t) -> str:
|
284
|
+
# Already a string?
|
285
|
+
if isinstance(t, str):
|
286
|
+
return t.lower().strip()
|
287
|
+
|
288
|
+
# It's a Python built-in type?
|
289
|
+
if hasattr(t, "__name__"):
|
290
|
+
if t.__name__ == "List":
|
291
|
+
return "list"
|
292
|
+
# For int, float, str, etc.
|
293
|
+
return t.__name__.lower()
|
294
|
+
|
295
|
+
# If it's a generic type like List[str], parse from its __origin__ / __args__
|
296
|
+
# or fallback:
|
297
|
+
return str(t).lower()
|
298
|
+
|
299
|
+
return [normalize_type(t) for t in value_types]
|
300
|
+
|
325
301
|
def to_dict(self, add_edsl_version: bool = True) -> dict:
|
326
302
|
"""Serialize to JSON-compatible dictionary."""
|
327
303
|
return {
|
@@ -366,12 +342,13 @@ class QuestionDict(QuestionBase):
|
|
366
342
|
)
|
367
343
|
|
368
344
|
def _simulate_answer(self) -> dict:
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
345
|
+
"""Simulate an answer for the question."""
|
346
|
+
return {
|
347
|
+
"answer": self._get_default_answer(),
|
348
|
+
"comment": None
|
349
|
+
}
|
350
|
+
|
374
351
|
|
375
352
|
if __name__ == "__main__":
|
376
353
|
q = QuestionDict.example()
|
377
|
-
print(q.to_dict())
|
354
|
+
print(q.to_dict())
|
@@ -1,14 +1,11 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
from typing import
|
3
|
-
from typing import Optional, Any, List
|
2
|
+
from typing import Optional
|
4
3
|
|
5
4
|
from uuid import uuid4
|
6
5
|
|
7
|
-
from pydantic import
|
6
|
+
from pydantic import model_validator, BaseModel
|
8
7
|
|
9
|
-
from ..prompts import Prompt
|
10
8
|
|
11
|
-
from .exceptions import QuestionAnswerValidationError
|
12
9
|
from .question_base import QuestionBase
|
13
10
|
from .response_validator_abc import ResponseValidatorABC
|
14
11
|
from .decorators import inject_exception
|
@@ -48,7 +45,8 @@ class FreeTextResponse(BaseModel):
|
|
48
45
|
if self.generated_tokens is not None: # If generated_tokens exists
|
49
46
|
# Ensure exact string equality
|
50
47
|
if self.answer.strip() != self.generated_tokens.strip(): # They MUST match exactly
|
51
|
-
|
48
|
+
from .exceptions import QuestionAnswerValidationError
|
49
|
+
raise QuestionAnswerValidationError(
|
52
50
|
f"answer '{self.answer}' must exactly match generated_tokens '{self.generated_tokens}'. "
|
53
51
|
f"Type of answer: {type(self.answer)}, Type of tokens: {type(self.generated_tokens)}"
|
54
52
|
)
|
@@ -5,7 +5,6 @@ import inspect
|
|
5
5
|
from .question_base import QuestionBase
|
6
6
|
|
7
7
|
from ..utilities.restricted_python import create_restricted_function
|
8
|
-
from ..utilities.decorators import add_edsl_version, remove_edsl_version
|
9
8
|
|
10
9
|
|
11
10
|
class QuestionFunctional(QuestionBase):
|
@@ -99,11 +98,13 @@ class QuestionFunctional(QuestionBase):
|
|
99
98
|
|
100
99
|
def _simulate_answer(self, human_readable=True) -> dict[str, str]:
|
101
100
|
"""Required by Question, but not used by QuestionFunctional."""
|
102
|
-
|
101
|
+
from .exceptions import QuestionNotImplementedError
|
102
|
+
raise QuestionNotImplementedError("_simulate_answer not implemented for QuestionFunctional")
|
103
103
|
|
104
104
|
def _validate_answer(self, answer: dict[str, str]):
|
105
105
|
"""Required by Question, but not used by QuestionFunctional."""
|
106
|
-
|
106
|
+
from .exceptions import QuestionNotImplementedError
|
107
|
+
raise QuestionNotImplementedError("_validate_answer not implemented for QuestionFunctional")
|
107
108
|
|
108
109
|
@property
|
109
110
|
def question_html_content(self) -> str:
|