bead 0.1.0__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.
- bead/__init__.py +11 -0
- bead/__main__.py +11 -0
- bead/active_learning/__init__.py +15 -0
- bead/active_learning/config.py +231 -0
- bead/active_learning/loop.py +566 -0
- bead/active_learning/models/__init__.py +24 -0
- bead/active_learning/models/base.py +852 -0
- bead/active_learning/models/binary.py +910 -0
- bead/active_learning/models/categorical.py +943 -0
- bead/active_learning/models/cloze.py +862 -0
- bead/active_learning/models/forced_choice.py +956 -0
- bead/active_learning/models/free_text.py +773 -0
- bead/active_learning/models/lora.py +365 -0
- bead/active_learning/models/magnitude.py +835 -0
- bead/active_learning/models/multi_select.py +795 -0
- bead/active_learning/models/ordinal_scale.py +811 -0
- bead/active_learning/models/peft_adapter.py +155 -0
- bead/active_learning/models/random_effects.py +639 -0
- bead/active_learning/selection.py +354 -0
- bead/active_learning/strategies.py +391 -0
- bead/active_learning/trainers/__init__.py +26 -0
- bead/active_learning/trainers/base.py +210 -0
- bead/active_learning/trainers/data_collator.py +172 -0
- bead/active_learning/trainers/dataset_utils.py +261 -0
- bead/active_learning/trainers/huggingface.py +304 -0
- bead/active_learning/trainers/lightning.py +324 -0
- bead/active_learning/trainers/metrics.py +424 -0
- bead/active_learning/trainers/mixed_effects.py +551 -0
- bead/active_learning/trainers/model_wrapper.py +509 -0
- bead/active_learning/trainers/registry.py +104 -0
- bead/adapters/__init__.py +11 -0
- bead/adapters/huggingface.py +61 -0
- bead/behavioral/__init__.py +116 -0
- bead/behavioral/analytics.py +646 -0
- bead/behavioral/extraction.py +343 -0
- bead/behavioral/merging.py +343 -0
- bead/cli/__init__.py +11 -0
- bead/cli/active_learning.py +513 -0
- bead/cli/active_learning_commands.py +779 -0
- bead/cli/completion.py +359 -0
- bead/cli/config.py +624 -0
- bead/cli/constraint_builders.py +286 -0
- bead/cli/deployment.py +859 -0
- bead/cli/deployment_trials.py +493 -0
- bead/cli/deployment_ui.py +332 -0
- bead/cli/display.py +378 -0
- bead/cli/items.py +960 -0
- bead/cli/items_factories.py +776 -0
- bead/cli/list_constraints.py +714 -0
- bead/cli/lists.py +490 -0
- bead/cli/main.py +430 -0
- bead/cli/models.py +877 -0
- bead/cli/resource_loaders.py +621 -0
- bead/cli/resources.py +1036 -0
- bead/cli/shell.py +356 -0
- bead/cli/simulate.py +840 -0
- bead/cli/templates.py +1158 -0
- bead/cli/training.py +1080 -0
- bead/cli/utils.py +614 -0
- bead/cli/workflow.py +1273 -0
- bead/config/__init__.py +68 -0
- bead/config/active_learning.py +1009 -0
- bead/config/config.py +192 -0
- bead/config/defaults.py +118 -0
- bead/config/deployment.py +217 -0
- bead/config/env.py +147 -0
- bead/config/item.py +45 -0
- bead/config/list.py +193 -0
- bead/config/loader.py +149 -0
- bead/config/logging.py +42 -0
- bead/config/model.py +49 -0
- bead/config/paths.py +46 -0
- bead/config/profiles.py +320 -0
- bead/config/resources.py +47 -0
- bead/config/serialization.py +210 -0
- bead/config/simulation.py +206 -0
- bead/config/template.py +238 -0
- bead/config/validation.py +267 -0
- bead/data/__init__.py +65 -0
- bead/data/base.py +87 -0
- bead/data/identifiers.py +97 -0
- bead/data/language_codes.py +61 -0
- bead/data/metadata.py +270 -0
- bead/data/range.py +123 -0
- bead/data/repository.py +358 -0
- bead/data/serialization.py +249 -0
- bead/data/timestamps.py +89 -0
- bead/data/validation.py +349 -0
- bead/data_collection/__init__.py +11 -0
- bead/data_collection/jatos.py +223 -0
- bead/data_collection/merger.py +154 -0
- bead/data_collection/prolific.py +198 -0
- bead/deployment/__init__.py +5 -0
- bead/deployment/distribution.py +402 -0
- bead/deployment/jatos/__init__.py +1 -0
- bead/deployment/jatos/api.py +200 -0
- bead/deployment/jatos/exporter.py +210 -0
- bead/deployment/jspsych/__init__.py +9 -0
- bead/deployment/jspsych/biome.json +44 -0
- bead/deployment/jspsych/config.py +411 -0
- bead/deployment/jspsych/generator.py +598 -0
- bead/deployment/jspsych/package.json +51 -0
- bead/deployment/jspsych/pnpm-lock.yaml +2141 -0
- bead/deployment/jspsych/randomizer.py +299 -0
- bead/deployment/jspsych/src/lib/list-distributor.test.ts +327 -0
- bead/deployment/jspsych/src/lib/list-distributor.ts +1282 -0
- bead/deployment/jspsych/src/lib/randomizer.test.ts +232 -0
- bead/deployment/jspsych/src/lib/randomizer.ts +367 -0
- bead/deployment/jspsych/src/plugins/cloze-dropdown.ts +252 -0
- bead/deployment/jspsych/src/plugins/forced-choice.ts +265 -0
- bead/deployment/jspsych/src/plugins/plugins.test.ts +141 -0
- bead/deployment/jspsych/src/plugins/rating.ts +248 -0
- bead/deployment/jspsych/src/slopit/index.ts +9 -0
- bead/deployment/jspsych/src/types/jatos.d.ts +256 -0
- bead/deployment/jspsych/src/types/jspsych.d.ts +228 -0
- bead/deployment/jspsych/templates/experiment.css +1 -0
- bead/deployment/jspsych/templates/experiment.js.template +289 -0
- bead/deployment/jspsych/templates/index.html +51 -0
- bead/deployment/jspsych/templates/randomizer.js +241 -0
- bead/deployment/jspsych/templates/randomizer.js.template +313 -0
- bead/deployment/jspsych/trials.py +723 -0
- bead/deployment/jspsych/tsconfig.json +23 -0
- bead/deployment/jspsych/tsup.config.ts +30 -0
- bead/deployment/jspsych/ui/__init__.py +1 -0
- bead/deployment/jspsych/ui/components.py +383 -0
- bead/deployment/jspsych/ui/styles.py +411 -0
- bead/dsl/__init__.py +80 -0
- bead/dsl/ast.py +168 -0
- bead/dsl/context.py +178 -0
- bead/dsl/errors.py +71 -0
- bead/dsl/evaluator.py +570 -0
- bead/dsl/grammar.lark +81 -0
- bead/dsl/parser.py +231 -0
- bead/dsl/stdlib.py +929 -0
- bead/evaluation/__init__.py +13 -0
- bead/evaluation/convergence.py +485 -0
- bead/evaluation/interannotator.py +398 -0
- bead/items/__init__.py +40 -0
- bead/items/adapters/__init__.py +70 -0
- bead/items/adapters/anthropic.py +224 -0
- bead/items/adapters/api_utils.py +167 -0
- bead/items/adapters/base.py +216 -0
- bead/items/adapters/google.py +259 -0
- bead/items/adapters/huggingface.py +1074 -0
- bead/items/adapters/openai.py +323 -0
- bead/items/adapters/registry.py +202 -0
- bead/items/adapters/sentence_transformers.py +224 -0
- bead/items/adapters/togetherai.py +309 -0
- bead/items/binary.py +515 -0
- bead/items/cache.py +558 -0
- bead/items/categorical.py +593 -0
- bead/items/cloze.py +757 -0
- bead/items/constructor.py +784 -0
- bead/items/forced_choice.py +413 -0
- bead/items/free_text.py +681 -0
- bead/items/generation.py +432 -0
- bead/items/item.py +396 -0
- bead/items/item_template.py +787 -0
- bead/items/magnitude.py +573 -0
- bead/items/multi_select.py +621 -0
- bead/items/ordinal_scale.py +569 -0
- bead/items/scoring.py +448 -0
- bead/items/validation.py +723 -0
- bead/lists/__init__.py +30 -0
- bead/lists/balancer.py +263 -0
- bead/lists/constraints.py +1067 -0
- bead/lists/experiment_list.py +286 -0
- bead/lists/list_collection.py +378 -0
- bead/lists/partitioner.py +1141 -0
- bead/lists/stratification.py +254 -0
- bead/participants/__init__.py +73 -0
- bead/participants/collection.py +699 -0
- bead/participants/merging.py +312 -0
- bead/participants/metadata_spec.py +491 -0
- bead/participants/models.py +276 -0
- bead/resources/__init__.py +29 -0
- bead/resources/adapters/__init__.py +19 -0
- bead/resources/adapters/base.py +104 -0
- bead/resources/adapters/cache.py +128 -0
- bead/resources/adapters/glazing.py +508 -0
- bead/resources/adapters/registry.py +117 -0
- bead/resources/adapters/unimorph.py +796 -0
- bead/resources/classification.py +856 -0
- bead/resources/constraint_builders.py +329 -0
- bead/resources/constraints.py +165 -0
- bead/resources/lexical_item.py +223 -0
- bead/resources/lexicon.py +744 -0
- bead/resources/loaders.py +209 -0
- bead/resources/template.py +441 -0
- bead/resources/template_collection.py +707 -0
- bead/resources/template_generation.py +349 -0
- bead/simulation/__init__.py +29 -0
- bead/simulation/annotators/__init__.py +15 -0
- bead/simulation/annotators/base.py +175 -0
- bead/simulation/annotators/distance_based.py +135 -0
- bead/simulation/annotators/lm_based.py +114 -0
- bead/simulation/annotators/oracle.py +182 -0
- bead/simulation/annotators/random.py +181 -0
- bead/simulation/dsl_extension/__init__.py +3 -0
- bead/simulation/noise_models/__init__.py +13 -0
- bead/simulation/noise_models/base.py +42 -0
- bead/simulation/noise_models/random_noise.py +82 -0
- bead/simulation/noise_models/systematic.py +132 -0
- bead/simulation/noise_models/temperature.py +86 -0
- bead/simulation/runner.py +144 -0
- bead/simulation/strategies/__init__.py +23 -0
- bead/simulation/strategies/base.py +123 -0
- bead/simulation/strategies/binary.py +103 -0
- bead/simulation/strategies/categorical.py +123 -0
- bead/simulation/strategies/cloze.py +224 -0
- bead/simulation/strategies/forced_choice.py +127 -0
- bead/simulation/strategies/free_text.py +105 -0
- bead/simulation/strategies/magnitude.py +116 -0
- bead/simulation/strategies/multi_select.py +129 -0
- bead/simulation/strategies/ordinal_scale.py +131 -0
- bead/templates/__init__.py +27 -0
- bead/templates/adapters/__init__.py +17 -0
- bead/templates/adapters/base.py +128 -0
- bead/templates/adapters/cache.py +178 -0
- bead/templates/adapters/huggingface.py +312 -0
- bead/templates/combinatorics.py +103 -0
- bead/templates/filler.py +605 -0
- bead/templates/renderers.py +177 -0
- bead/templates/resolver.py +178 -0
- bead/templates/strategies.py +1806 -0
- bead/templates/streaming.py +195 -0
- bead-0.1.0.dist-info/METADATA +212 -0
- bead-0.1.0.dist-info/RECORD +231 -0
- bead-0.1.0.dist-info/WHEEL +4 -0
- bead-0.1.0.dist-info/entry_points.txt +2 -0
- bead-0.1.0.dist-info/licenses/LICENSE +21 -0
bead/items/validation.py
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
"""Validation utilities for constructed items.
|
|
2
|
+
|
|
3
|
+
This module provides validation functions to ensure constructed items
|
|
4
|
+
meet all requirements and contain complete, valid data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from bead.items.item import Item, ModelOutput
|
|
10
|
+
from bead.items.item_template import ItemTemplate, TaskType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_item(item: Item, item_template: ItemTemplate) -> list[str]:
|
|
14
|
+
"""Validate a constructed item against its template.
|
|
15
|
+
|
|
16
|
+
Check that the item has all required fields, references valid templates,
|
|
17
|
+
has consistent constraint satisfaction, and contains valid model outputs.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
item : Item
|
|
22
|
+
Item to validate.
|
|
23
|
+
item_template : ItemTemplate
|
|
24
|
+
Template the item was constructed from.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
list[str]
|
|
29
|
+
List of validation error messages. Empty list if valid.
|
|
30
|
+
|
|
31
|
+
Examples
|
|
32
|
+
--------
|
|
33
|
+
>>> errors = validate_item(item, template)
|
|
34
|
+
>>> if errors:
|
|
35
|
+
... print(f"Item is invalid: {errors}")
|
|
36
|
+
>>> else:
|
|
37
|
+
... print("Item is valid")
|
|
38
|
+
"""
|
|
39
|
+
errors: list[str] = []
|
|
40
|
+
|
|
41
|
+
# Check item_template_id matches
|
|
42
|
+
if item.item_template_id != item_template.id:
|
|
43
|
+
errors.append(
|
|
44
|
+
f"Item template ID mismatch: {item.item_template_id} != {item_template.id}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Check all elements are rendered
|
|
48
|
+
expected_elements = {elem.element_name for elem in item_template.elements}
|
|
49
|
+
actual_elements = set(item.rendered_elements.keys())
|
|
50
|
+
|
|
51
|
+
missing = expected_elements - actual_elements
|
|
52
|
+
if missing:
|
|
53
|
+
errors.append(f"Missing rendered elements: {missing}")
|
|
54
|
+
|
|
55
|
+
extra = actual_elements - expected_elements
|
|
56
|
+
if extra:
|
|
57
|
+
errors.append(f"Extra rendered elements: {extra}")
|
|
58
|
+
|
|
59
|
+
# Check all constraints are evaluated
|
|
60
|
+
expected_constraints = set(item_template.constraints)
|
|
61
|
+
actual_constraints = set(item.constraint_satisfaction.keys())
|
|
62
|
+
|
|
63
|
+
missing_constraints = expected_constraints - actual_constraints
|
|
64
|
+
if missing_constraints:
|
|
65
|
+
errors.append(f"Missing constraint evaluations: {missing_constraints}")
|
|
66
|
+
|
|
67
|
+
# Check model outputs are valid
|
|
68
|
+
for output in item.model_outputs:
|
|
69
|
+
output_errors = validate_model_output(output)
|
|
70
|
+
errors.extend(output_errors)
|
|
71
|
+
|
|
72
|
+
return errors
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def validate_model_output(output: ModelOutput) -> list[str]:
|
|
76
|
+
"""Validate a model output.
|
|
77
|
+
|
|
78
|
+
Check that the model output has all required fields and valid values.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
output : ModelOutput
|
|
83
|
+
Model output to validate.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
list[str]
|
|
88
|
+
List of validation error messages. Empty list if valid.
|
|
89
|
+
|
|
90
|
+
Examples
|
|
91
|
+
--------
|
|
92
|
+
>>> errors = validate_model_output(output)
|
|
93
|
+
>>> if not errors:
|
|
94
|
+
... print("Model output is valid")
|
|
95
|
+
"""
|
|
96
|
+
errors: list[str] = []
|
|
97
|
+
|
|
98
|
+
# Check required fields are not empty
|
|
99
|
+
if not output.model_name or not output.model_name.strip():
|
|
100
|
+
errors.append("Model output has empty model_name")
|
|
101
|
+
|
|
102
|
+
if not output.operation or not output.operation.strip():
|
|
103
|
+
errors.append("Model output has empty operation")
|
|
104
|
+
|
|
105
|
+
if not output.cache_key or not output.cache_key.strip():
|
|
106
|
+
errors.append("Model output has empty cache_key")
|
|
107
|
+
|
|
108
|
+
# Check operation-specific output structure
|
|
109
|
+
if output.operation == "nli":
|
|
110
|
+
# NLI should return dict with entailment/neutral/contradiction
|
|
111
|
+
if not isinstance(output.output, dict):
|
|
112
|
+
errors.append(f"NLI output should be dict, got {type(output.output)}")
|
|
113
|
+
else:
|
|
114
|
+
expected_keys = {"entailment", "neutral", "contradiction"}
|
|
115
|
+
actual_keys = set(output.output.keys()) # type: ignore[union-attr]
|
|
116
|
+
if actual_keys != expected_keys:
|
|
117
|
+
errors.append(
|
|
118
|
+
f"NLI output keys mismatch: expected {expected_keys}, "
|
|
119
|
+
f"got {actual_keys}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
elif output.operation in ("log_probability", "perplexity", "similarity"):
|
|
123
|
+
# These should return numeric values
|
|
124
|
+
if not isinstance(output.output, int | float):
|
|
125
|
+
errors.append(
|
|
126
|
+
f"{output.operation} output should be numeric, "
|
|
127
|
+
f"got {type(output.output)}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
elif output.operation == "embedding":
|
|
131
|
+
# Should return list or array
|
|
132
|
+
if not isinstance(output.output, list | dict):
|
|
133
|
+
# dict could be serialized ndarray
|
|
134
|
+
errors.append(
|
|
135
|
+
f"Embedding output should be list/array, got {type(output.output)}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return errors
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def validate_constraint_satisfaction(
|
|
142
|
+
item: Item, item_template: ItemTemplate
|
|
143
|
+
) -> list[str]:
|
|
144
|
+
"""Validate constraint satisfaction consistency.
|
|
145
|
+
|
|
146
|
+
Check that all constraints in the template have been evaluated and
|
|
147
|
+
that the results are boolean values.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
item : Item
|
|
152
|
+
Item to validate.
|
|
153
|
+
item_template : ItemTemplate
|
|
154
|
+
Template with constraints.
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
list[str]
|
|
159
|
+
List of validation error messages. Empty list if valid.
|
|
160
|
+
|
|
161
|
+
Examples
|
|
162
|
+
--------
|
|
163
|
+
>>> errors = validate_constraint_satisfaction(item, template)
|
|
164
|
+
>>> if not errors:
|
|
165
|
+
... print("Constraint satisfaction is valid")
|
|
166
|
+
"""
|
|
167
|
+
errors: list[str] = []
|
|
168
|
+
|
|
169
|
+
# Check all template constraints are evaluated
|
|
170
|
+
for constraint_id in item_template.constraints:
|
|
171
|
+
if constraint_id not in item.constraint_satisfaction:
|
|
172
|
+
errors.append(f"Constraint {constraint_id} not evaluated")
|
|
173
|
+
else:
|
|
174
|
+
# Check value is boolean
|
|
175
|
+
value = item.constraint_satisfaction[constraint_id]
|
|
176
|
+
if type(value) is not bool:
|
|
177
|
+
errors.append(
|
|
178
|
+
f"Constraint {constraint_id} satisfaction should be bool, "
|
|
179
|
+
f"got {type(value)}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return errors
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def validate_metadata_completeness(item: Item) -> list[str]:
|
|
186
|
+
"""Validate that item metadata is complete.
|
|
187
|
+
|
|
188
|
+
Check that the item has all expected metadata fields populated.
|
|
189
|
+
Since Item inherits from BeadBaseModel, id, created_at, and modified_at
|
|
190
|
+
are always present. This function is kept for consistency and future
|
|
191
|
+
extensibility.
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
item : Item
|
|
196
|
+
Item to validate.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
list[str]
|
|
201
|
+
List of validation error messages. Empty list if valid.
|
|
202
|
+
|
|
203
|
+
Examples
|
|
204
|
+
--------
|
|
205
|
+
>>> errors = validate_metadata_completeness(item)
|
|
206
|
+
>>> if not errors:
|
|
207
|
+
... print("Metadata is complete")
|
|
208
|
+
"""
|
|
209
|
+
errors: list[str] = []
|
|
210
|
+
|
|
211
|
+
# Check base model fields (from BeadBaseModel)
|
|
212
|
+
# These are always present due to Pydantic model initialization,
|
|
213
|
+
# but we check for completeness
|
|
214
|
+
if not hasattr(item, "id"):
|
|
215
|
+
errors.append("Item missing id field") # pragma: no cover
|
|
216
|
+
|
|
217
|
+
if not hasattr(item, "created_at"):
|
|
218
|
+
errors.append("Item missing created_at timestamp") # pragma: no cover
|
|
219
|
+
|
|
220
|
+
if not hasattr(item, "modified_at"):
|
|
221
|
+
errors.append("Item missing modified_at timestamp") # pragma: no cover
|
|
222
|
+
|
|
223
|
+
return errors
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def item_passes_all_constraints(item: Item) -> bool:
|
|
227
|
+
"""Check if item satisfies all constraints.
|
|
228
|
+
|
|
229
|
+
Convenience function to check if all constraints are satisfied.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
item : Item
|
|
234
|
+
Item to check.
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
bool
|
|
239
|
+
True if all constraints satisfied, False otherwise.
|
|
240
|
+
|
|
241
|
+
Examples
|
|
242
|
+
--------
|
|
243
|
+
>>> if item_passes_all_constraints(item):
|
|
244
|
+
... print("Item is valid")
|
|
245
|
+
"""
|
|
246
|
+
return all(item.constraint_satisfaction.values())
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _check_options(item: Item) -> tuple[bool, int]:
|
|
250
|
+
"""Check if item has valid options list.
|
|
251
|
+
|
|
252
|
+
Helper function for detecting forced_choice and multi_select task types.
|
|
253
|
+
Checks the item.options field for a valid list of options.
|
|
254
|
+
|
|
255
|
+
Parameters
|
|
256
|
+
----------
|
|
257
|
+
item : Item
|
|
258
|
+
Item to check for options.
|
|
259
|
+
|
|
260
|
+
Returns
|
|
261
|
+
-------
|
|
262
|
+
tuple[bool, int]
|
|
263
|
+
Tuple of (has_options, n_options) where has_options is True if
|
|
264
|
+
the item has at least 2 options, and n_options is the count.
|
|
265
|
+
|
|
266
|
+
Examples
|
|
267
|
+
--------
|
|
268
|
+
>>> item = Item(item_template_id=uuid4(), options=["A", "B"])
|
|
269
|
+
>>> _check_options(item)
|
|
270
|
+
(True, 2)
|
|
271
|
+
>>> item = Item(item_template_id=uuid4(), options=[])
|
|
272
|
+
>>> _check_options(item)
|
|
273
|
+
(False, 0)
|
|
274
|
+
>>> item = Item(item_template_id=uuid4(), options=["A"])
|
|
275
|
+
>>> _check_options(item)
|
|
276
|
+
(False, 0) # Need at least 2 options
|
|
277
|
+
"""
|
|
278
|
+
if not item.options:
|
|
279
|
+
return (False, 0)
|
|
280
|
+
|
|
281
|
+
n_options = len(item.options)
|
|
282
|
+
|
|
283
|
+
# Must have at least 2 options to be valid
|
|
284
|
+
if n_options < 2:
|
|
285
|
+
return (False, 0)
|
|
286
|
+
|
|
287
|
+
return (True, n_options)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _check_option_keys( # pyright: ignore[reportUnusedFunction]
|
|
291
|
+
rendered_elements: dict[str, str],
|
|
292
|
+
) -> tuple[bool, int]:
|
|
293
|
+
"""Check if rendered_elements has consecutive option_a, option_b, ... keys.
|
|
294
|
+
|
|
295
|
+
.. deprecated::
|
|
296
|
+
This function is deprecated. Use _check_options() instead, which
|
|
297
|
+
checks the item.options list field.
|
|
298
|
+
|
|
299
|
+
Helper function for detecting forced_choice and multi_select task types
|
|
300
|
+
in legacy items that store options in rendered_elements.
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
rendered_elements : dict
|
|
305
|
+
Dictionary of rendered elements to check.
|
|
306
|
+
|
|
307
|
+
Returns
|
|
308
|
+
-------
|
|
309
|
+
tuple[bool, int]
|
|
310
|
+
Tuple of (has_options, n_options) where has_options is True if
|
|
311
|
+
consecutive option keys found, and n_options is the count.
|
|
312
|
+
|
|
313
|
+
Examples
|
|
314
|
+
--------
|
|
315
|
+
>>> _check_option_keys({"option_a": "A", "option_b": "B"})
|
|
316
|
+
(True, 2)
|
|
317
|
+
>>> _check_option_keys({"text": "Hello"})
|
|
318
|
+
(False, 0)
|
|
319
|
+
>>> _check_option_keys({"option_a": "A", "option_c": "C"})
|
|
320
|
+
(False, 0) # Not consecutive
|
|
321
|
+
"""
|
|
322
|
+
# Check for option_a, option_b, option_c, ...
|
|
323
|
+
if "option_a" not in rendered_elements:
|
|
324
|
+
return (False, 0)
|
|
325
|
+
|
|
326
|
+
# Count consecutive options starting from option_a
|
|
327
|
+
n_options = 0
|
|
328
|
+
expected_letters = "abcdefghijklmnopqrstuvwxyz"
|
|
329
|
+
|
|
330
|
+
for letter in expected_letters:
|
|
331
|
+
key = f"option_{letter}"
|
|
332
|
+
if key in rendered_elements:
|
|
333
|
+
n_options += 1
|
|
334
|
+
else:
|
|
335
|
+
break
|
|
336
|
+
|
|
337
|
+
# Must have at least 2 options to be valid
|
|
338
|
+
return (n_options >= 2, n_options)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def get_task_type_requirements(task_type: TaskType) -> dict[str, list[str] | str]:
|
|
342
|
+
"""Get validation requirements for a task type.
|
|
343
|
+
|
|
344
|
+
Returns a dictionary describing the structural requirements
|
|
345
|
+
for items of the specified task type. Useful for introspection,
|
|
346
|
+
error messages, and documentation generation.
|
|
347
|
+
|
|
348
|
+
Parameters
|
|
349
|
+
----------
|
|
350
|
+
task_type : TaskType
|
|
351
|
+
Task type to get requirements for.
|
|
352
|
+
|
|
353
|
+
Returns
|
|
354
|
+
-------
|
|
355
|
+
dict
|
|
356
|
+
Requirements specification with keys:
|
|
357
|
+
- required_rendered_keys: List of required rendered_elements keys
|
|
358
|
+
- required_metadata_keys: List of required item_metadata keys
|
|
359
|
+
- optional_metadata_keys: List of optional item_metadata keys
|
|
360
|
+
- special_fields: List of special fields (e.g., ["unfilled_slots"])
|
|
361
|
+
- description: Human-readable description
|
|
362
|
+
|
|
363
|
+
Examples
|
|
364
|
+
--------
|
|
365
|
+
>>> reqs = get_task_type_requirements("ordinal_scale")
|
|
366
|
+
>>> print(reqs["required_rendered_keys"])
|
|
367
|
+
['text']
|
|
368
|
+
>>> print(reqs["required_metadata_keys"])
|
|
369
|
+
['scale_min', 'scale_max']
|
|
370
|
+
"""
|
|
371
|
+
requirements = {
|
|
372
|
+
"forced_choice": {
|
|
373
|
+
"required_rendered_keys": [],
|
|
374
|
+
"required_metadata_keys": [],
|
|
375
|
+
"optional_metadata_keys": [
|
|
376
|
+
"source_items",
|
|
377
|
+
"group_key",
|
|
378
|
+
"pair_type",
|
|
379
|
+
"n_options",
|
|
380
|
+
],
|
|
381
|
+
"special_fields": ["options"],
|
|
382
|
+
"description": (
|
|
383
|
+
"Pick exactly one option from N alternatives (2AFC, 3AFC, ...)"
|
|
384
|
+
),
|
|
385
|
+
},
|
|
386
|
+
"multi_select": {
|
|
387
|
+
"required_rendered_keys": [],
|
|
388
|
+
"required_metadata_keys": ["min_selections", "max_selections"],
|
|
389
|
+
"optional_metadata_keys": ["source_items", "group_key"],
|
|
390
|
+
"special_fields": ["options"],
|
|
391
|
+
"description": "Pick one or more options (checkboxes)",
|
|
392
|
+
},
|
|
393
|
+
"ordinal_scale": {
|
|
394
|
+
"required_rendered_keys": ["text", "prompt"],
|
|
395
|
+
"required_metadata_keys": ["scale_min", "scale_max"],
|
|
396
|
+
"optional_metadata_keys": ["source_items", "group_key", "scale_labels"],
|
|
397
|
+
"special_fields": [],
|
|
398
|
+
"description": "Value on ordered discrete scale (Likert, slider)",
|
|
399
|
+
},
|
|
400
|
+
"magnitude": {
|
|
401
|
+
"required_rendered_keys": ["text", "prompt"],
|
|
402
|
+
"required_metadata_keys": ["min_value", "max_value"],
|
|
403
|
+
"optional_metadata_keys": [
|
|
404
|
+
"unit",
|
|
405
|
+
"step",
|
|
406
|
+
"source_items",
|
|
407
|
+
"group_key",
|
|
408
|
+
],
|
|
409
|
+
"special_fields": [],
|
|
410
|
+
"description": "Unbounded numeric value (reading time, confidence)",
|
|
411
|
+
},
|
|
412
|
+
"binary": {
|
|
413
|
+
"required_rendered_keys": ["text", "prompt"],
|
|
414
|
+
"required_metadata_keys": [],
|
|
415
|
+
"optional_metadata_keys": ["binary_options", "source_items", "group_key"],
|
|
416
|
+
"special_fields": [],
|
|
417
|
+
"description": "Yes/No, True/False (absolute judgment)",
|
|
418
|
+
},
|
|
419
|
+
"categorical": {
|
|
420
|
+
"required_rendered_keys": ["text", "prompt"],
|
|
421
|
+
"required_metadata_keys": ["categories"],
|
|
422
|
+
"optional_metadata_keys": ["source_items", "group_key"],
|
|
423
|
+
"special_fields": [],
|
|
424
|
+
"description": "Pick from unordered categories (NLI, semantic relations)",
|
|
425
|
+
},
|
|
426
|
+
"free_text": {
|
|
427
|
+
"required_rendered_keys": ["text", "prompt"],
|
|
428
|
+
"required_metadata_keys": [],
|
|
429
|
+
"optional_metadata_keys": [
|
|
430
|
+
"max_length",
|
|
431
|
+
"validation_pattern",
|
|
432
|
+
"multiline",
|
|
433
|
+
"source_items",
|
|
434
|
+
"group_key",
|
|
435
|
+
],
|
|
436
|
+
"special_fields": [],
|
|
437
|
+
"description": "Open-ended text (paraphrase, comprehension)",
|
|
438
|
+
},
|
|
439
|
+
"cloze": {
|
|
440
|
+
"required_rendered_keys": ["text"],
|
|
441
|
+
"required_metadata_keys": ["n_unfilled_slots"],
|
|
442
|
+
"optional_metadata_keys": ["source_items", "group_key", "template_id"],
|
|
443
|
+
"special_fields": ["unfilled_slots"],
|
|
444
|
+
"description": "Fill-in-the-blank (constraint-based UI)",
|
|
445
|
+
},
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if task_type not in requirements:
|
|
449
|
+
raise ValueError(
|
|
450
|
+
f"Unknown task type: {task_type}. "
|
|
451
|
+
f"Expected one of: {list(requirements.keys())}"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
return requirements[task_type]
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def validate_item_for_task_type(item: Item, task_type: TaskType) -> bool:
|
|
458
|
+
"""Validate that an Item's structure matches requirements for a task type.
|
|
459
|
+
|
|
460
|
+
Checks that the item has the required rendered_elements keys,
|
|
461
|
+
item_metadata keys, and special fields for the specified task type.
|
|
462
|
+
Raises descriptive ValueError if validation fails.
|
|
463
|
+
|
|
464
|
+
Parameters
|
|
465
|
+
----------
|
|
466
|
+
item : Item
|
|
467
|
+
Item to validate.
|
|
468
|
+
task_type : TaskType
|
|
469
|
+
Expected task type (from bead.items.item_template.TaskType).
|
|
470
|
+
|
|
471
|
+
Returns
|
|
472
|
+
-------
|
|
473
|
+
bool
|
|
474
|
+
True if valid.
|
|
475
|
+
|
|
476
|
+
Raises
|
|
477
|
+
------
|
|
478
|
+
ValueError
|
|
479
|
+
If item structure doesn't match task type requirements,
|
|
480
|
+
with detailed explanation of what's wrong.
|
|
481
|
+
|
|
482
|
+
Examples
|
|
483
|
+
--------
|
|
484
|
+
>>> from bead.items.ordinal_scale import create_ordinal_scale_item
|
|
485
|
+
>>> item = create_ordinal_scale_item("How natural?", scale_bounds=(1, 7))
|
|
486
|
+
>>> validate_item_for_task_type(item, "ordinal_scale")
|
|
487
|
+
True
|
|
488
|
+
|
|
489
|
+
>>> from bead.items.forced_choice import create_forced_choice_item
|
|
490
|
+
>>> fc_item = create_forced_choice_item("A", "B")
|
|
491
|
+
>>> validate_item_for_task_type(fc_item, "ordinal_scale")
|
|
492
|
+
ValueError: ordinal_scale items must have 'text' in rendered_elements...
|
|
493
|
+
"""
|
|
494
|
+
reqs = get_task_type_requirements(task_type)
|
|
495
|
+
|
|
496
|
+
# Check rendered_elements keys
|
|
497
|
+
actual_rendered = set(item.rendered_elements.keys())
|
|
498
|
+
required_rendered = set(reqs["required_rendered_keys"])
|
|
499
|
+
|
|
500
|
+
# Special handling for forced_choice and multi_select (options field)
|
|
501
|
+
if task_type in ("forced_choice", "multi_select"):
|
|
502
|
+
has_options, n_options = _check_options(item)
|
|
503
|
+
if not has_options:
|
|
504
|
+
raise ValueError(
|
|
505
|
+
f"{task_type} items must have at least 2 options in the options field, "
|
|
506
|
+
f"but found {n_options} option(s): {item.options}"
|
|
507
|
+
)
|
|
508
|
+
# For these types, we don't check for exact required rendered_elements keys
|
|
509
|
+
else:
|
|
510
|
+
# Check for exact required keys
|
|
511
|
+
missing_rendered = required_rendered - actual_rendered
|
|
512
|
+
if missing_rendered:
|
|
513
|
+
raise ValueError(
|
|
514
|
+
f"{task_type} items must have {list(required_rendered)} "
|
|
515
|
+
f"in rendered_elements, but missing: {list(missing_rendered)}. "
|
|
516
|
+
f"Found keys: {list(actual_rendered)}"
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Check item_metadata keys
|
|
520
|
+
actual_metadata = set(item.item_metadata.keys())
|
|
521
|
+
required_metadata = set(reqs["required_metadata_keys"])
|
|
522
|
+
|
|
523
|
+
missing_metadata = required_metadata - actual_metadata
|
|
524
|
+
if missing_metadata:
|
|
525
|
+
raise ValueError(
|
|
526
|
+
f"{task_type} items must have {list(required_metadata)} "
|
|
527
|
+
f"in item_metadata, but missing: {list(missing_metadata)}. "
|
|
528
|
+
f"Found keys: {list(actual_metadata)}"
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
# Check special fields
|
|
532
|
+
if "unfilled_slots" in reqs["special_fields"]:
|
|
533
|
+
if not item.unfilled_slots:
|
|
534
|
+
raise ValueError(
|
|
535
|
+
f"{task_type} items must have unfilled_slots field populated, "
|
|
536
|
+
f"but found empty list"
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
if "options" in reqs["special_fields"]:
|
|
540
|
+
if not item.options or len(item.options) < 2:
|
|
541
|
+
raise ValueError(
|
|
542
|
+
f"{task_type} items must have at least 2 options in the options field, "
|
|
543
|
+
f"but found {len(item.options) if item.options else 0} option(s)"
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# Task-specific validation
|
|
547
|
+
if task_type == "ordinal_scale":
|
|
548
|
+
scale_min = item.item_metadata.get("scale_min")
|
|
549
|
+
scale_max = item.item_metadata.get("scale_max")
|
|
550
|
+
if not isinstance(scale_min, int) or not isinstance(scale_max, int):
|
|
551
|
+
raise ValueError(
|
|
552
|
+
f"ordinal_scale items must have integer scale_min and scale_max, "
|
|
553
|
+
f"but got scale_min={type(scale_min).__name__}, "
|
|
554
|
+
f"scale_max={type(scale_max).__name__}"
|
|
555
|
+
)
|
|
556
|
+
if scale_min >= scale_max:
|
|
557
|
+
raise ValueError(
|
|
558
|
+
f"ordinal_scale items must have scale_min < scale_max, "
|
|
559
|
+
f"but got scale_min={scale_min}, scale_max={scale_max}"
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
if task_type == "multi_select":
|
|
563
|
+
min_sel = item.item_metadata.get("min_selections")
|
|
564
|
+
max_sel = item.item_metadata.get("max_selections")
|
|
565
|
+
if not isinstance(min_sel, int) or not isinstance(max_sel, int):
|
|
566
|
+
raise ValueError(
|
|
567
|
+
"multi_select items must have integer min_selections "
|
|
568
|
+
f"and max_selections, but got min_selections="
|
|
569
|
+
f"{type(min_sel).__name__}, max_selections="
|
|
570
|
+
f"{type(max_sel).__name__}"
|
|
571
|
+
)
|
|
572
|
+
if min_sel <= 0 or max_sel <= 0:
|
|
573
|
+
raise ValueError(
|
|
574
|
+
"multi_select items must have positive min_selections "
|
|
575
|
+
f"and max_selections, but got min_selections={min_sel}, "
|
|
576
|
+
f"max_selections={max_sel}"
|
|
577
|
+
)
|
|
578
|
+
if min_sel > max_sel:
|
|
579
|
+
raise ValueError(
|
|
580
|
+
f"multi_select items must have min_selections <= max_selections, "
|
|
581
|
+
f"but got min_selections={min_sel}, max_selections={max_sel}"
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
if task_type == "magnitude":
|
|
585
|
+
min_val = item.item_metadata.get("min_value")
|
|
586
|
+
max_val = item.item_metadata.get("max_value")
|
|
587
|
+
if min_val is not None and max_val is not None:
|
|
588
|
+
if not isinstance(min_val, int | float) or not isinstance(
|
|
589
|
+
max_val, int | float
|
|
590
|
+
):
|
|
591
|
+
raise ValueError(
|
|
592
|
+
"magnitude items with bounds must have numeric "
|
|
593
|
+
f"min_value and max_value, but got min_value="
|
|
594
|
+
f"{type(min_val).__name__}, max_value="
|
|
595
|
+
f"{type(max_val).__name__}"
|
|
596
|
+
)
|
|
597
|
+
if min_val >= max_val:
|
|
598
|
+
raise ValueError(
|
|
599
|
+
f"magnitude items must have min_value < max_value, "
|
|
600
|
+
f"but got min_value={min_val}, max_value={max_val}"
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
if task_type in ("binary", "categorical", "free_text"):
|
|
604
|
+
prompt = item.rendered_elements.get("prompt")
|
|
605
|
+
if not prompt or not str(prompt).strip():
|
|
606
|
+
raise ValueError(
|
|
607
|
+
f"{task_type} items must have non-empty 'prompt' in rendered_elements"
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
if task_type == "categorical":
|
|
611
|
+
categories = item.item_metadata.get("categories")
|
|
612
|
+
if not isinstance(categories, list) or len(categories) == 0:
|
|
613
|
+
raise ValueError(
|
|
614
|
+
"categorical items must have non-empty list in "
|
|
615
|
+
f"item_metadata['categories'], but got "
|
|
616
|
+
f"{type(categories).__name__}"
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# No additional validation needed for forced_choice
|
|
620
|
+
# (n_options is optional metadata, not required)
|
|
621
|
+
|
|
622
|
+
return True
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def infer_task_type_from_item(item: Item) -> TaskType:
|
|
626
|
+
"""Infer most likely task type from Item structure.
|
|
627
|
+
|
|
628
|
+
Examines the item's rendered_elements, item_metadata, and special fields
|
|
629
|
+
to determine which task type it matches. Uses priority order to handle
|
|
630
|
+
ambiguous cases.
|
|
631
|
+
|
|
632
|
+
Parameters
|
|
633
|
+
----------
|
|
634
|
+
item : Item
|
|
635
|
+
Item to infer from.
|
|
636
|
+
|
|
637
|
+
Returns
|
|
638
|
+
-------
|
|
639
|
+
TaskType
|
|
640
|
+
Inferred task type.
|
|
641
|
+
|
|
642
|
+
Raises
|
|
643
|
+
------
|
|
644
|
+
ValueError
|
|
645
|
+
If item structure doesn't match any task type or is ambiguous.
|
|
646
|
+
|
|
647
|
+
Examples
|
|
648
|
+
--------
|
|
649
|
+
>>> from bead.items.ordinal_scale import create_likert_7_item
|
|
650
|
+
>>> item = create_likert_7_item("How natural is this sentence?")
|
|
651
|
+
>>> infer_task_type_from_item(item)
|
|
652
|
+
'ordinal_scale'
|
|
653
|
+
|
|
654
|
+
>>> from bead.items.categorical import create_nli_item
|
|
655
|
+
>>> item2 = create_nli_item("All dogs bark", "Some dogs bark")
|
|
656
|
+
>>> infer_task_type_from_item(item2)
|
|
657
|
+
'categorical'
|
|
658
|
+
"""
|
|
659
|
+
rendered = item.rendered_elements
|
|
660
|
+
metadata = item.item_metadata
|
|
661
|
+
|
|
662
|
+
# Priority 1: Check for cloze (unique unfilled_slots field)
|
|
663
|
+
if item.unfilled_slots:
|
|
664
|
+
if "n_unfilled_slots" in metadata:
|
|
665
|
+
return "cloze"
|
|
666
|
+
|
|
667
|
+
# Priority 2: Check for forced_choice/multi_select (options list field)
|
|
668
|
+
has_options, _ = _check_options(item)
|
|
669
|
+
if has_options:
|
|
670
|
+
# Distinguish between forced_choice and multi_select
|
|
671
|
+
if "min_selections" in metadata and "max_selections" in metadata:
|
|
672
|
+
return "multi_select"
|
|
673
|
+
if "n_options" in metadata:
|
|
674
|
+
return "forced_choice"
|
|
675
|
+
# Default to forced_choice if has options but no specific metadata
|
|
676
|
+
return "forced_choice"
|
|
677
|
+
|
|
678
|
+
# Priority 3: Check for single "text" key (cloze without unfilled_slots)
|
|
679
|
+
if "text" in rendered and len(rendered) == 1:
|
|
680
|
+
# Must be cloze if only "text" key exists
|
|
681
|
+
# (but we already checked unfilled_slots above)
|
|
682
|
+
# Ambiguous: could be improperly constructed item
|
|
683
|
+
raise ValueError(
|
|
684
|
+
"Item has single 'text' key without unfilled_slots. "
|
|
685
|
+
"If this is a cloze item, ensure unfilled_slots is populated. "
|
|
686
|
+
"Other task types require additional keys."
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
# Priority 4: Check for text + prompt
|
|
690
|
+
# (ordinal_scale, magnitude, binary, categorical, free_text)
|
|
691
|
+
if "text" in rendered and "prompt" in rendered:
|
|
692
|
+
# Ordinal scale has scale_min/scale_max
|
|
693
|
+
if "scale_min" in metadata and "scale_max" in metadata:
|
|
694
|
+
return "ordinal_scale"
|
|
695
|
+
# Magnitude has min_value/max_value (always set, may be None)
|
|
696
|
+
if "min_value" in metadata and "max_value" in metadata:
|
|
697
|
+
return "magnitude"
|
|
698
|
+
# Categorical has categories
|
|
699
|
+
if "categories" in metadata:
|
|
700
|
+
return "categorical"
|
|
701
|
+
# Binary may have binary_options
|
|
702
|
+
if "binary_options" in metadata:
|
|
703
|
+
return "binary"
|
|
704
|
+
# Free text may have max_length, validation_pattern, or multiline
|
|
705
|
+
if (
|
|
706
|
+
"max_length" in metadata
|
|
707
|
+
or "validation_pattern" in metadata
|
|
708
|
+
or "multiline" in metadata
|
|
709
|
+
):
|
|
710
|
+
return "free_text"
|
|
711
|
+
# Could be binary or free_text (most ambiguous case)
|
|
712
|
+
raise ValueError(
|
|
713
|
+
"Could be binary or free_text based on structure. "
|
|
714
|
+
"Item has 'text' and 'prompt' but no distinguishing metadata. "
|
|
715
|
+
"Use explicit task type validation."
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
# No match
|
|
719
|
+
raise ValueError(
|
|
720
|
+
f"Could not infer task type from item structure. "
|
|
721
|
+
f"rendered_elements keys: {list(rendered.keys())}, "
|
|
722
|
+
f"item_metadata keys: {list(metadata.keys())}"
|
|
723
|
+
)
|