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.
Files changed (231) hide show
  1. bead/__init__.py +11 -0
  2. bead/__main__.py +11 -0
  3. bead/active_learning/__init__.py +15 -0
  4. bead/active_learning/config.py +231 -0
  5. bead/active_learning/loop.py +566 -0
  6. bead/active_learning/models/__init__.py +24 -0
  7. bead/active_learning/models/base.py +852 -0
  8. bead/active_learning/models/binary.py +910 -0
  9. bead/active_learning/models/categorical.py +943 -0
  10. bead/active_learning/models/cloze.py +862 -0
  11. bead/active_learning/models/forced_choice.py +956 -0
  12. bead/active_learning/models/free_text.py +773 -0
  13. bead/active_learning/models/lora.py +365 -0
  14. bead/active_learning/models/magnitude.py +835 -0
  15. bead/active_learning/models/multi_select.py +795 -0
  16. bead/active_learning/models/ordinal_scale.py +811 -0
  17. bead/active_learning/models/peft_adapter.py +155 -0
  18. bead/active_learning/models/random_effects.py +639 -0
  19. bead/active_learning/selection.py +354 -0
  20. bead/active_learning/strategies.py +391 -0
  21. bead/active_learning/trainers/__init__.py +26 -0
  22. bead/active_learning/trainers/base.py +210 -0
  23. bead/active_learning/trainers/data_collator.py +172 -0
  24. bead/active_learning/trainers/dataset_utils.py +261 -0
  25. bead/active_learning/trainers/huggingface.py +304 -0
  26. bead/active_learning/trainers/lightning.py +324 -0
  27. bead/active_learning/trainers/metrics.py +424 -0
  28. bead/active_learning/trainers/mixed_effects.py +551 -0
  29. bead/active_learning/trainers/model_wrapper.py +509 -0
  30. bead/active_learning/trainers/registry.py +104 -0
  31. bead/adapters/__init__.py +11 -0
  32. bead/adapters/huggingface.py +61 -0
  33. bead/behavioral/__init__.py +116 -0
  34. bead/behavioral/analytics.py +646 -0
  35. bead/behavioral/extraction.py +343 -0
  36. bead/behavioral/merging.py +343 -0
  37. bead/cli/__init__.py +11 -0
  38. bead/cli/active_learning.py +513 -0
  39. bead/cli/active_learning_commands.py +779 -0
  40. bead/cli/completion.py +359 -0
  41. bead/cli/config.py +624 -0
  42. bead/cli/constraint_builders.py +286 -0
  43. bead/cli/deployment.py +859 -0
  44. bead/cli/deployment_trials.py +493 -0
  45. bead/cli/deployment_ui.py +332 -0
  46. bead/cli/display.py +378 -0
  47. bead/cli/items.py +960 -0
  48. bead/cli/items_factories.py +776 -0
  49. bead/cli/list_constraints.py +714 -0
  50. bead/cli/lists.py +490 -0
  51. bead/cli/main.py +430 -0
  52. bead/cli/models.py +877 -0
  53. bead/cli/resource_loaders.py +621 -0
  54. bead/cli/resources.py +1036 -0
  55. bead/cli/shell.py +356 -0
  56. bead/cli/simulate.py +840 -0
  57. bead/cli/templates.py +1158 -0
  58. bead/cli/training.py +1080 -0
  59. bead/cli/utils.py +614 -0
  60. bead/cli/workflow.py +1273 -0
  61. bead/config/__init__.py +68 -0
  62. bead/config/active_learning.py +1009 -0
  63. bead/config/config.py +192 -0
  64. bead/config/defaults.py +118 -0
  65. bead/config/deployment.py +217 -0
  66. bead/config/env.py +147 -0
  67. bead/config/item.py +45 -0
  68. bead/config/list.py +193 -0
  69. bead/config/loader.py +149 -0
  70. bead/config/logging.py +42 -0
  71. bead/config/model.py +49 -0
  72. bead/config/paths.py +46 -0
  73. bead/config/profiles.py +320 -0
  74. bead/config/resources.py +47 -0
  75. bead/config/serialization.py +210 -0
  76. bead/config/simulation.py +206 -0
  77. bead/config/template.py +238 -0
  78. bead/config/validation.py +267 -0
  79. bead/data/__init__.py +65 -0
  80. bead/data/base.py +87 -0
  81. bead/data/identifiers.py +97 -0
  82. bead/data/language_codes.py +61 -0
  83. bead/data/metadata.py +270 -0
  84. bead/data/range.py +123 -0
  85. bead/data/repository.py +358 -0
  86. bead/data/serialization.py +249 -0
  87. bead/data/timestamps.py +89 -0
  88. bead/data/validation.py +349 -0
  89. bead/data_collection/__init__.py +11 -0
  90. bead/data_collection/jatos.py +223 -0
  91. bead/data_collection/merger.py +154 -0
  92. bead/data_collection/prolific.py +198 -0
  93. bead/deployment/__init__.py +5 -0
  94. bead/deployment/distribution.py +402 -0
  95. bead/deployment/jatos/__init__.py +1 -0
  96. bead/deployment/jatos/api.py +200 -0
  97. bead/deployment/jatos/exporter.py +210 -0
  98. bead/deployment/jspsych/__init__.py +9 -0
  99. bead/deployment/jspsych/biome.json +44 -0
  100. bead/deployment/jspsych/config.py +411 -0
  101. bead/deployment/jspsych/generator.py +598 -0
  102. bead/deployment/jspsych/package.json +51 -0
  103. bead/deployment/jspsych/pnpm-lock.yaml +2141 -0
  104. bead/deployment/jspsych/randomizer.py +299 -0
  105. bead/deployment/jspsych/src/lib/list-distributor.test.ts +327 -0
  106. bead/deployment/jspsych/src/lib/list-distributor.ts +1282 -0
  107. bead/deployment/jspsych/src/lib/randomizer.test.ts +232 -0
  108. bead/deployment/jspsych/src/lib/randomizer.ts +367 -0
  109. bead/deployment/jspsych/src/plugins/cloze-dropdown.ts +252 -0
  110. bead/deployment/jspsych/src/plugins/forced-choice.ts +265 -0
  111. bead/deployment/jspsych/src/plugins/plugins.test.ts +141 -0
  112. bead/deployment/jspsych/src/plugins/rating.ts +248 -0
  113. bead/deployment/jspsych/src/slopit/index.ts +9 -0
  114. bead/deployment/jspsych/src/types/jatos.d.ts +256 -0
  115. bead/deployment/jspsych/src/types/jspsych.d.ts +228 -0
  116. bead/deployment/jspsych/templates/experiment.css +1 -0
  117. bead/deployment/jspsych/templates/experiment.js.template +289 -0
  118. bead/deployment/jspsych/templates/index.html +51 -0
  119. bead/deployment/jspsych/templates/randomizer.js +241 -0
  120. bead/deployment/jspsych/templates/randomizer.js.template +313 -0
  121. bead/deployment/jspsych/trials.py +723 -0
  122. bead/deployment/jspsych/tsconfig.json +23 -0
  123. bead/deployment/jspsych/tsup.config.ts +30 -0
  124. bead/deployment/jspsych/ui/__init__.py +1 -0
  125. bead/deployment/jspsych/ui/components.py +383 -0
  126. bead/deployment/jspsych/ui/styles.py +411 -0
  127. bead/dsl/__init__.py +80 -0
  128. bead/dsl/ast.py +168 -0
  129. bead/dsl/context.py +178 -0
  130. bead/dsl/errors.py +71 -0
  131. bead/dsl/evaluator.py +570 -0
  132. bead/dsl/grammar.lark +81 -0
  133. bead/dsl/parser.py +231 -0
  134. bead/dsl/stdlib.py +929 -0
  135. bead/evaluation/__init__.py +13 -0
  136. bead/evaluation/convergence.py +485 -0
  137. bead/evaluation/interannotator.py +398 -0
  138. bead/items/__init__.py +40 -0
  139. bead/items/adapters/__init__.py +70 -0
  140. bead/items/adapters/anthropic.py +224 -0
  141. bead/items/adapters/api_utils.py +167 -0
  142. bead/items/adapters/base.py +216 -0
  143. bead/items/adapters/google.py +259 -0
  144. bead/items/adapters/huggingface.py +1074 -0
  145. bead/items/adapters/openai.py +323 -0
  146. bead/items/adapters/registry.py +202 -0
  147. bead/items/adapters/sentence_transformers.py +224 -0
  148. bead/items/adapters/togetherai.py +309 -0
  149. bead/items/binary.py +515 -0
  150. bead/items/cache.py +558 -0
  151. bead/items/categorical.py +593 -0
  152. bead/items/cloze.py +757 -0
  153. bead/items/constructor.py +784 -0
  154. bead/items/forced_choice.py +413 -0
  155. bead/items/free_text.py +681 -0
  156. bead/items/generation.py +432 -0
  157. bead/items/item.py +396 -0
  158. bead/items/item_template.py +787 -0
  159. bead/items/magnitude.py +573 -0
  160. bead/items/multi_select.py +621 -0
  161. bead/items/ordinal_scale.py +569 -0
  162. bead/items/scoring.py +448 -0
  163. bead/items/validation.py +723 -0
  164. bead/lists/__init__.py +30 -0
  165. bead/lists/balancer.py +263 -0
  166. bead/lists/constraints.py +1067 -0
  167. bead/lists/experiment_list.py +286 -0
  168. bead/lists/list_collection.py +378 -0
  169. bead/lists/partitioner.py +1141 -0
  170. bead/lists/stratification.py +254 -0
  171. bead/participants/__init__.py +73 -0
  172. bead/participants/collection.py +699 -0
  173. bead/participants/merging.py +312 -0
  174. bead/participants/metadata_spec.py +491 -0
  175. bead/participants/models.py +276 -0
  176. bead/resources/__init__.py +29 -0
  177. bead/resources/adapters/__init__.py +19 -0
  178. bead/resources/adapters/base.py +104 -0
  179. bead/resources/adapters/cache.py +128 -0
  180. bead/resources/adapters/glazing.py +508 -0
  181. bead/resources/adapters/registry.py +117 -0
  182. bead/resources/adapters/unimorph.py +796 -0
  183. bead/resources/classification.py +856 -0
  184. bead/resources/constraint_builders.py +329 -0
  185. bead/resources/constraints.py +165 -0
  186. bead/resources/lexical_item.py +223 -0
  187. bead/resources/lexicon.py +744 -0
  188. bead/resources/loaders.py +209 -0
  189. bead/resources/template.py +441 -0
  190. bead/resources/template_collection.py +707 -0
  191. bead/resources/template_generation.py +349 -0
  192. bead/simulation/__init__.py +29 -0
  193. bead/simulation/annotators/__init__.py +15 -0
  194. bead/simulation/annotators/base.py +175 -0
  195. bead/simulation/annotators/distance_based.py +135 -0
  196. bead/simulation/annotators/lm_based.py +114 -0
  197. bead/simulation/annotators/oracle.py +182 -0
  198. bead/simulation/annotators/random.py +181 -0
  199. bead/simulation/dsl_extension/__init__.py +3 -0
  200. bead/simulation/noise_models/__init__.py +13 -0
  201. bead/simulation/noise_models/base.py +42 -0
  202. bead/simulation/noise_models/random_noise.py +82 -0
  203. bead/simulation/noise_models/systematic.py +132 -0
  204. bead/simulation/noise_models/temperature.py +86 -0
  205. bead/simulation/runner.py +144 -0
  206. bead/simulation/strategies/__init__.py +23 -0
  207. bead/simulation/strategies/base.py +123 -0
  208. bead/simulation/strategies/binary.py +103 -0
  209. bead/simulation/strategies/categorical.py +123 -0
  210. bead/simulation/strategies/cloze.py +224 -0
  211. bead/simulation/strategies/forced_choice.py +127 -0
  212. bead/simulation/strategies/free_text.py +105 -0
  213. bead/simulation/strategies/magnitude.py +116 -0
  214. bead/simulation/strategies/multi_select.py +129 -0
  215. bead/simulation/strategies/ordinal_scale.py +131 -0
  216. bead/templates/__init__.py +27 -0
  217. bead/templates/adapters/__init__.py +17 -0
  218. bead/templates/adapters/base.py +128 -0
  219. bead/templates/adapters/cache.py +178 -0
  220. bead/templates/adapters/huggingface.py +312 -0
  221. bead/templates/combinatorics.py +103 -0
  222. bead/templates/filler.py +605 -0
  223. bead/templates/renderers.py +177 -0
  224. bead/templates/resolver.py +178 -0
  225. bead/templates/strategies.py +1806 -0
  226. bead/templates/streaming.py +195 -0
  227. bead-0.1.0.dist-info/METADATA +212 -0
  228. bead-0.1.0.dist-info/RECORD +231 -0
  229. bead-0.1.0.dist-info/WHEEL +4 -0
  230. bead-0.1.0.dist-info/entry_points.txt +2 -0
  231. bead-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,776 @@
1
+ """Task-type-specific item creation commands for bead CLI.
2
+
3
+ This module provides CLI commands for creating experimental items for all 8 supported
4
+ task types. Each task type has specialized creation functions that wrap the core
5
+ utilities in bead.items.
6
+
7
+ Supported task types:
8
+ - forced_choice: N-AFC (2AFC, 3AFC, etc.)
9
+ - ordinal_scale: Likert scales, sliders
10
+ - categorical: Unordered categories (NLI, semantic relations)
11
+ - binary: Yes/No, True/False
12
+ - multi_select: Multiple selection (checkboxes)
13
+ - magnitude: Unbounded numeric (reading time, confidence)
14
+ - free_text: Open-ended text responses
15
+ - cloze: Fill-in-the-blank
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import itertools
21
+ import random
22
+ from pathlib import Path
23
+
24
+ import click
25
+ from rich.console import Console
26
+
27
+ from bead.cli.display import (
28
+ create_progress,
29
+ display_file_stats,
30
+ print_error,
31
+ print_info,
32
+ print_success,
33
+ )
34
+ from bead.cli.utils import parse_key_value_pairs
35
+ from bead.items.binary import create_binary_items_from_texts
36
+ from bead.items.categorical import (
37
+ create_categorical_item,
38
+ create_nli_item,
39
+ )
40
+ from bead.items.cloze import create_simple_cloze_item
41
+ from bead.items.forced_choice import create_forced_choice_item
42
+ from bead.items.free_text import (
43
+ create_free_text_items_from_texts,
44
+ )
45
+ from bead.items.item import Item, MetadataValue
46
+ from bead.items.magnitude import (
47
+ create_magnitude_items_from_texts,
48
+ )
49
+ from bead.items.multi_select import create_multi_select_item
50
+ from bead.items.ordinal_scale import (
51
+ create_likert_7_item,
52
+ create_ordinal_scale_items_from_texts,
53
+ )
54
+
55
+ console = Console()
56
+
57
+
58
+ # ==================== Forced Choice Commands ====================
59
+
60
+
61
+ @click.command()
62
+ @click.argument("options", nargs=-1, required=True)
63
+ @click.option(
64
+ "--output",
65
+ "-o",
66
+ type=click.Path(path_type=Path),
67
+ required=True,
68
+ help="Output JSONL file",
69
+ )
70
+ @click.option(
71
+ "--metadata",
72
+ type=str,
73
+ help="Metadata as key=value pairs (comma-separated)",
74
+ )
75
+ def create_forced_choice(
76
+ options: tuple[str, ...],
77
+ output: Path,
78
+ metadata: str | None,
79
+ ) -> None:
80
+ r"""Create a single forced-choice item from options.
81
+
82
+ Examples
83
+ --------
84
+ $ bead items create-forced-choice "Option A" "Option B" -o item.jsonl
85
+
86
+ $ bead items create-forced-choice "The cat" "The dog" "The bird" \\
87
+ --metadata "contrast=subject" -o item.jsonl
88
+ """
89
+ try:
90
+ if len(options) < 2:
91
+ print_error("At least 2 options required")
92
+ return
93
+
94
+ # Parse metadata
95
+ meta_dict_str: dict[str, str] = (
96
+ parse_key_value_pairs(metadata) if metadata else {}
97
+ )
98
+ # Cast to MetadataValue dict (str is a valid MetadataValue)
99
+ meta_dict: dict[str, MetadataValue] = dict(meta_dict_str)
100
+
101
+ # Create item
102
+ item: Item = create_forced_choice_item(*options, metadata=meta_dict)
103
+
104
+ # Save
105
+ output.parent.mkdir(parents=True, exist_ok=True)
106
+ with open(output, "w") as f:
107
+ f.write(item.model_dump_json() + "\n")
108
+
109
+ print_success(f"Created forced-choice item: {output}")
110
+
111
+ except Exception as e:
112
+ print_error(f"Failed to create forced-choice item: {e}")
113
+
114
+
115
+ @click.command()
116
+ @click.option(
117
+ "--texts-file",
118
+ type=click.Path(exists=True, path_type=Path),
119
+ required=True,
120
+ help="File with text options (one per line)",
121
+ )
122
+ @click.option(
123
+ "--output",
124
+ "-o",
125
+ type=click.Path(path_type=Path),
126
+ required=True,
127
+ help="Output JSONL file",
128
+ )
129
+ @click.option(
130
+ "--n-alternatives",
131
+ type=int,
132
+ default=2,
133
+ help="Number of alternatives per item (default: 2)",
134
+ )
135
+ @click.option(
136
+ "--sample",
137
+ type=int,
138
+ help="Sample N items randomly",
139
+ )
140
+ def create_forced_choice_from_texts(
141
+ texts_file: Path,
142
+ output: Path,
143
+ n_alternatives: int,
144
+ sample: int | None,
145
+ ) -> None:
146
+ r"""Create forced-choice items from text file.
147
+
148
+ Examples
149
+ --------
150
+ $ bead items create-forced-choice-from-texts \\
151
+ --texts-file sentences.txt --output items.jsonl
152
+
153
+ $ bead items create-forced-choice-from-texts \\
154
+ --texts-file sentences.txt --n-alternatives 3 \\
155
+ --sample 100 --output items.jsonl
156
+ """
157
+ try:
158
+ # Load texts
159
+ texts: list[str] = [line.strip() for line in open(texts_file) if line.strip()]
160
+ print_info(f"Loaded {len(texts)} texts")
161
+
162
+ # Create items by generating all combinations of n_alternatives from texts
163
+ items: list[Item] = []
164
+ for combination in itertools.combinations(texts, n_alternatives):
165
+ item: Item = create_forced_choice_item(*combination)
166
+ items.append(item)
167
+
168
+ print_info(f"Created {len(items)} forced-choice items")
169
+
170
+ # Sample if requested
171
+ if sample and sample < len(items):
172
+ items = random.sample(items, sample)
173
+ print_info(f"Sampled {sample} items")
174
+
175
+ # Save
176
+ output.parent.mkdir(parents=True, exist_ok=True)
177
+ with open(output, "w") as f:
178
+ for item in items:
179
+ f.write(item.model_dump_json() + "\n")
180
+
181
+ display_file_stats(output, len(items), "forced-choice items")
182
+
183
+ except Exception as e:
184
+ print_error(f"Failed to create forced-choice items: {e}")
185
+
186
+
187
+ # ==================== Ordinal Scale Commands ====================
188
+
189
+
190
+ @click.command()
191
+ @click.option(
192
+ "--text",
193
+ type=str,
194
+ required=True,
195
+ help="Text to rate",
196
+ )
197
+ @click.option(
198
+ "--output",
199
+ "-o",
200
+ type=click.Path(path_type=Path),
201
+ required=True,
202
+ help="Output JSONL file",
203
+ )
204
+ @click.option(
205
+ "--prompt",
206
+ type=str,
207
+ default="Rate this item:",
208
+ help="Rating prompt",
209
+ )
210
+ def create_likert_7(
211
+ text: str,
212
+ output: Path,
213
+ prompt: str,
214
+ ) -> None:
215
+ r"""Create a 7-point Likert scale item.
216
+
217
+ Examples
218
+ --------
219
+ $ bead items create-likert-7 --text "The cat sat on the mat" -o item.jsonl
220
+
221
+ $ bead items create-likert-7 --text "Sentence text" \\
222
+ --prompt "How natural is this?" -o item.jsonl
223
+ """
224
+ try:
225
+ item = create_likert_7_item(text, prompt=prompt)
226
+
227
+ output.parent.mkdir(parents=True, exist_ok=True)
228
+ with open(output, "w") as f:
229
+ f.write(item.model_dump_json() + "\n")
230
+
231
+ print_success(f"Created Likert-7 item: {output}")
232
+
233
+ except Exception as e:
234
+ print_error(f"Failed to create Likert-7 item: {e}")
235
+
236
+
237
+ @click.command()
238
+ @click.option(
239
+ "--texts-file",
240
+ type=click.Path(exists=True, path_type=Path),
241
+ required=True,
242
+ help="File with texts to rate (one per line)",
243
+ )
244
+ @click.option(
245
+ "--output",
246
+ "-o",
247
+ type=click.Path(path_type=Path),
248
+ required=True,
249
+ help="Output JSONL file",
250
+ )
251
+ @click.option(
252
+ "--scale-min",
253
+ type=int,
254
+ default=1,
255
+ help="Minimum scale value (default: 1)",
256
+ )
257
+ @click.option(
258
+ "--scale-max",
259
+ type=int,
260
+ default=7,
261
+ help="Maximum scale value (default: 7)",
262
+ )
263
+ @click.option(
264
+ "--prompt",
265
+ type=str,
266
+ default="Rate this item:",
267
+ help="Rating prompt",
268
+ )
269
+ def create_ordinal_scale_from_texts(
270
+ texts_file: Path,
271
+ output: Path,
272
+ scale_min: int,
273
+ scale_max: int,
274
+ prompt: str,
275
+ ) -> None:
276
+ r"""Create ordinal scale items from text file.
277
+
278
+ Examples
279
+ --------
280
+ $ bead items create-ordinal-scale-from-texts \\
281
+ --texts-file sentences.txt --output items.jsonl
282
+
283
+ $ bead items create-ordinal-scale-from-texts \\
284
+ --texts-file sentences.txt --scale-min 1 --scale-max 5 \\
285
+ --prompt "How acceptable?" --output items.jsonl
286
+ """
287
+ try:
288
+ # Load texts
289
+ texts = [line.strip() for line in open(texts_file) if line.strip()]
290
+ print_info(f"Loaded {len(texts)} texts")
291
+
292
+ # Create items
293
+ with create_progress() as progress:
294
+ task = progress.add_task(
295
+ "Creating ordinal scale items...", total=len(texts)
296
+ )
297
+ items = create_ordinal_scale_items_from_texts(
298
+ texts, scale_bounds=(scale_min, scale_max), prompt=prompt
299
+ )
300
+ progress.update(task, completed=len(texts))
301
+
302
+ # Save
303
+ output.parent.mkdir(parents=True, exist_ok=True)
304
+ with open(output, "w") as f:
305
+ for item in items:
306
+ f.write(item.model_dump_json() + "\n")
307
+
308
+ display_file_stats(output, len(items), "ordinal scale items")
309
+
310
+ except Exception as e:
311
+ print_error(f"Failed to create ordinal scale items: {e}")
312
+
313
+
314
+ # ==================== Categorical Commands ====================
315
+
316
+
317
+ @click.command()
318
+ @click.option(
319
+ "--premise",
320
+ type=str,
321
+ required=True,
322
+ help="Premise text",
323
+ )
324
+ @click.option(
325
+ "--hypothesis",
326
+ type=str,
327
+ required=True,
328
+ help="Hypothesis text",
329
+ )
330
+ @click.option(
331
+ "--output",
332
+ "-o",
333
+ type=click.Path(path_type=Path),
334
+ required=True,
335
+ help="Output JSONL file",
336
+ )
337
+ def create_nli(
338
+ premise: str,
339
+ hypothesis: str,
340
+ output: Path,
341
+ ) -> None:
342
+ r"""Create an NLI (natural language inference) item.
343
+
344
+ Examples
345
+ --------
346
+ $ bead items create-nli \\
347
+ --premise "All dogs bark" \\
348
+ --hypothesis "Some dogs bark" \\
349
+ -o item.jsonl
350
+ """
351
+ try:
352
+ item = create_nli_item(premise, hypothesis)
353
+
354
+ output.parent.mkdir(parents=True, exist_ok=True)
355
+ with open(output, "w") as f:
356
+ f.write(item.model_dump_json() + "\n")
357
+
358
+ print_success(f"Created NLI item: {output}")
359
+
360
+ except Exception as e:
361
+ print_error(f"Failed to create NLI item: {e}")
362
+
363
+
364
+ @click.command()
365
+ @click.option(
366
+ "--text",
367
+ type=str,
368
+ required=True,
369
+ help="Text for categorization",
370
+ )
371
+ @click.option(
372
+ "--categories",
373
+ type=str,
374
+ required=True,
375
+ help="Categories (comma-separated)",
376
+ )
377
+ @click.option(
378
+ "--output",
379
+ "-o",
380
+ type=click.Path(path_type=Path),
381
+ required=True,
382
+ help="Output JSONL file",
383
+ )
384
+ @click.option(
385
+ "--prompt",
386
+ type=str,
387
+ default="Categorize this item:",
388
+ help="Categorization prompt",
389
+ )
390
+ def create_categorical(
391
+ text: str,
392
+ categories: str,
393
+ output: Path,
394
+ prompt: str,
395
+ ) -> None:
396
+ r"""Create a categorical item.
397
+
398
+ Examples
399
+ --------
400
+ $ bead items create-categorical --text "Example text" \\
401
+ --categories "entailment,contradiction,neutral" -o item.jsonl
402
+ """
403
+ try:
404
+ cat_list = [c.strip() for c in categories.split(",")]
405
+ item = create_categorical_item(text, categories=cat_list, prompt=prompt)
406
+
407
+ output.parent.mkdir(parents=True, exist_ok=True)
408
+ with open(output, "w") as f:
409
+ f.write(item.model_dump_json() + "\n")
410
+
411
+ print_success(f"Created categorical item: {output}")
412
+
413
+ except Exception as e:
414
+ print_error(f"Failed to create categorical item: {e}")
415
+
416
+
417
+ # ==================== Binary Commands ====================
418
+
419
+
420
+ @click.command()
421
+ @click.option(
422
+ "--texts-file",
423
+ type=click.Path(exists=True, path_type=Path),
424
+ required=True,
425
+ help="File with texts (one per line)",
426
+ )
427
+ @click.option(
428
+ "--output",
429
+ "-o",
430
+ type=click.Path(path_type=Path),
431
+ required=True,
432
+ help="Output JSONL file",
433
+ )
434
+ @click.option(
435
+ "--prompt",
436
+ type=str,
437
+ default="Is this acceptable?",
438
+ help="Binary judgment prompt",
439
+ )
440
+ def create_binary_from_texts(
441
+ texts_file: Path,
442
+ output: Path,
443
+ prompt: str,
444
+ ) -> None:
445
+ r"""Create binary judgment items from text file.
446
+
447
+ Examples
448
+ --------
449
+ $ bead items create-binary-from-texts \\
450
+ --texts-file sentences.txt --output items.jsonl
451
+
452
+ $ bead items create-binary-from-texts \\
453
+ --texts-file sentences.txt \\
454
+ --prompt "Is this grammatical?" --output items.jsonl
455
+ """
456
+ try:
457
+ # Load texts
458
+ texts = [line.strip() for line in open(texts_file) if line.strip()]
459
+ print_info(f"Loaded {len(texts)} texts")
460
+
461
+ # Create items
462
+ items = create_binary_items_from_texts(texts, prompt=prompt)
463
+
464
+ # Save
465
+ output.parent.mkdir(parents=True, exist_ok=True)
466
+ with open(output, "w") as f:
467
+ for item in items:
468
+ f.write(item.model_dump_json() + "\n")
469
+
470
+ display_file_stats(output, len(items), "binary items")
471
+
472
+ except Exception as e:
473
+ print_error(f"Failed to create binary items: {e}")
474
+
475
+
476
+ # ==================== Multi-Select Commands ====================
477
+
478
+
479
+ @click.command()
480
+ @click.option(
481
+ "--texts-file",
482
+ type=click.Path(exists=True, path_type=Path),
483
+ required=True,
484
+ help="File with texts (one per line)",
485
+ )
486
+ @click.option(
487
+ "--options",
488
+ type=str,
489
+ required=True,
490
+ help="Options (comma-separated)",
491
+ )
492
+ @click.option(
493
+ "--output",
494
+ "-o",
495
+ type=click.Path(path_type=Path),
496
+ required=True,
497
+ help="Output JSONL file",
498
+ )
499
+ @click.option(
500
+ "--min-selections",
501
+ type=int,
502
+ default=1,
503
+ help="Minimum selections (default: 1)",
504
+ )
505
+ @click.option(
506
+ "--max-selections",
507
+ type=int,
508
+ help="Maximum selections (default: all)",
509
+ )
510
+ def create_multi_select_from_texts(
511
+ texts_file: Path,
512
+ options: str,
513
+ output: Path,
514
+ min_selections: int,
515
+ max_selections: int | None,
516
+ ) -> None:
517
+ r"""Create multi-select items from text file.
518
+
519
+ Examples
520
+ --------
521
+ $ bead items create-multi-select-from-texts \\
522
+ --texts-file sentences.txt \\
523
+ --options "Agent,Patient,Theme,Goal" \\
524
+ --output items.jsonl
525
+
526
+ $ bead items create-multi-select-from-texts \\
527
+ --texts-file sentences.txt \\
528
+ --options "Semantic,Syntactic,Pragmatic" \\
529
+ --min-selections 1 --max-selections 2 \\
530
+ --output items.jsonl
531
+ """
532
+ try:
533
+ # Load texts
534
+ texts: list[str] = [line.strip() for line in open(texts_file) if line.strip()]
535
+ print_info(f"Loaded {len(texts)} texts")
536
+
537
+ # Parse options
538
+ option_list: list[str] = [o.strip() for o in options.split(",")]
539
+
540
+ # Create items - one multi-select item per text, using options as selections
541
+ items: list[Item] = []
542
+ for text in texts:
543
+ # For multi-select, we need options. Use the text as metadata
544
+ # and the option_list as the actual options
545
+ meta: dict[str, MetadataValue] = {"stimulus": text}
546
+ item: Item = create_multi_select_item(
547
+ *option_list,
548
+ min_selections=min_selections,
549
+ max_selections=max_selections,
550
+ metadata=meta,
551
+ )
552
+ items.append(item)
553
+
554
+ print_info(f"Created {len(items)} multi-select items")
555
+
556
+ # Save
557
+ output.parent.mkdir(parents=True, exist_ok=True)
558
+ with open(output, "w") as f:
559
+ for item in items:
560
+ f.write(item.model_dump_json() + "\n")
561
+
562
+ display_file_stats(output, len(items), "multi-select items")
563
+
564
+ except Exception as e:
565
+ print_error(f"Failed to create multi-select items: {e}")
566
+
567
+
568
+ # ==================== Magnitude Commands ====================
569
+
570
+
571
+ @click.command()
572
+ @click.option(
573
+ "--texts-file",
574
+ type=click.Path(exists=True, path_type=Path),
575
+ required=True,
576
+ help="File with texts (one per line)",
577
+ )
578
+ @click.option(
579
+ "--output",
580
+ "-o",
581
+ type=click.Path(path_type=Path),
582
+ required=True,
583
+ help="Output JSONL file",
584
+ )
585
+ @click.option(
586
+ "--measure",
587
+ type=str,
588
+ default="value",
589
+ help="Measure name (default: 'value')",
590
+ )
591
+ @click.option(
592
+ "--prompt",
593
+ type=str,
594
+ default="Enter value:",
595
+ help="Input prompt",
596
+ )
597
+ def create_magnitude_from_texts(
598
+ texts_file: Path,
599
+ output: Path,
600
+ measure: str,
601
+ prompt: str,
602
+ ) -> None:
603
+ r"""Create magnitude estimation items from text file.
604
+
605
+ Examples
606
+ --------
607
+ $ bead items create-magnitude-from-texts \\
608
+ --texts-file sentences.txt --output items.jsonl
609
+
610
+ $ bead items create-magnitude-from-texts \\
611
+ --texts-file sentences.txt \\
612
+ --measure "reading_time_ms" \\
613
+ --prompt "Reading time (ms):" \\
614
+ --output items.jsonl
615
+ """
616
+ try:
617
+ # Load texts
618
+ texts = [line.strip() for line in open(texts_file) if line.strip()]
619
+ print_info(f"Loaded {len(texts)} texts")
620
+
621
+ # Create items
622
+ items = create_magnitude_items_from_texts(texts, unit=measure, prompt=prompt)
623
+
624
+ # Save
625
+ output.parent.mkdir(parents=True, exist_ok=True)
626
+ with open(output, "w") as f:
627
+ for item in items:
628
+ f.write(item.model_dump_json() + "\n")
629
+
630
+ display_file_stats(output, len(items), "magnitude items")
631
+
632
+ except Exception as e:
633
+ print_error(f"Failed to create magnitude items: {e}")
634
+
635
+
636
+ # ==================== Free Text Commands ====================
637
+
638
+
639
+ @click.command()
640
+ @click.option(
641
+ "--texts-file",
642
+ type=click.Path(exists=True, path_type=Path),
643
+ required=True,
644
+ help="File with texts (one per line)",
645
+ )
646
+ @click.option(
647
+ "--output",
648
+ "-o",
649
+ type=click.Path(path_type=Path),
650
+ required=True,
651
+ help="Output JSONL file",
652
+ )
653
+ @click.option(
654
+ "--prompt",
655
+ type=str,
656
+ default="Provide your response:",
657
+ help="Response prompt",
658
+ )
659
+ def create_free_text_from_texts(
660
+ texts_file: Path,
661
+ output: Path,
662
+ prompt: str,
663
+ ) -> None:
664
+ r"""Create free text response items from text file.
665
+
666
+ Examples
667
+ --------
668
+ $ bead items create-free-text-from-texts \\
669
+ --texts-file sentences.txt --output items.jsonl
670
+
671
+ $ bead items create-free-text-from-texts \\
672
+ --texts-file sentences.txt \\
673
+ --prompt "Paraphrase this sentence:" \\
674
+ --output items.jsonl
675
+ """
676
+ try:
677
+ # Load texts
678
+ texts = [line.strip() for line in open(texts_file) if line.strip()]
679
+ print_info(f"Loaded {len(texts)} texts")
680
+
681
+ # Create items
682
+ items = create_free_text_items_from_texts(texts, prompt=prompt)
683
+
684
+ # Save
685
+ output.parent.mkdir(parents=True, exist_ok=True)
686
+ with open(output, "w") as f:
687
+ for item in items:
688
+ f.write(item.model_dump_json() + "\n")
689
+
690
+ display_file_stats(output, len(items), "free-text items")
691
+
692
+ except Exception as e:
693
+ print_error(f"Failed to create free-text items: {e}")
694
+
695
+
696
+ # ==================== Cloze Commands ====================
697
+
698
+
699
+ @click.command()
700
+ @click.option(
701
+ "--text",
702
+ type=str,
703
+ required=True,
704
+ help="Text with blank",
705
+ )
706
+ @click.option(
707
+ "--blank-position",
708
+ type=int,
709
+ required=True,
710
+ help="Position of blank (0-indexed word)",
711
+ )
712
+ @click.option(
713
+ "--output",
714
+ "-o",
715
+ type=click.Path(path_type=Path),
716
+ required=True,
717
+ help="Output JSONL file",
718
+ )
719
+ @click.option(
720
+ "--blank-label",
721
+ type=str,
722
+ default="blank",
723
+ help="Label for blank (default: 'blank')",
724
+ )
725
+ def create_simple_cloze(
726
+ text: str,
727
+ blank_position: int,
728
+ output: Path,
729
+ blank_label: str,
730
+ ) -> None:
731
+ r"""Create a simple cloze item.
732
+
733
+ Examples
734
+ --------
735
+ $ bead items create-simple-cloze \\
736
+ --text "The quick brown fox" \\
737
+ --blank-position 1 \\
738
+ -o item.jsonl
739
+
740
+ $ bead items create-simple-cloze \\
741
+ --text "The cat sat on the mat" \\
742
+ --blank-position 3 \\
743
+ --blank-label "preposition" \\
744
+ -o item.jsonl
745
+ """
746
+ try:
747
+ item = create_simple_cloze_item(
748
+ text=text,
749
+ blank_positions=[blank_position],
750
+ blank_labels=[blank_label],
751
+ )
752
+
753
+ output.parent.mkdir(parents=True, exist_ok=True)
754
+ with open(output, "w") as f:
755
+ f.write(item.model_dump_json() + "\n")
756
+
757
+ print_success(f"Created cloze item: {output}")
758
+
759
+ except Exception as e:
760
+ print_error(f"Failed to create cloze item: {e}")
761
+
762
+
763
+ # Export all commands
764
+ __all__ = [
765
+ "create_forced_choice",
766
+ "create_forced_choice_from_texts",
767
+ "create_likert_7",
768
+ "create_ordinal_scale_from_texts",
769
+ "create_nli",
770
+ "create_categorical",
771
+ "create_binary_from_texts",
772
+ "create_multi_select_from_texts",
773
+ "create_magnitude_from_texts",
774
+ "create_free_text_from_texts",
775
+ "create_simple_cloze",
776
+ ]