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
bead/dsl/evaluator.py ADDED
@@ -0,0 +1,570 @@
1
+ """Constraint evaluator for DSL.
2
+
3
+ This module provides the Evaluator class that executes AST nodes
4
+ against an evaluation context to produce boolean results, and the
5
+ DSLEvaluator class that provides a high-level interface for evaluating
6
+ constraint expressions.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from bead.dsl import ast
14
+ from bead.dsl.context import EvaluationContext
15
+ from bead.dsl.errors import EvaluationError
16
+ from bead.dsl.parser import parse
17
+ from bead.dsl.stdlib import register_stdlib
18
+
19
+ if TYPE_CHECKING:
20
+ from bead.items.item import Item
21
+ from bead.resources.constraints import ContextValue
22
+ from bead.resources.lexical_item import LexicalItem
23
+ from bead.templates.filler import FilledTemplate
24
+
25
+
26
+ class Evaluator:
27
+ """Evaluator for constraint AST nodes.
28
+
29
+ The evaluator walks the AST and computes values based on the
30
+ evaluation context. It supports:
31
+ - All AST node types
32
+ - Operator evaluation
33
+ - Function calls
34
+ - Attribute access
35
+ - Caching for performance
36
+
37
+ Parameters
38
+ ----------
39
+ use_cache : bool
40
+ Whether to cache evaluation results.
41
+
42
+ Examples
43
+ --------
44
+ >>> from bead.dsl.context import EvaluationContext
45
+ >>> from bead.dsl.parser import parse
46
+ >>> ctx = EvaluationContext()
47
+ >>> ctx.set_variable("x", 10)
48
+ >>> evaluator = Evaluator()
49
+ >>> node = parse("x > 5")
50
+ >>> evaluator.evaluate(node, ctx)
51
+ True
52
+ """
53
+
54
+ def __init__(self, use_cache: bool = True) -> None:
55
+ self._use_cache = use_cache
56
+ self._cache: dict[tuple[str, ...], Any] = {}
57
+
58
+ def evaluate(self, node: ast.ASTNode, context: EvaluationContext) -> Any:
59
+ """Evaluate an AST node in the given context.
60
+
61
+ Parameters
62
+ ----------
63
+ node : ast.ASTNode
64
+ AST node to evaluate.
65
+ context : EvaluationContext
66
+ Evaluation context with variables and functions.
67
+
68
+ Returns
69
+ -------
70
+ Any
71
+ Result of evaluation.
72
+
73
+ Raises
74
+ ------
75
+ EvaluationError
76
+ If evaluation fails (undefined variable, type error, etc.).
77
+ """
78
+ # dispatch to specific evaluation methods
79
+ if isinstance(node, ast.Literal):
80
+ return self._evaluate_literal(node, context)
81
+ elif isinstance(node, ast.Variable):
82
+ return self._evaluate_variable(node, context)
83
+ elif isinstance(node, ast.BinaryOp):
84
+ return self._evaluate_binary_op(node, context)
85
+ elif isinstance(node, ast.UnaryOp):
86
+ return self._evaluate_unary_op(node, context)
87
+ elif isinstance(node, ast.FunctionCall):
88
+ return self._evaluate_function_call(node, context)
89
+ elif isinstance(node, ast.AttributeAccess):
90
+ return self._evaluate_attribute_access(node, context)
91
+ elif isinstance(node, ast.Subscript):
92
+ return self._evaluate_subscript(node, context)
93
+ elif isinstance(node, ast.ListLiteral):
94
+ return self._evaluate_list_literal(node, context)
95
+ else:
96
+ raise EvaluationError(f"Unknown node type: {type(node).__name__}")
97
+
98
+ def _evaluate_literal(self, node: ast.Literal, context: EvaluationContext) -> Any:
99
+ """Evaluate literal node.
100
+
101
+ Parameters
102
+ ----------
103
+ node : ast.Literal
104
+ Literal node.
105
+ context : EvaluationContext
106
+ Evaluation context.
107
+
108
+ Returns
109
+ -------
110
+ Any
111
+ Literal value.
112
+ """
113
+ return node.value
114
+
115
+ def _evaluate_variable(self, node: ast.Variable, context: EvaluationContext) -> Any:
116
+ """Evaluate variable node.
117
+
118
+ Parameters
119
+ ----------
120
+ node : ast.Variable
121
+ Variable node.
122
+ context : EvaluationContext
123
+ Evaluation context.
124
+
125
+ Returns
126
+ -------
127
+ Any
128
+ Variable value from context.
129
+
130
+ Raises
131
+ ------
132
+ EvaluationError
133
+ If variable is not defined.
134
+ """
135
+ if not context.has_variable(node.name):
136
+ raise EvaluationError(f"Undefined variable: {node.name}")
137
+ return context.get_variable(node.name)
138
+
139
+ def _evaluate_binary_op(
140
+ self, node: ast.BinaryOp, context: EvaluationContext
141
+ ) -> Any:
142
+ """Evaluate binary operation node.
143
+
144
+ Parameters
145
+ ----------
146
+ node : ast.BinaryOp
147
+ Binary operation node.
148
+ context : EvaluationContext
149
+ Evaluation context.
150
+
151
+ Returns
152
+ -------
153
+ Any
154
+ Result of binary operation.
155
+
156
+ Raises
157
+ ------
158
+ EvaluationError
159
+ If operator is unknown or operation fails.
160
+ """
161
+ # short-circuit evaluation for logical operators
162
+ if node.operator == "and":
163
+ left = self.evaluate(node.left, context)
164
+ if not left:
165
+ return False
166
+ return bool(self.evaluate(node.right, context))
167
+ elif node.operator == "or":
168
+ left = self.evaluate(node.left, context)
169
+ if left:
170
+ return True
171
+ return bool(self.evaluate(node.right, context))
172
+
173
+ # evaluate both operands for other operators
174
+ left = self.evaluate(node.left, context)
175
+ right = self.evaluate(node.right, context)
176
+
177
+ try:
178
+ # comparison operators
179
+ if node.operator == "==":
180
+ return left == right
181
+ elif node.operator == "!=":
182
+ return left != right
183
+ elif node.operator == "<":
184
+ return left < right
185
+ elif node.operator == ">":
186
+ return left > right
187
+ elif node.operator == "<=":
188
+ return left <= right
189
+ elif node.operator == ">=":
190
+ return left >= right
191
+ # membership operators
192
+ elif node.operator == "in":
193
+ return left in right
194
+ elif node.operator == "not in":
195
+ return left not in right
196
+ # arithmetic operators
197
+ elif node.operator == "+":
198
+ return left + right
199
+ elif node.operator == "-":
200
+ return left - right
201
+ elif node.operator == "*":
202
+ return left * right
203
+ elif node.operator == "/":
204
+ if right == 0:
205
+ raise EvaluationError("Division by zero")
206
+ return left / right
207
+ elif node.operator == "%":
208
+ if right == 0:
209
+ raise EvaluationError("Modulo by zero")
210
+ return left % right
211
+ else:
212
+ raise EvaluationError(f"Unknown operator: {node.operator}")
213
+ except TypeError as e:
214
+ raise EvaluationError(
215
+ f"Type error in operation '{node.operator}': "
216
+ f"cannot operate on {type(left).__name__} and {type(right).__name__}"
217
+ ) from e
218
+ except ZeroDivisionError as e:
219
+ raise EvaluationError("Division by zero") from e
220
+
221
+ def _evaluate_unary_op(self, node: ast.UnaryOp, context: EvaluationContext) -> Any:
222
+ """Evaluate unary operation node.
223
+
224
+ Parameters
225
+ ----------
226
+ node : ast.UnaryOp
227
+ Unary operation node.
228
+ context : EvaluationContext
229
+ Evaluation context.
230
+
231
+ Returns
232
+ -------
233
+ Any
234
+ Result of unary operation.
235
+
236
+ Raises
237
+ ------
238
+ EvaluationError
239
+ If operator is unknown or operation fails.
240
+ """
241
+ operand = self.evaluate(node.operand, context)
242
+
243
+ try:
244
+ if node.operator == "not":
245
+ return not operand
246
+ elif node.operator == "-":
247
+ return -operand
248
+ elif node.operator == "+":
249
+ return +operand
250
+ else:
251
+ raise EvaluationError(f"Unknown unary operator: {node.operator}")
252
+ except TypeError as e:
253
+ raise EvaluationError(
254
+ f"Type error in unary operation '{node.operator}': "
255
+ f"cannot operate on {type(operand).__name__}"
256
+ ) from e
257
+
258
+ def _evaluate_function_call(
259
+ self, node: ast.FunctionCall, context: EvaluationContext
260
+ ) -> Any:
261
+ """Evaluate function call node.
262
+
263
+ Parameters
264
+ ----------
265
+ node : ast.FunctionCall
266
+ Function call node.
267
+ context : EvaluationContext
268
+ Evaluation context.
269
+
270
+ Returns
271
+ -------
272
+ Any
273
+ Function return value.
274
+
275
+ Raises
276
+ ------
277
+ EvaluationError
278
+ If function is not defined or call fails.
279
+ """
280
+ # evaluate arguments
281
+ args = [self.evaluate(arg, context) for arg in node.arguments]
282
+
283
+ # handle method calls (e.g., subject.features.get(...))
284
+ if isinstance(node.function, ast.AttributeAccess):
285
+ # evaluate the object
286
+ obj = self.evaluate(node.function.object, context)
287
+ # get the method
288
+ method_name = node.function.attribute
289
+ try:
290
+ method = getattr(obj, method_name)
291
+ return method(*args)
292
+ except AttributeError as e:
293
+ raise EvaluationError(
294
+ f"Object of type {type(obj).__name__} has no method: {method_name}"
295
+ ) from e
296
+ except TypeError as e:
297
+ raise EvaluationError(f"Error calling method {method_name}: {e}") from e
298
+
299
+ # handle regular function calls (e.g., len(...))
300
+ if isinstance(node.function, ast.Variable):
301
+ func_name = node.function.name
302
+ return context.call_function(func_name, args)
303
+
304
+ func_type = type(node.function).__name__
305
+ raise EvaluationError(
306
+ f"Function must be a variable or attribute access, got {func_type}"
307
+ )
308
+
309
+ def _evaluate_attribute_access(
310
+ self, node: ast.AttributeAccess, context: EvaluationContext
311
+ ) -> Any:
312
+ """Evaluate attribute access node.
313
+
314
+ Parameters
315
+ ----------
316
+ node : ast.AttributeAccess
317
+ Attribute access node.
318
+ context : EvaluationContext
319
+ Evaluation context.
320
+
321
+ Returns
322
+ -------
323
+ Any
324
+ Attribute value.
325
+
326
+ Raises
327
+ ------
328
+ EvaluationError
329
+ If attribute access fails.
330
+ """
331
+ obj = self.evaluate(node.object, context)
332
+
333
+ # try dictionary-style access first
334
+ if isinstance(obj, dict):
335
+ if node.attribute not in obj:
336
+ raise EvaluationError(f"Dictionary does not have key: {node.attribute}")
337
+ return obj[node.attribute] # type: ignore[reportUnknownVariableType]
338
+
339
+ # try attribute access
340
+ try:
341
+ return getattr(obj, node.attribute)
342
+ except AttributeError as e:
343
+ raise EvaluationError(
344
+ f"Object of type {type(obj).__name__} has no attribute: "
345
+ f"{node.attribute}"
346
+ ) from e
347
+
348
+ def _evaluate_subscript(
349
+ self, node: ast.Subscript, context: EvaluationContext
350
+ ) -> Any:
351
+ """Evaluate subscript access node.
352
+
353
+ Parameters
354
+ ----------
355
+ node : ast.Subscript
356
+ Subscript access node.
357
+ context : EvaluationContext
358
+ Evaluation context.
359
+
360
+ Returns
361
+ -------
362
+ Any
363
+ Subscripted value.
364
+
365
+ Raises
366
+ ------
367
+ EvaluationError
368
+ If subscript access fails.
369
+ """
370
+ obj = self.evaluate(node.object, context)
371
+ index = self.evaluate(node.index, context)
372
+
373
+ try:
374
+ return obj[index] # type: ignore[reportUnknownVariableType]
375
+ except (KeyError, IndexError, TypeError) as e:
376
+ obj_type = type(obj).__name__
377
+ raise EvaluationError(
378
+ f"Subscript access failed on {obj_type} with index {index}: {e}"
379
+ ) from e
380
+
381
+ def _evaluate_list_literal(
382
+ self, node: ast.ListLiteral, context: EvaluationContext
383
+ ) -> list[Any]:
384
+ """Evaluate list literal node.
385
+
386
+ Parameters
387
+ ----------
388
+ node : ast.ListLiteral
389
+ List literal node.
390
+ context : EvaluationContext
391
+ Evaluation context.
392
+
393
+ Returns
394
+ -------
395
+ list[Any]
396
+ Evaluated list elements.
397
+ """
398
+ return [self.evaluate(element, context) for element in node.elements]
399
+
400
+ def clear_cache(self) -> None:
401
+ """Clear evaluation cache.
402
+
403
+ Examples
404
+ --------
405
+ >>> evaluator = Evaluator()
406
+ >>> evaluator.clear_cache()
407
+ """
408
+ self._cache.clear()
409
+
410
+
411
+ class DSLEvaluator:
412
+ """High-level evaluator for DSL constraint expressions.
413
+
414
+ This class provides a simplified interface for evaluating constraint
415
+ expressions. It handles:
416
+
417
+ - Parsing expression strings to AST
418
+ - Building evaluation contexts from dictionaries
419
+ - Caching compiled ASTs
420
+ - Registering standard library functions
421
+ - Property extraction for list partitioning
422
+
423
+ The DSLEvaluator is the primary interface for constraint evaluation
424
+ in the bead package. It wraps the lower-level Evaluator class.
425
+
426
+ Attributes
427
+ ----------
428
+ evaluator : Evaluator
429
+ The underlying AST evaluator instance.
430
+ compiled_cache : dict[str, ast.ASTNode]
431
+ Cache mapping expression strings to their compiled AST nodes.
432
+
433
+ Examples
434
+ --------
435
+ >>> from bead.resources.items import LexicalItem
436
+ >>> evaluator = DSLEvaluator()
437
+ >>> item = LexicalItem(lemma="walk", pos="VERB")
438
+ >>> evaluator.evaluate(
439
+ ... "self.pos == 'VERB'",
440
+ ... {"self": item}
441
+ ... )
442
+ True
443
+ >>> evaluator.evaluate(
444
+ ... "self.lemma in motion_verbs",
445
+ ... {"self": item, "motion_verbs": {"walk", "run", "jump"}}
446
+ ... )
447
+ True
448
+ """
449
+
450
+ def __init__(self) -> None:
451
+ self.evaluator = Evaluator(use_cache=True)
452
+ self.compiled_cache: dict[str, ast.ASTNode] = {}
453
+
454
+ def evaluate(
455
+ self,
456
+ expression: str,
457
+ context: dict[str, ContextValue | LexicalItem | FilledTemplate | Item],
458
+ ) -> bool | str | int | float | list[Any]:
459
+ """Evaluate DSL expression with given context.
460
+
461
+ Parameters
462
+ ----------
463
+ expression : str
464
+ DSL expression to evaluate.
465
+ context : dict[str, ContextValue | LexicalItem | FilledTemplate | Item]
466
+ Variables available during evaluation. Can include:
467
+ - ContextValue: primitive values, lists, sets
468
+ - LexicalItem: lexical items for single-slot constraints
469
+ - FilledTemplate: filled templates for multi-slot constraints
470
+ - Item: items for list partitioning
471
+
472
+ Returns
473
+ -------
474
+ bool | str | int | float | list[Any]
475
+ Result of evaluation.
476
+
477
+ Raises
478
+ ------
479
+ EvaluationError
480
+ If evaluation fails (parse error, undefined variable, etc.).
481
+
482
+ Examples
483
+ --------
484
+ >>> evaluator = DSLEvaluator()
485
+ >>> evaluator.evaluate("x > 5", {"x": 10})
486
+ True
487
+ >>> evaluator.evaluate(
488
+ ... "subject.lemma == verb.lemma",
489
+ ... {"subject": item1, "verb": item2}
490
+ ... )
491
+ False
492
+ """
493
+ # get or compile AST
494
+ if expression in self.compiled_cache:
495
+ ast_node = self.compiled_cache[expression]
496
+ else:
497
+ ast_node = parse(expression)
498
+ self.compiled_cache[expression] = ast_node
499
+
500
+ # build evaluation context
501
+ eval_context = EvaluationContext()
502
+ register_stdlib(eval_context)
503
+
504
+ # add context variables
505
+ for name, value in context.items():
506
+ eval_context.set_variable(name, value)
507
+
508
+ # evaluate
509
+ return self.evaluator.evaluate(ast_node, eval_context)
510
+
511
+ def extract_property_value(
512
+ self,
513
+ obj: Any,
514
+ property_expression: str,
515
+ context: dict[str, ContextValue] | None = None,
516
+ ) -> Any:
517
+ """Extract property value using DSL expression.
518
+
519
+ This method is used by ListPartitioner to extract property values
520
+ from items using DSL expressions. The property_expression is evaluated
521
+ with the object available as 'item' in the context.
522
+
523
+ Parameters
524
+ ----------
525
+ obj : Any
526
+ Object to extract property from (typically a LexicalItem or Item).
527
+ property_expression : str
528
+ DSL expression that accesses object properties (e.g., "item.lemma",
529
+ "item.features.number", "len(item.lemma)").
530
+ context : dict[str, ContextValue] | None
531
+ Additional context variables (e.g., constants, helper data).
532
+
533
+ Returns
534
+ -------
535
+ Any
536
+ Extracted property value.
537
+
538
+ Raises
539
+ ------
540
+ EvaluationError
541
+ If property extraction fails.
542
+
543
+ Examples
544
+ --------
545
+ >>> evaluator = DSLEvaluator()
546
+ >>> item = LexicalItem(lemma="walk", pos="VERB")
547
+ >>> evaluator.extract_property_value(item, "item.lemma")
548
+ 'walk'
549
+ >>> evaluator.extract_property_value(item, "len(item.lemma)")
550
+ 4
551
+ """
552
+ eval_context_dict: dict[str, Any] = {"item": obj}
553
+ if context:
554
+ eval_context_dict.update(context)
555
+
556
+ return self.evaluate(property_expression, eval_context_dict)
557
+
558
+ def clear_cache(self) -> None:
559
+ """Clear compiled AST cache.
560
+
561
+ This should be called if you want to free memory or if expression
562
+ strings might have changed meaning.
563
+
564
+ Examples
565
+ --------
566
+ >>> evaluator = DSLEvaluator()
567
+ >>> evaluator.clear_cache()
568
+ """
569
+ self.compiled_cache.clear()
570
+ self.evaluator.clear_cache()
bead/dsl/grammar.lark ADDED
@@ -0,0 +1,81 @@
1
+ // Constraint DSL Grammar
2
+ // This grammar defines the syntax for constraint expressions used in templates
3
+
4
+ ?start: expr
5
+
6
+ // Expressions (with precedence)
7
+ ?expr: or_expr
8
+
9
+ ?or_expr: and_expr
10
+ | or_expr "or" and_expr -> binary_op
11
+
12
+ ?and_expr: not_expr
13
+ | and_expr "and" not_expr -> binary_op
14
+
15
+ ?not_expr: comparison
16
+ | "not" not_expr -> unary_op
17
+
18
+ ?comparison: arithmetic
19
+ | comparison "==" arithmetic -> binary_op
20
+ | comparison "!=" arithmetic -> binary_op
21
+ | comparison "<" arithmetic -> binary_op
22
+ | comparison ">" arithmetic -> binary_op
23
+ | comparison "<=" arithmetic -> binary_op
24
+ | comparison ">=" arithmetic -> binary_op
25
+ | comparison "in" arithmetic -> binary_op
26
+ | comparison "not" "in" arithmetic -> binary_op_not_in
27
+
28
+ ?arithmetic: term
29
+ | arithmetic "+" term -> binary_op
30
+ | arithmetic "-" term -> binary_op
31
+
32
+ ?term: factor
33
+ | term "*" factor -> binary_op
34
+ | term "/" factor -> binary_op
35
+ | term "%" factor -> binary_op
36
+
37
+ ?factor: atom
38
+ | "-" factor -> unary_op
39
+ | "+" factor -> unary_op
40
+
41
+ ?atom: literal
42
+ | variable
43
+ | attribute_access
44
+ | subscript
45
+ | function_call
46
+ | list_literal
47
+ | "(" expr ")"
48
+
49
+ // Literals
50
+ literal: STRING -> string_literal
51
+ | NUMBER -> number_literal
52
+ | "true" -> true_literal
53
+ | "false" -> false_literal
54
+
55
+ // Variables and identifiers
56
+ variable: NAME
57
+
58
+ attribute_access: atom "." NAME
59
+
60
+ subscript: atom "[" expr "]"
61
+
62
+ function_call: atom "(" [arguments] ")"
63
+
64
+ arguments: expr ("," expr)*
65
+
66
+ list_literal: "[" [list_elements] "]"
67
+
68
+ list_elements: expr ("," expr)*
69
+
70
+ // Terminals
71
+ STRING: /"[^"]*"/ | /'[^']*'/
72
+ NUMBER: /\d+\.?\d*/
73
+ NAME: /[a-zA-Z_][a-zA-Z0-9_]*/
74
+
75
+ // Whitespace
76
+ %import common.WS
77
+ %ignore WS
78
+
79
+ // Comments
80
+ COMMENT: /#[^\n]*/
81
+ %ignore COMMENT