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/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
|