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/magnitude.py
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
"""Utilities for creating magnitude experimental items.
|
|
2
|
+
|
|
3
|
+
This module provides language-agnostic utilities for creating magnitude
|
|
4
|
+
items where participants enter numeric values (bounded or unbounded),
|
|
5
|
+
such as reading times, confidence ratings, or counts.
|
|
6
|
+
|
|
7
|
+
Integration Points
|
|
8
|
+
------------------
|
|
9
|
+
- Active Learning: bead/active_learning/models/magnitude.py
|
|
10
|
+
- Simulation: bead/simulation/strategies/magnitude.py
|
|
11
|
+
- Deployment: bead/deployment/jspsych/ (numeric input)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from itertools import product
|
|
19
|
+
from typing import Any
|
|
20
|
+
from uuid import UUID, uuid4
|
|
21
|
+
|
|
22
|
+
from bead.items.item import Item, MetadataValue
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_magnitude_item(
|
|
26
|
+
text: str,
|
|
27
|
+
unit: str | None = None,
|
|
28
|
+
bounds: tuple[int | float | None, int | float | None] = (None, None),
|
|
29
|
+
prompt: str | None = None,
|
|
30
|
+
step: int | float | None = None,
|
|
31
|
+
item_template_id: UUID | None = None,
|
|
32
|
+
metadata: dict[str, MetadataValue] | None = None,
|
|
33
|
+
) -> Item:
|
|
34
|
+
"""Create a magnitude (numeric input) item.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
text : str
|
|
39
|
+
The stimulus text or question.
|
|
40
|
+
unit : str | None
|
|
41
|
+
Optional unit for the value (e.g., "ms", "%", "count").
|
|
42
|
+
bounds : tuple[int | float | None, int | float | None]
|
|
43
|
+
Tuple of (min, max) bounds. None means unbounded in that direction.
|
|
44
|
+
Default: (None, None) for fully unbounded.
|
|
45
|
+
prompt : str | None
|
|
46
|
+
Optional prompt for the numeric input.
|
|
47
|
+
If None, uses "Enter a value:".
|
|
48
|
+
step : int | float | None
|
|
49
|
+
Optional step size for input validation (e.g., 1 for integers, 0.01 for
|
|
50
|
+
hundredths).
|
|
51
|
+
item_template_id : UUID | None
|
|
52
|
+
Template ID for the item. If None, generates new UUID.
|
|
53
|
+
metadata : dict[str, MetadataValue] | None
|
|
54
|
+
Additional metadata for item_metadata field.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
Item
|
|
59
|
+
Magnitude item with text and prompt in rendered_elements.
|
|
60
|
+
|
|
61
|
+
Raises
|
|
62
|
+
------
|
|
63
|
+
ValueError
|
|
64
|
+
If text is empty or if both bounds are provided and min >= max.
|
|
65
|
+
|
|
66
|
+
Examples
|
|
67
|
+
--------
|
|
68
|
+
>>> item = create_magnitude_item(
|
|
69
|
+
... text="How long did it take to read this sentence?",
|
|
70
|
+
... unit="ms",
|
|
71
|
+
... bounds=(0, None),
|
|
72
|
+
... prompt="Enter time in milliseconds:"
|
|
73
|
+
... )
|
|
74
|
+
>>> item.rendered_elements["text"]
|
|
75
|
+
'How long did it take to read this sentence?'
|
|
76
|
+
>>> item.item_metadata["unit"]
|
|
77
|
+
'ms'
|
|
78
|
+
>>> item.item_metadata["min_value"]
|
|
79
|
+
0
|
|
80
|
+
>>> item.item_metadata["max_value"] is None
|
|
81
|
+
True
|
|
82
|
+
|
|
83
|
+
>>> # Confidence with bounded range
|
|
84
|
+
>>> item = create_magnitude_item(
|
|
85
|
+
... text="How confident are you in your answer?",
|
|
86
|
+
... unit="%",
|
|
87
|
+
... bounds=(0, 100),
|
|
88
|
+
... step=1
|
|
89
|
+
... )
|
|
90
|
+
>>> item.item_metadata["max_value"]
|
|
91
|
+
100
|
|
92
|
+
"""
|
|
93
|
+
if not text or not text.strip():
|
|
94
|
+
raise ValueError("text cannot be empty")
|
|
95
|
+
|
|
96
|
+
min_value, max_value = bounds
|
|
97
|
+
|
|
98
|
+
# Validate bounds if both are provided
|
|
99
|
+
if min_value is not None and max_value is not None:
|
|
100
|
+
if min_value >= max_value:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"min_value ({min_value}) must be less than max_value ({max_value})"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if item_template_id is None:
|
|
106
|
+
item_template_id = uuid4()
|
|
107
|
+
|
|
108
|
+
if prompt is None:
|
|
109
|
+
prompt = "Enter a value:"
|
|
110
|
+
|
|
111
|
+
rendered_elements: dict[str, str] = {
|
|
112
|
+
"text": text,
|
|
113
|
+
"prompt": prompt,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if unit:
|
|
117
|
+
rendered_elements["unit"] = unit
|
|
118
|
+
|
|
119
|
+
# Build item metadata
|
|
120
|
+
item_metadata: dict[str, MetadataValue] = {
|
|
121
|
+
"min_value": min_value,
|
|
122
|
+
"max_value": max_value,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if unit:
|
|
126
|
+
item_metadata["unit"] = unit
|
|
127
|
+
|
|
128
|
+
if step is not None:
|
|
129
|
+
item_metadata["step"] = step
|
|
130
|
+
|
|
131
|
+
if metadata:
|
|
132
|
+
item_metadata.update(metadata)
|
|
133
|
+
|
|
134
|
+
return Item(
|
|
135
|
+
item_template_id=item_template_id,
|
|
136
|
+
rendered_elements=rendered_elements,
|
|
137
|
+
item_metadata=item_metadata,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def create_magnitude_items_from_texts(
|
|
142
|
+
texts: list[str],
|
|
143
|
+
unit: str | None = None,
|
|
144
|
+
bounds: tuple[int | float | None, int | float | None] = (None, None),
|
|
145
|
+
prompt: str | None = None,
|
|
146
|
+
step: int | float | None = None,
|
|
147
|
+
*,
|
|
148
|
+
item_template_id: UUID | None = None,
|
|
149
|
+
metadata_fn: Callable[[str], dict[str, MetadataValue]] | None = None,
|
|
150
|
+
) -> list[Item]:
|
|
151
|
+
"""Create magnitude items from a list of texts.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
texts : list[str]
|
|
156
|
+
List of stimulus texts.
|
|
157
|
+
unit : str | None
|
|
158
|
+
Optional unit for all items.
|
|
159
|
+
bounds : tuple[int | float | None, int | float | None]
|
|
160
|
+
Bounds (min, max) for all items.
|
|
161
|
+
prompt : str | None
|
|
162
|
+
The question/prompt for all items.
|
|
163
|
+
step : int | float | None
|
|
164
|
+
Step size for all items.
|
|
165
|
+
item_template_id : UUID | None
|
|
166
|
+
Template ID for all created items. If None, generates one per item.
|
|
167
|
+
metadata_fn : Callable[[str], dict[str, MetadataValue]] | None
|
|
168
|
+
Function to generate metadata from each text.
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
list[Item]
|
|
173
|
+
Magnitude items for each text.
|
|
174
|
+
|
|
175
|
+
Examples
|
|
176
|
+
--------
|
|
177
|
+
>>> texts = ["Sentence 1", "Sentence 2", "Sentence 3"]
|
|
178
|
+
>>> items = create_magnitude_items_from_texts(
|
|
179
|
+
... texts,
|
|
180
|
+
... unit="ms",
|
|
181
|
+
... bounds=(0, None),
|
|
182
|
+
... prompt="Reading time?",
|
|
183
|
+
... metadata_fn=lambda t: {"text_length": len(t)}
|
|
184
|
+
... )
|
|
185
|
+
>>> len(items)
|
|
186
|
+
3
|
|
187
|
+
>>> items[0].item_metadata["unit"]
|
|
188
|
+
'ms'
|
|
189
|
+
"""
|
|
190
|
+
magnitude_items: list[Item] = []
|
|
191
|
+
|
|
192
|
+
for text in texts:
|
|
193
|
+
item_metadata: dict[str, MetadataValue] = {}
|
|
194
|
+
if metadata_fn:
|
|
195
|
+
item_metadata = metadata_fn(text)
|
|
196
|
+
|
|
197
|
+
item = create_magnitude_item(
|
|
198
|
+
text=text,
|
|
199
|
+
unit=unit,
|
|
200
|
+
bounds=bounds,
|
|
201
|
+
prompt=prompt,
|
|
202
|
+
step=step,
|
|
203
|
+
item_template_id=item_template_id,
|
|
204
|
+
metadata=item_metadata,
|
|
205
|
+
)
|
|
206
|
+
magnitude_items.append(item)
|
|
207
|
+
|
|
208
|
+
return magnitude_items
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def create_magnitude_items_from_groups(
|
|
212
|
+
items: list[Item],
|
|
213
|
+
group_by: Callable[[Item], Any],
|
|
214
|
+
unit: str | None = None,
|
|
215
|
+
bounds: tuple[int | float | None, int | float | None] = (None, None),
|
|
216
|
+
prompt: str | None = None,
|
|
217
|
+
step: int | float | None = None,
|
|
218
|
+
*,
|
|
219
|
+
extract_text: Callable[[Item], str] | None = None,
|
|
220
|
+
include_group_metadata: bool = True,
|
|
221
|
+
item_template_id: UUID | None = None,
|
|
222
|
+
) -> list[Item]:
|
|
223
|
+
"""Create magnitude items from grouped source items.
|
|
224
|
+
|
|
225
|
+
Groups items and creates one magnitude item per source item,
|
|
226
|
+
preserving group information in metadata.
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
items : list[Item]
|
|
231
|
+
Source items to process.
|
|
232
|
+
group_by : Callable[[Item], Any]
|
|
233
|
+
Function to extract grouping key from items.
|
|
234
|
+
unit : str | None
|
|
235
|
+
Optional unit for all items.
|
|
236
|
+
bounds : tuple[int | float | None, int | float | None]
|
|
237
|
+
Bounds (min, max) for all items.
|
|
238
|
+
prompt : str | None
|
|
239
|
+
The question/prompt for all items.
|
|
240
|
+
step : int | float | None
|
|
241
|
+
Step size for all items.
|
|
242
|
+
extract_text : Callable[[Item], str] | None
|
|
243
|
+
Function to extract text from item. If None, tries common keys.
|
|
244
|
+
include_group_metadata : bool
|
|
245
|
+
Whether to include group key in item metadata.
|
|
246
|
+
item_template_id : UUID | None
|
|
247
|
+
Template ID for all created items. If None, generates one per item.
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
list[Item]
|
|
252
|
+
Magnitude items from source items.
|
|
253
|
+
|
|
254
|
+
Examples
|
|
255
|
+
--------
|
|
256
|
+
>>> source_items = [
|
|
257
|
+
... Item(
|
|
258
|
+
... uuid4(),
|
|
259
|
+
... rendered_elements={"text": "The cat sat."},
|
|
260
|
+
... item_metadata={"category": "simple"}
|
|
261
|
+
... )
|
|
262
|
+
... ]
|
|
263
|
+
>>> magnitude_items = create_magnitude_items_from_groups(
|
|
264
|
+
... source_items,
|
|
265
|
+
... group_by=lambda i: i.item_metadata["category"],
|
|
266
|
+
... unit="ms",
|
|
267
|
+
... bounds=(0, None),
|
|
268
|
+
... prompt="Reading time:"
|
|
269
|
+
... )
|
|
270
|
+
>>> len(magnitude_items)
|
|
271
|
+
1
|
|
272
|
+
"""
|
|
273
|
+
# Group items
|
|
274
|
+
groups: dict[Any, list[Item]] = defaultdict(list)
|
|
275
|
+
for item in items:
|
|
276
|
+
group_key = group_by(item)
|
|
277
|
+
groups[group_key].append(item)
|
|
278
|
+
|
|
279
|
+
magnitude_items: list[Item] = []
|
|
280
|
+
|
|
281
|
+
for group_key, group_items in groups.items():
|
|
282
|
+
for item in group_items:
|
|
283
|
+
# Extract text
|
|
284
|
+
if extract_text:
|
|
285
|
+
text: str = extract_text(item)
|
|
286
|
+
else:
|
|
287
|
+
text = _extract_text_from_item(item)
|
|
288
|
+
|
|
289
|
+
# Build metadata
|
|
290
|
+
item_metadata: dict[str, MetadataValue] = {
|
|
291
|
+
"source_item_id": str(item.id),
|
|
292
|
+
}
|
|
293
|
+
if include_group_metadata:
|
|
294
|
+
item_metadata["group_key"] = str(group_key)
|
|
295
|
+
|
|
296
|
+
# Create magnitude item
|
|
297
|
+
magnitude_item = create_magnitude_item(
|
|
298
|
+
text=text,
|
|
299
|
+
unit=unit,
|
|
300
|
+
bounds=bounds,
|
|
301
|
+
prompt=prompt,
|
|
302
|
+
step=step,
|
|
303
|
+
item_template_id=item_template_id,
|
|
304
|
+
metadata=item_metadata,
|
|
305
|
+
)
|
|
306
|
+
magnitude_items.append(magnitude_item)
|
|
307
|
+
|
|
308
|
+
return magnitude_items
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def create_magnitude_items_cross_product(
|
|
312
|
+
texts: list[str],
|
|
313
|
+
prompts: list[str],
|
|
314
|
+
unit: str | None = None,
|
|
315
|
+
bounds: tuple[int | float | None, int | float | None] = (None, None),
|
|
316
|
+
step: int | float | None = None,
|
|
317
|
+
*,
|
|
318
|
+
item_template_id: UUID | None = None,
|
|
319
|
+
metadata_fn: (Callable[[str, str], dict[str, MetadataValue]] | None) = None,
|
|
320
|
+
) -> list[Item]:
|
|
321
|
+
"""Create magnitude items from cross-product of texts and prompts.
|
|
322
|
+
|
|
323
|
+
Useful when you want to apply multiple prompts to each text.
|
|
324
|
+
|
|
325
|
+
Parameters
|
|
326
|
+
----------
|
|
327
|
+
texts : list[str]
|
|
328
|
+
List of stimulus texts.
|
|
329
|
+
prompts : list[str]
|
|
330
|
+
List of prompts to apply.
|
|
331
|
+
unit : str | None
|
|
332
|
+
Optional unit for all items.
|
|
333
|
+
bounds : tuple[int | float | None, int | float | None]
|
|
334
|
+
Bounds (min, max) for all items.
|
|
335
|
+
step : int | float | None
|
|
336
|
+
Step size for all items.
|
|
337
|
+
item_template_id : UUID | None
|
|
338
|
+
Template ID for all created items.
|
|
339
|
+
metadata_fn : Callable[[str, str], dict[str, MetadataValue]] | None
|
|
340
|
+
Function to generate metadata from (text, prompt).
|
|
341
|
+
|
|
342
|
+
Returns
|
|
343
|
+
-------
|
|
344
|
+
list[Item]
|
|
345
|
+
Magnitude items from cross-product.
|
|
346
|
+
|
|
347
|
+
Examples
|
|
348
|
+
--------
|
|
349
|
+
>>> texts = ["Sentence 1.", "Sentence 2."]
|
|
350
|
+
>>> prompts = ["Reading time?", "Processing time?"]
|
|
351
|
+
>>> items = create_magnitude_items_cross_product(
|
|
352
|
+
... texts, prompts, unit="ms", bounds=(0, None)
|
|
353
|
+
... )
|
|
354
|
+
>>> len(items)
|
|
355
|
+
4
|
|
356
|
+
"""
|
|
357
|
+
magnitude_items: list[Item] = []
|
|
358
|
+
|
|
359
|
+
for text, prompt in product(texts, prompts):
|
|
360
|
+
item_metadata: dict[str, MetadataValue] = {}
|
|
361
|
+
if metadata_fn:
|
|
362
|
+
item_metadata = metadata_fn(text, prompt)
|
|
363
|
+
|
|
364
|
+
item = create_magnitude_item(
|
|
365
|
+
text=text,
|
|
366
|
+
unit=unit,
|
|
367
|
+
bounds=bounds,
|
|
368
|
+
prompt=prompt,
|
|
369
|
+
step=step,
|
|
370
|
+
item_template_id=item_template_id,
|
|
371
|
+
metadata=item_metadata,
|
|
372
|
+
)
|
|
373
|
+
magnitude_items.append(item)
|
|
374
|
+
|
|
375
|
+
return magnitude_items
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def create_filtered_magnitude_items(
|
|
379
|
+
items: list[Item],
|
|
380
|
+
unit: str | None = None,
|
|
381
|
+
bounds: tuple[int | float | None, int | float | None] = (None, None),
|
|
382
|
+
prompt: str | None = None,
|
|
383
|
+
step: int | float | None = None,
|
|
384
|
+
*,
|
|
385
|
+
item_filter: Callable[[Item], bool] | None = None,
|
|
386
|
+
extract_text: Callable[[Item], str] | None = None,
|
|
387
|
+
item_template_id: UUID | None = None,
|
|
388
|
+
) -> list[Item]:
|
|
389
|
+
"""Create magnitude items with filtering.
|
|
390
|
+
|
|
391
|
+
Parameters
|
|
392
|
+
----------
|
|
393
|
+
items : list[Item]
|
|
394
|
+
Source items.
|
|
395
|
+
unit : str | None
|
|
396
|
+
Optional unit for all items.
|
|
397
|
+
bounds : tuple[int | float | None, int | float | None]
|
|
398
|
+
Bounds (min, max) for all items.
|
|
399
|
+
prompt : str | None
|
|
400
|
+
The question/prompt for all items.
|
|
401
|
+
step : int | float | None
|
|
402
|
+
Step size for all items.
|
|
403
|
+
item_filter : Callable[[Item], bool] | None
|
|
404
|
+
Filter individual items.
|
|
405
|
+
extract_text : Callable[[Item], str] | None
|
|
406
|
+
Text extraction function.
|
|
407
|
+
item_template_id : UUID | None
|
|
408
|
+
Template ID for created items.
|
|
409
|
+
|
|
410
|
+
Returns
|
|
411
|
+
-------
|
|
412
|
+
list[Item]
|
|
413
|
+
Filtered magnitude items.
|
|
414
|
+
|
|
415
|
+
Examples
|
|
416
|
+
--------
|
|
417
|
+
>>> magnitude_items = create_filtered_magnitude_items(
|
|
418
|
+
... items,
|
|
419
|
+
... unit="ms",
|
|
420
|
+
... bounds=(0, None),
|
|
421
|
+
... prompt="Reading time:",
|
|
422
|
+
... item_filter=lambda i: i.item_metadata.get("valid", True)
|
|
423
|
+
... ) # doctest: +SKIP
|
|
424
|
+
"""
|
|
425
|
+
# Filter items
|
|
426
|
+
filtered_items = items
|
|
427
|
+
if item_filter:
|
|
428
|
+
filtered_items = [item for item in items if item_filter(item)]
|
|
429
|
+
|
|
430
|
+
magnitude_items: list[Item] = []
|
|
431
|
+
|
|
432
|
+
for item in filtered_items:
|
|
433
|
+
# Extract text
|
|
434
|
+
if extract_text:
|
|
435
|
+
text: str = extract_text(item)
|
|
436
|
+
else:
|
|
437
|
+
text = _extract_text_from_item(item)
|
|
438
|
+
|
|
439
|
+
# Create magnitude item
|
|
440
|
+
item_metadata: dict[str, MetadataValue] = {
|
|
441
|
+
"source_item_id": str(item.id),
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
magnitude_item = create_magnitude_item(
|
|
445
|
+
text=text,
|
|
446
|
+
unit=unit,
|
|
447
|
+
bounds=bounds,
|
|
448
|
+
prompt=prompt,
|
|
449
|
+
step=step,
|
|
450
|
+
item_template_id=item_template_id,
|
|
451
|
+
metadata=item_metadata,
|
|
452
|
+
)
|
|
453
|
+
magnitude_items.append(magnitude_item)
|
|
454
|
+
|
|
455
|
+
return magnitude_items
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def create_reading_time_item(
|
|
459
|
+
text: str,
|
|
460
|
+
item_template_id: UUID | None = None,
|
|
461
|
+
metadata: dict[str, MetadataValue] | None = None,
|
|
462
|
+
) -> Item:
|
|
463
|
+
"""Create a reading time measurement item.
|
|
464
|
+
|
|
465
|
+
Convenience function for reading time in milliseconds with a lower bound
|
|
466
|
+
of 0 (no upper bound).
|
|
467
|
+
|
|
468
|
+
Parameters
|
|
469
|
+
----------
|
|
470
|
+
text : str
|
|
471
|
+
The text to measure reading time for.
|
|
472
|
+
item_template_id : UUID | None
|
|
473
|
+
Template ID for the item. If None, generates new UUID.
|
|
474
|
+
metadata : dict[str, MetadataValue] | None
|
|
475
|
+
Additional metadata for item_metadata field.
|
|
476
|
+
|
|
477
|
+
Returns
|
|
478
|
+
-------
|
|
479
|
+
Item
|
|
480
|
+
Reading time magnitude item.
|
|
481
|
+
|
|
482
|
+
Examples
|
|
483
|
+
--------
|
|
484
|
+
>>> item = create_reading_time_item("The cat sat on the mat.")
|
|
485
|
+
>>> item.item_metadata["unit"]
|
|
486
|
+
'ms'
|
|
487
|
+
>>> item.item_metadata["min_value"]
|
|
488
|
+
0
|
|
489
|
+
"""
|
|
490
|
+
return create_magnitude_item(
|
|
491
|
+
text,
|
|
492
|
+
unit="ms",
|
|
493
|
+
bounds=(0, None),
|
|
494
|
+
prompt="How long did it take to read?",
|
|
495
|
+
step=1,
|
|
496
|
+
item_template_id=item_template_id,
|
|
497
|
+
metadata=metadata,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def create_confidence_item(
|
|
502
|
+
text: str,
|
|
503
|
+
item_template_id: UUID | None = None,
|
|
504
|
+
metadata: dict[str, MetadataValue] | None = None,
|
|
505
|
+
) -> Item:
|
|
506
|
+
"""Create a confidence rating item (0-100%).
|
|
507
|
+
|
|
508
|
+
Convenience function for confidence percentage with bounds (0, 100).
|
|
509
|
+
|
|
510
|
+
Parameters
|
|
511
|
+
----------
|
|
512
|
+
text : str
|
|
513
|
+
The text or question to rate confidence for.
|
|
514
|
+
item_template_id : UUID | None
|
|
515
|
+
Template ID for the item. If None, generates new UUID.
|
|
516
|
+
metadata : dict[str, MetadataValue] | None
|
|
517
|
+
Additional metadata for item_metadata field.
|
|
518
|
+
|
|
519
|
+
Returns
|
|
520
|
+
-------
|
|
521
|
+
Item
|
|
522
|
+
Confidence magnitude item.
|
|
523
|
+
|
|
524
|
+
Examples
|
|
525
|
+
--------
|
|
526
|
+
>>> item = create_confidence_item("Is this sentence grammatical?")
|
|
527
|
+
>>> item.item_metadata["unit"]
|
|
528
|
+
'%'
|
|
529
|
+
>>> item.item_metadata["max_value"]
|
|
530
|
+
100
|
|
531
|
+
"""
|
|
532
|
+
return create_magnitude_item(
|
|
533
|
+
text,
|
|
534
|
+
unit="%",
|
|
535
|
+
bounds=(0, 100),
|
|
536
|
+
prompt="How confident are you?",
|
|
537
|
+
step=1,
|
|
538
|
+
item_template_id=item_template_id,
|
|
539
|
+
metadata=metadata,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _extract_text_from_item(item: Item) -> str:
|
|
544
|
+
"""Extract text from item's rendered_elements.
|
|
545
|
+
|
|
546
|
+
Tries common keys: "text", "sentence", "content".
|
|
547
|
+
Raises error if no suitable text found.
|
|
548
|
+
|
|
549
|
+
Parameters
|
|
550
|
+
----------
|
|
551
|
+
item : Item
|
|
552
|
+
Item to extract text from.
|
|
553
|
+
|
|
554
|
+
Returns
|
|
555
|
+
-------
|
|
556
|
+
str
|
|
557
|
+
Extracted text.
|
|
558
|
+
|
|
559
|
+
Raises
|
|
560
|
+
------
|
|
561
|
+
ValueError
|
|
562
|
+
If no suitable text key found in rendered_elements.
|
|
563
|
+
"""
|
|
564
|
+
for key in ["text", "sentence", "content"]:
|
|
565
|
+
if key in item.rendered_elements:
|
|
566
|
+
return item.rendered_elements[key]
|
|
567
|
+
|
|
568
|
+
raise ValueError(
|
|
569
|
+
f"Cannot extract text from item {item.id}. "
|
|
570
|
+
f"Expected one of ['text', 'sentence', 'content'] in rendered_elements, "
|
|
571
|
+
f"but found keys: {list(item.rendered_elements.keys())}. "
|
|
572
|
+
f"Use the extract_text parameter to provide a custom extraction function."
|
|
573
|
+
)
|