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,329 @@
1
+ """Abstract base classes for programmatic constraint generation.
2
+
3
+ This module provides language-agnostic base classes for building constraints
4
+ programmatically. Language-specific implementations should extend these bases.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from abc import ABC, abstractmethod
10
+ from typing import Any
11
+
12
+ from bead.resources.constraints import Constraint, ContextValue
13
+
14
+
15
+ class ConstraintBuilder(ABC):
16
+ """Abstract base class for programmatic constraint generation.
17
+
18
+ Constraint builders encapsulate logic for generating DSL constraints
19
+ based on configuration and rules. Subclasses implement specific
20
+ constraint generation strategies.
21
+
22
+ Examples
23
+ --------
24
+ >>> class NumberAgreementBuilder(ConstraintBuilder):
25
+ ... def build(self, *slot_names: str) -> Constraint:
26
+ ... # Generate number agreement constraint
27
+ ... pairs = []
28
+ ... for i, slot1 in enumerate(slot_names):
29
+ ... for slot2 in slot_names[i+1:]:
30
+ ... pairs.append(f"{slot1}.number == {slot2}.number")
31
+ ... return Constraint(
32
+ ... expression=" and ".join(pairs),
33
+ ... description=f"Number agreement: {', '.join(slot_names)}"
34
+ ... )
35
+ """
36
+
37
+ @abstractmethod
38
+ def build(self, *args: Any, **kwargs: Any) -> Constraint:
39
+ """Build a Constraint object.
40
+
41
+ Parameters
42
+ ----------
43
+ *args : Any
44
+ Positional arguments (slot names, properties, etc.).
45
+ **kwargs : Any
46
+ Keyword arguments (configuration options).
47
+
48
+ Returns
49
+ -------
50
+ Constraint
51
+ Generated constraint.
52
+ """
53
+ ...
54
+
55
+
56
+ class AgreementConstraintBuilder(ConstraintBuilder):
57
+ """Builder for feature agreement constraints.
58
+
59
+ Generates constraints that enforce feature agreement across slots
60
+ (e.g., number, gender, case). Supports exact matching or equivalence
61
+ classes via agreement rules.
62
+
63
+ Parameters
64
+ ----------
65
+ feature_name : str
66
+ Name of the feature to enforce agreement on (e.g., "number", "gender").
67
+ agreement_rules : dict[str, list[str]] | None
68
+ Optional equivalence classes. Maps canonical value to list of
69
+ equivalent values. For example:
70
+ {"singular": ["singular", "sing", "sg"], "plural": ["plural", "pl"]}
71
+
72
+ Examples
73
+ --------
74
+ Exact number agreement:
75
+ >>> builder = AgreementConstraintBuilder("number")
76
+ >>> constraint = builder.build("subject", "verb")
77
+ >>> expr = "subject.features.get('number') == verb.features.get('number')"
78
+ >>> expr in constraint.expression
79
+ True
80
+
81
+ Agreement with equivalence rules:
82
+ >>> rules = {"singular": ["sing", "sg"], "plural": ["pl"]}
83
+ >>> builder = AgreementConstraintBuilder("number", agreement_rules=rules)
84
+ >>> constraint = builder.build("det", "noun")
85
+ >>> "equiv_" in constraint.expression # Uses equivalence class checks
86
+ True
87
+ """
88
+
89
+ def __init__(
90
+ self,
91
+ feature_name: str,
92
+ *,
93
+ agreement_rules: dict[str, list[str]] | None = None,
94
+ ) -> None:
95
+ self.feature_name = feature_name
96
+ self.agreement_rules = agreement_rules
97
+
98
+ def build(self, *slot_names: str) -> Constraint:
99
+ """Build agreement constraint for given slots.
100
+
101
+ Parameters
102
+ ----------
103
+ *slot_names : str
104
+ Names of slots to enforce agreement between (≥2 required).
105
+
106
+ Returns
107
+ -------
108
+ Constraint
109
+ Agreement constraint.
110
+
111
+ Raises
112
+ ------
113
+ ValueError
114
+ If fewer than 2 slot names provided.
115
+ """
116
+ if len(slot_names) < 2:
117
+ raise ValueError("Agreement requires at least 2 slot names")
118
+
119
+ if self.agreement_rules:
120
+ return self._build_with_rules(slot_names)
121
+ else:
122
+ return self._build_exact_match(slot_names)
123
+
124
+ def _build_exact_match(self, slot_names: tuple[str, ...]) -> Constraint:
125
+ """Build exact match agreement constraint."""
126
+ # create pairwise equality checks
127
+ pairs: list[str] = []
128
+ for i, slot1 in enumerate(slot_names):
129
+ for slot2 in slot_names[i + 1 :]:
130
+ left = f"{slot1}.features.get('{self.feature_name}')"
131
+ right = f"{slot2}.features.get('{self.feature_name}')"
132
+ expr = f"{left} == {right}"
133
+ pairs.append(expr)
134
+
135
+ expression: str = " and ".join(pairs)
136
+ slot_list = ", ".join(slot_names)
137
+ description = f"{self.feature_name.capitalize()} agreement: {slot_list}"
138
+
139
+ return Constraint(expression=expression, description=description)
140
+
141
+ def _build_with_rules(self, slot_names: tuple[str, ...]) -> Constraint:
142
+ """Build agreement constraint with equivalence classes."""
143
+ # build context with equivalence class sets
144
+ context: dict[str, Any] = {}
145
+ for canonical, variants in self.agreement_rules.items(): # type: ignore
146
+ equiv_set = set(variants)
147
+ context[f"equiv_{canonical}"] = equiv_set
148
+
149
+ # build expression: check if all slots' values are in same equivalence class
150
+ equiv_checks: list[str] = []
151
+ for canonical in self.agreement_rules.keys(): # type: ignore
152
+ # all slots must have values in this equivalence class
153
+ slot_checks: list[str] = [
154
+ f"{slot}.features.get('{self.feature_name}') in equiv_{canonical}"
155
+ for slot in slot_names
156
+ ]
157
+ equiv_checks.append(f"({' and '.join(slot_checks)})")
158
+
159
+ expression: str = " or ".join(equiv_checks)
160
+ slot_list = ", ".join(slot_names)
161
+ feat_name = self.feature_name.capitalize()
162
+ description = f"{feat_name} agreement with rules: {slot_list}"
163
+
164
+ return Constraint(
165
+ expression=expression, context=context, description=description
166
+ )
167
+
168
+
169
+ class ConditionalConstraintBuilder(ConstraintBuilder):
170
+ """Builder for IF-THEN (conditional) constraints.
171
+
172
+ Generates constraints that enforce requirements when conditions are met.
173
+ Implements logical implication: IF condition THEN requirement.
174
+
175
+ Examples
176
+ --------
177
+ >>> builder = ConditionalConstraintBuilder()
178
+ >>> constraint = builder.build(
179
+ ... condition="det.lemma == 'a'",
180
+ ... requirement="noun.features.get('number') == 'singular'",
181
+ ... description="'a' requires singular noun"
182
+ ... )
183
+ >>> "not (" in constraint.expression # IF-THEN encoded as: not cond or req
184
+ True
185
+ """
186
+
187
+ def build(
188
+ self,
189
+ *,
190
+ condition: str,
191
+ requirement: str,
192
+ description: str | None = None,
193
+ context: dict[str, Any] | None = None,
194
+ ) -> Constraint:
195
+ """Build conditional constraint.
196
+
197
+ Parameters
198
+ ----------
199
+ condition : str
200
+ Condition expression (IF part).
201
+ requirement : str
202
+ Requirement expression (THEN part).
203
+ description : str | None
204
+ Human-readable description.
205
+ context : dict[str, Any] | None
206
+ Context variables for evaluation.
207
+
208
+ Returns
209
+ -------
210
+ Constraint
211
+ Conditional constraint.
212
+
213
+ Notes
214
+ -----
215
+ Logical implication (IF A THEN B) is encoded as: (NOT A) OR B
216
+ """
217
+ # encode IF-THEN as: (NOT condition) OR requirement
218
+ expression = f"not ({condition}) or ({requirement})"
219
+
220
+ return Constraint(
221
+ expression=expression,
222
+ context=context or {},
223
+ description=description,
224
+ )
225
+
226
+
227
+ class SetMembershipConstraintBuilder(ConstraintBuilder):
228
+ """Builder for whitelist/blacklist constraints.
229
+
230
+ Generates constraints that restrict slot properties to allowed values
231
+ (whitelist) or exclude forbidden values (blacklist).
232
+
233
+ Parameters
234
+ ----------
235
+ slot_name : str
236
+ Name of slot to constrain.
237
+ property_path : str
238
+ Dot-separated path to property (e.g., "lemma", "features.number").
239
+ allowed_values : set | None
240
+ Whitelist of allowed values (mutually exclusive with forbidden_values).
241
+ forbidden_values : set | None
242
+ Blacklist of forbidden values.
243
+ description : str | None
244
+ Custom description.
245
+
246
+ Examples
247
+ --------
248
+ Whitelist constraint:
249
+ >>> builder = SetMembershipConstraintBuilder()
250
+ >>> constraint = builder.build(
251
+ ... slot_name="verb",
252
+ ... property_path="lemma",
253
+ ... allowed_values={"walk", "run", "jump"},
254
+ ... description="Motion verbs only"
255
+ ... )
256
+ >>> "verb.lemma in allowed_values" in constraint.expression
257
+ True
258
+
259
+ Blacklist constraint:
260
+ >>> constraint = builder.build(
261
+ ... slot_name="verb",
262
+ ... property_path="lemma",
263
+ ... forbidden_values={"be", "have"},
264
+ ... description="Exclude copula and auxiliary"
265
+ ... )
266
+ >>> "verb.lemma not in forbidden_values" in constraint.expression
267
+ True
268
+ """
269
+
270
+ def build(
271
+ self,
272
+ *,
273
+ slot_name: str,
274
+ property_path: str,
275
+ allowed_values: set[str] | None = None,
276
+ forbidden_values: set[str] | None = None,
277
+ description: str | None = None,
278
+ ) -> Constraint:
279
+ """Build set membership constraint.
280
+
281
+ Parameters
282
+ ----------
283
+ slot_name : str
284
+ Slot to constrain.
285
+ property_path : str
286
+ Property path within slot.
287
+ allowed_values : set | None
288
+ Whitelist of allowed values.
289
+ forbidden_values : set | None
290
+ Blacklist of forbidden values.
291
+ description : str | None
292
+ Constraint description.
293
+
294
+ Returns
295
+ -------
296
+ Constraint
297
+ Set membership constraint.
298
+
299
+ Raises
300
+ ------
301
+ ValueError
302
+ If neither or both of allowed_values/forbidden_values provided.
303
+ """
304
+ # exactly one of allowed_values or forbidden_values must be provided
305
+ if (allowed_values is None) == (forbidden_values is None):
306
+ raise ValueError(
307
+ "Exactly one of 'allowed_values' or 'forbidden_values' must be provided"
308
+ )
309
+
310
+ expression: str
311
+ context: dict[str, ContextValue]
312
+
313
+ if allowed_values is not None:
314
+ expression = f"{slot_name}.{property_path} in allowed_values"
315
+ context = {"allowed_values": allowed_values}
316
+ if description is None:
317
+ prop_path = f"{slot_name}.{property_path}"
318
+ description = f"Restrict {prop_path} to allowed values"
319
+ else:
320
+ assert forbidden_values is not None
321
+ expression = f"{slot_name}.{property_path} not in forbidden_values"
322
+ context = {"forbidden_values": forbidden_values}
323
+ if description is None:
324
+ prop_path = f"{slot_name}.{property_path}"
325
+ description = f"Exclude {prop_path} from forbidden values"
326
+
327
+ return Constraint(
328
+ expression=expression, context=context, description=description
329
+ )
@@ -0,0 +1,165 @@
1
+ """Constraint models for lexical item selection.
2
+
3
+ This module provides a universal constraint model based on DSL expressions.
4
+ Constraints are pure DSL expressions with optional context variables.
5
+
6
+ Scope is determined by storage location:
7
+ - Slot.constraints → single-slot constraints (self = slot filler)
8
+ - Template.constraints → multi-slot constraints (slot names as variables)
9
+ - TemplateSequence.constraints → cross-template constraints
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from uuid import UUID
15
+
16
+ from pydantic import Field
17
+
18
+ from bead.data.base import BeadBaseModel
19
+ from bead.dsl.ast import ASTNode
20
+
21
+ # Type aliases for constraint context values
22
+ type ContextValue = str | int | float | bool | list[str] | set[str] | set[UUID]
23
+ type MetadataValue = (
24
+ str | int | float | bool | list[str | int | float] | dict[str, str | int | float]
25
+ )
26
+
27
+
28
+ class Constraint(BeadBaseModel):
29
+ """Universal constraint expressed via DSL.
30
+
31
+ All constraints are DSL expressions evaluated with a context dictionary.
32
+ The scope of the constraint is determined by where it is stored:
33
+ - Slot.constraints: single-slot constraints where 'self' refers to the slot filler
34
+ - Template.constraints: multi-slot constraints where slot names are variables
35
+ - TemplateSequence.constraints: cross-template constraints
36
+
37
+ Attributes
38
+ ----------
39
+ expression : str
40
+ DSL expression to evaluate (must return boolean).
41
+ context : dict[str, ContextValue]
42
+ Context variables available during evaluation (e.g., whitelists, constants).
43
+ description : str | None
44
+ Optional human-readable description of the constraint.
45
+ compiled : ASTNode | None
46
+ Cached compiled AST after first compilation (optimization).
47
+
48
+ Examples
49
+ --------
50
+ Extensional (whitelist):
51
+ >>> constraint = Constraint(
52
+ ... expression="self.lemma in motion_verbs",
53
+ ... context={"motion_verbs": {"walk", "run", "jump"}}
54
+ ... )
55
+
56
+ Intensional (feature-based):
57
+ >>> constraint = Constraint(
58
+ ... expression="self.pos == 'VERB' and self.features.number == 'singular'"
59
+ ... )
60
+
61
+ Binary agreement:
62
+ >>> constraint = Constraint(
63
+ ... expression="subject.features.number == verb.features.number"
64
+ ... )
65
+
66
+ IF-THEN conditional:
67
+ >>> constraint = Constraint(
68
+ ... expression="det.lemma != 'a' or noun.features.number == 'singular'"
69
+ ... )
70
+ """
71
+
72
+ expression: str
73
+ context: dict[str, ContextValue] = Field(default_factory=dict)
74
+ description: str | None = None
75
+ compiled: ASTNode | None = None
76
+
77
+ @classmethod
78
+ def combine(
79
+ cls,
80
+ *constraints: Constraint,
81
+ logic: str = "and",
82
+ ) -> Constraint:
83
+ """Combine multiple constraints with AND or OR logic.
84
+
85
+ Merges all context dictionaries from input constraints and combines
86
+ their expressions using the specified logical operator.
87
+
88
+ Parameters
89
+ ----------
90
+ *constraints : Constraint
91
+ Variable number of constraints to combine.
92
+ logic : str
93
+ Logical operator to use: "and" or "or" (default: "and").
94
+
95
+ Returns
96
+ -------
97
+ Constraint
98
+ New constraint with combined expressions and merged contexts.
99
+
100
+ Raises
101
+ ------
102
+ ValueError
103
+ If no constraints provided or invalid logic operator.
104
+
105
+ Examples
106
+ --------
107
+ >>> c1 = Constraint(
108
+ ... expression="self.pos == 'VERB'",
109
+ ... description="Must be a verb"
110
+ ... )
111
+ >>> c2 = Constraint(
112
+ ... expression="self.features.tense == 'present'",
113
+ ... description="Must be present tense"
114
+ ... )
115
+ >>> combined = Constraint.combine(c1, c2)
116
+ >>> "and" in combined.expression
117
+ True
118
+ >>> combined.description
119
+ 'Must be a verb; Must be present tense'
120
+
121
+ With OR logic and contexts:
122
+ >>> c1 = Constraint(
123
+ ... expression="self.lemma in verbs",
124
+ ... context={"verbs": {"walk", "run"}},
125
+ ... description="Motion verb"
126
+ ... )
127
+ >>> c2 = Constraint(
128
+ ... expression="self.lemma in actions",
129
+ ... context={"actions": {"jump", "hop"}},
130
+ ... description="Action verb"
131
+ ... )
132
+ >>> combined = Constraint.combine(c1, c2, logic="or")
133
+ >>> " or " in combined.expression
134
+ True
135
+ >>> len(combined.context)
136
+ 2
137
+ """
138
+ if not constraints:
139
+ raise ValueError("Must provide at least one constraint")
140
+
141
+ if logic not in ("and", "or"):
142
+ raise ValueError(f"Invalid logic operator '{logic}'. Must be 'and' or 'or'")
143
+
144
+ if len(constraints) == 1:
145
+ return constraints[0]
146
+
147
+ # combine expressions with specified logic operator
148
+ expressions = [f"({c.expression})" for c in constraints]
149
+ combined_expression = f" {logic} ".join(expressions)
150
+
151
+ # merge contexts
152
+ combined_context: dict[str, ContextValue] = {}
153
+ for constraint in constraints:
154
+ if constraint.context:
155
+ combined_context.update(constraint.context)
156
+
157
+ # combine descriptions
158
+ descriptions = [c.description for c in constraints if c.description]
159
+ combined_description = "; ".join(descriptions) if descriptions else None
160
+
161
+ return cls(
162
+ expression=combined_expression,
163
+ context=combined_context,
164
+ description=combined_description,
165
+ )