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
|
@@ -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
|
+
)
|