ripple-down-rules 0.2.3__tar.gz → 0.3.0__tar.gz

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 (35) hide show
  1. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/PKG-INFO +1 -1
  2. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/pyproject.toml +1 -1
  3. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules/datasets.py +66 -6
  4. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules/datastructures/callable_expression.py +13 -3
  5. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules/datastructures/case.py +33 -5
  6. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules/datastructures/dataclasses.py +53 -9
  7. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules/datastructures/enums.py +30 -1
  8. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules/experts.py +2 -1
  9. ripple_down_rules-0.3.0/src/ripple_down_rules/prompt.py +510 -0
  10. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules/rdr.py +7 -5
  11. ripple_down_rules-0.3.0/src/ripple_down_rules/rdr_decorators.py +139 -0
  12. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules/utils.py +162 -18
  13. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules.egg-info/PKG-INFO +1 -1
  14. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules.egg-info/SOURCES.txt +1 -0
  15. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/test/test_rdr_alchemy.py +6 -6
  16. ripple_down_rules-0.3.0/test/test_rdr_decorators.py +27 -0
  17. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/test/test_relational_rdr.py +6 -39
  18. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/test/test_relational_rdr_alchemy.py +15 -16
  19. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/test/test_sql_model.py +4 -4
  20. ripple_down_rules-0.2.3/src/ripple_down_rules/prompt.py +0 -354
  21. ripple_down_rules-0.2.3/src/ripple_down_rules/rdr_decorators.py +0 -55
  22. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/LICENSE +0 -0
  23. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/README.md +0 -0
  24. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/setup.cfg +0 -0
  25. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules/__init__.py +0 -0
  26. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules/datastructures/__init__.py +0 -0
  27. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules/failures.py +0 -0
  28. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules/helpers.py +0 -0
  29. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules/rules.py +0 -0
  30. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules.egg-info/dependency_links.txt +0 -0
  31. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/src/ripple_down_rules.egg-info/top_level.txt +0 -0
  32. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/test/test_json_serialization.py +0 -0
  33. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/test/test_on_mutagenic.py +0 -0
  34. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/test/test_rdr.py +0 -0
  35. {ripple_down_rules-0.2.3 → ripple_down_rules-0.3.0}/test/test_rdr_world.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripple_down_rules
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: Implements the various versions of Ripple Down Rules (RDR) for knowledge representation and reasoning.
5
5
  Author-email: Abdelrhman Bassiouny <abassiou@uni-bremen.de>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "ripple_down_rules"
9
- version = "0.2.3"
9
+ version = "0.3.0"
10
10
  description = "Implements the various versions of Ripple Down Rules (RDR) for knowledge representation and reasoning."
11
11
  readme = "README.md"
12
12
  authors = [{ name = "Abdelrhman Bassiouny", email = "abassiou@uni-bremen.de" }]
@@ -2,15 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import pickle
5
+ from dataclasses import dataclass, field
5
6
 
6
7
  import sqlalchemy
7
8
  from sqlalchemy import ForeignKey
8
- from sqlalchemy.orm import MappedAsDataclass, Mapped, mapped_column, relationship
9
- from typing_extensions import Tuple, List, Set, Optional
9
+ from sqlalchemy.orm import MappedAsDataclass, Mapped, mapped_column, relationship, MappedColumn
10
+ from typing_extensions import Tuple, List, Set, Optional, Self
10
11
  from ucimlrepo import fetch_ucirepo
11
12
 
12
13
  from .datastructures.case import Case, create_cases_from_dataframe
13
14
  from .datastructures.enums import Category
15
+ from .rdr_decorators import RDRDecorator
14
16
 
15
17
 
16
18
  def load_cached_dataset(cache_file):
@@ -106,8 +108,65 @@ class Habitat(Category):
106
108
  air = "air"
107
109
 
108
110
 
109
- # SpeciesCol = Column.create_from_enum(Species, mutually_exclusive=True)
110
- # HabitatCol = Column.create_from_enum(Habitat, mutually_exclusive=False)
111
+ class PhysicalObject:
112
+ """
113
+ A physical object is an object that can be contained in a container.
114
+ """
115
+ _rdr_json_dir: str = os.path.join(os.path.dirname(__file__), "../../test/test_results")
116
+ """
117
+ The directory where the RDR serialized JSON files are stored.
118
+ """
119
+ _rdr_python_dir: str = os.path.join(os.path.dirname(__file__), "../../test/test_generated_rdrs")
120
+ """
121
+ The directory where the RDR generated Python files are stored.
122
+ """
123
+ _is_a_robot_rdr: RDRDecorator = RDRDecorator(_rdr_json_dir, (bool,), True,
124
+ python_dir=_rdr_python_dir)
125
+ """
126
+ The RDR decorator that is used to determine if the object is a robot or not.
127
+ """
128
+ _select_parts_rdr: RDRDecorator = RDRDecorator(_rdr_json_dir, (Self,), False,
129
+ python_dir=_rdr_python_dir)
130
+ """
131
+ The RDR decorator that is used to determine if the object is a robot or not.
132
+ """
133
+
134
+ def __init__(self, name: str, contained_objects: Optional[List[PhysicalObject]] = None):
135
+ self.name: str = name
136
+ self._contained_objects: List[PhysicalObject] = contained_objects or []
137
+
138
+ @property
139
+ def contained_objects(self) -> List[PhysicalObject]:
140
+ return self._contained_objects
141
+
142
+ @contained_objects.setter
143
+ def contained_objects(self, value: List[PhysicalObject]):
144
+ self._contained_objects = value
145
+
146
+ @_is_a_robot_rdr.decorator
147
+ def is_a_robot(self) -> bool:
148
+ pass
149
+
150
+ @_select_parts_rdr.decorator
151
+ def select_objects_that_are_parts_of_robot(self, objects: List[PhysicalObject], robot: Robot) -> List[PhysicalObject]:
152
+ pass
153
+
154
+ def __str__(self):
155
+ return self.name
156
+
157
+ def __repr__(self):
158
+ return self.name
159
+
160
+
161
+ class Part(PhysicalObject):
162
+ ...
163
+
164
+
165
+ class Robot(PhysicalObject):
166
+
167
+ def __init__(self, name: str, parts: Optional[List[Part]] = None):
168
+ super().__init__(name)
169
+ self.parts: List[Part] = parts if parts else []
111
170
 
112
171
 
113
172
  class Base(sqlalchemy.orm.DeclarativeBase):
@@ -119,7 +178,7 @@ class HabitatTable(MappedAsDataclass, Base):
119
178
 
120
179
  id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
121
180
  habitat: Mapped[Habitat]
122
- animal_id = mapped_column(ForeignKey("Animal.id"), init=False)
181
+ animal_id: MappedColumn = mapped_column(ForeignKey("Animal.id"), init=False)
123
182
 
124
183
  def __hash__(self):
125
184
  return hash(self.habitat)
@@ -131,7 +190,7 @@ class HabitatTable(MappedAsDataclass, Base):
131
190
  return self.__str__()
132
191
 
133
192
 
134
- class Animal(MappedAsDataclass, Base):
193
+ class MappedAnimal(MappedAsDataclass, Base):
135
194
  __tablename__ = "Animal"
136
195
 
137
196
  id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
@@ -155,3 +214,4 @@ class Animal(MappedAsDataclass, Base):
155
214
  species: Mapped[Species] = mapped_column(nullable=True)
156
215
 
157
216
  habitats: Mapped[Set[HabitatTable]] = relationship(default_factory=set)
217
+
@@ -93,9 +93,12 @@ class CallableExpression(SubclassJSONSerializer):
93
93
  """
94
94
  encapsulating_function: str = "def _get_value(case):"
95
95
 
96
- def __init__(self, user_input: Optional[str] = None, conclusion_type: Optional[Tuple[Type]] = None,
96
+ def __init__(self, user_input: Optional[str] = None,
97
+ conclusion_type: Optional[Tuple[Type]] = None,
97
98
  expression_tree: Optional[AST] = None,
98
- scope: Optional[Dict[str, Any]] = None, conclusion: Optional[Any] = None):
99
+ scope: Optional[Dict[str, Any]] = None,
100
+ conclusion: Optional[Any] = None,
101
+ mutually_exclusive: bool = True):
99
102
  """
100
103
  Create a callable expression.
101
104
 
@@ -104,6 +107,8 @@ class CallableExpression(SubclassJSONSerializer):
104
107
  :param expression_tree: The AST tree parsed from the user input.
105
108
  :param scope: The scope to use for the callable expression.
106
109
  :param conclusion: The conclusion to use for the callable expression.
110
+ :param mutually_exclusive: If True, the conclusion is mutually exclusive, i.e. the callable expression can only
111
+ return one conclusion. If False, the callable expression can return multiple conclusions.
107
112
  """
108
113
  if user_input is None and conclusion is None:
109
114
  raise ValueError("Either user_input or conclusion must be provided.")
@@ -123,6 +128,7 @@ class CallableExpression(SubclassJSONSerializer):
123
128
  self.code = compile_expression_to_code(self.expression_tree)
124
129
  self.visitor = VariableVisitor()
125
130
  self.visitor.visit(self.expression_tree)
131
+ self.mutually_exclusive: bool = mutually_exclusive
126
132
 
127
133
  def __call__(self, case: Any, **kwargs) -> Any:
128
134
  try:
@@ -134,6 +140,8 @@ class CallableExpression(SubclassJSONSerializer):
134
140
  if output is None:
135
141
  output = scope['_get_value'](case)
136
142
  if self.conclusion_type is not None:
143
+ if self.mutually_exclusive and issubclass(type(output), (list, set)):
144
+ raise ValueError(f"Mutually exclusive types cannot be lists or sets, got {type(output)}")
137
145
  output_types = {type(o) for o in make_list(output)}
138
146
  output_types.add(type(output))
139
147
  if not are_results_subclass_of_types(output_types, self.conclusion_type):
@@ -227,6 +235,7 @@ class CallableExpression(SubclassJSONSerializer):
227
235
  "scope": {k: get_full_class_name(v) for k, v in self.scope.items()
228
236
  if hasattr(v, '__module__') and hasattr(v, '__name__')},
229
237
  "conclusion": conclusion_to_json(self.conclusion),
238
+ "mutually_exclusive": self.mutually_exclusive,
230
239
  }
231
240
 
232
241
  @classmethod
@@ -235,7 +244,8 @@ class CallableExpression(SubclassJSONSerializer):
235
244
  conclusion_type=tuple(get_type_from_string(t) for t in data["conclusion_type"])
236
245
  if data["conclusion_type"] else None,
237
246
  scope={k: get_type_from_string(v) for k, v in data["scope"].items()},
238
- conclusion=SubclassJSONSerializer.from_json(data["conclusion"]))
247
+ conclusion=SubclassJSONSerializer.from_json(data["conclusion"]),
248
+ mutually_exclusive=data["mutually_exclusive"])
239
249
 
240
250
 
241
251
  def compile_expression_to_code(expression_tree: AST) -> Any:
@@ -11,7 +11,7 @@ from sqlalchemy.orm import DeclarativeBase as SQLTable, MappedColumn as SQLColum
11
11
  from typing_extensions import Any, Optional, Dict, Type, Set, Hashable, Union, List, TYPE_CHECKING
12
12
 
13
13
  from ..utils import make_set, row_to_dict, table_rows_as_str, get_value_type_from_type_hint, SubclassJSONSerializer, \
14
- get_full_class_name, get_type_from_string, make_list, is_iterable, serialize_dataclass, dataclass_to_dict
14
+ get_full_class_name, get_type_from_string, make_list, is_iterable, serialize_dataclass, dataclass_to_dict, copy_case
15
15
 
16
16
  if TYPE_CHECKING:
17
17
  from ripple_down_rules.rules import Rule
@@ -65,7 +65,7 @@ class Case(UserDict, SubclassJSONSerializer):
65
65
  new_list.extend(make_list(value))
66
66
  super().__setitem__(name, new_list)
67
67
  else:
68
- super().__setitem__(name, self[name])
68
+ super().__setitem__(name, value)
69
69
  else:
70
70
  super().__setitem__(name, value)
71
71
  setattr(self, name, self[name])
@@ -102,6 +102,29 @@ class Case(UserDict, SubclassJSONSerializer):
102
102
  data[k] = SubclassJSONSerializer.from_json(v)
103
103
  return cls(_obj_type=obj_type, _id=id_, _name=name, **data)
104
104
 
105
+ def __deepcopy__(self, memo: Dict[Hashable, Any]) -> Case:
106
+ """
107
+ Create a deep copy of the case.
108
+
109
+ :param memo: A dictionary to keep track of objects that have already been copied.
110
+ :return: A deep copy of the case.
111
+ """
112
+ new_case = Case(self._obj_type, _id=self._id, _name=self._name, original_object=self._original_object)
113
+ for k, v in self.items():
114
+ new_case[k] = deepcopy(v)
115
+ return new_case
116
+
117
+ def __copy__(self) -> Case:
118
+ """
119
+ Create a shallow copy of the case.
120
+
121
+ :return: A shallow copy of the case.
122
+ """
123
+ new_case = Case(self._obj_type, _id=self._id, _name=self._name, original_object=self._original_object)
124
+ for k, v in self.items():
125
+ new_case[k] = copy(v)
126
+ return new_case
127
+
105
128
 
106
129
  @dataclass
107
130
  class CaseAttributeValue(SubclassJSONSerializer):
@@ -220,11 +243,16 @@ def create_case(obj: Any, recursion_idx: int = 0, max_recursion_idx: int = 0,
220
243
  return create_cases_from_dataframe(obj, obj_name)
221
244
  if isinstance(obj, Case) or (is_dataclass(obj) and not isinstance(obj, SQLTable)):
222
245
  return obj
223
- if ((recursion_idx > max_recursion_idx) or (obj.__class__.__module__ == "builtins")
246
+ if ((recursion_idx > max_recursion_idx)
247
+ or (obj.__class__.__module__ == "builtins" and not isinstance(obj, (list, set, dict)))
224
248
  or (obj.__class__ in [MetaData, registry])):
225
249
  return Case(type(obj), _id=id(obj), _name=obj_name, original_object=obj,
226
250
  **{obj_name or obj.__class__.__name__: make_list(obj) if parent_is_iterable else obj})
227
251
  case = Case(type(obj), _id=id(obj), _name=obj_name, original_object=obj)
252
+ if isinstance(obj, dict):
253
+ for k, v in obj.items():
254
+ case = create_or_update_case_from_attribute(v, k, obj, obj_name, recursion_idx,
255
+ max_recursion_idx, parent_is_iterable, case)
228
256
  for attr in dir(obj):
229
257
  if attr.startswith("_") or callable(getattr(obj, attr)):
230
258
  continue
@@ -322,9 +350,9 @@ def show_current_and_corner_cases(case: Any, targets: Optional[Dict[str, Any]] =
322
350
  if last_evaluated_rule and last_evaluated_rule.fired:
323
351
  corner_row_dict = dataclass_to_dict(last_evaluated_rule.corner_case)
324
352
  else:
325
- case_dict = case
353
+ case_dict = copy_case(case)
326
354
  if last_evaluated_rule and last_evaluated_rule.fired:
327
- corner_row_dict = corner_case
355
+ corner_row_dict = copy_case(corner_case)
328
356
 
329
357
  if corner_row_dict:
330
358
  corner_conclusion = last_evaluated_rule.conclusion(case)
@@ -1,14 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ import typing
4
5
  from dataclasses import dataclass, field
5
6
 
7
+ import typing_extensions
6
8
  from sqlalchemy.orm import DeclarativeBase as SQLTable
7
- from typing_extensions import Any, Optional, Dict, Type, Tuple, Union
9
+ from typing_extensions import Any, Optional, Dict, Type, Tuple, Union, List, get_origin, Set
8
10
 
9
11
  from .callable_expression import CallableExpression
10
12
  from .case import create_case, Case
11
- from ..utils import copy_case, make_list, make_set
13
+ from ..utils import copy_case, make_list, make_set, get_origin_and_args_from_type_hint, get_value_type_from_type_hint, \
14
+ typing_to_python_type
12
15
 
13
16
 
14
17
  @dataclass
@@ -60,6 +63,22 @@ class CaseQuery:
60
63
  """
61
64
  The conditions that must be satisfied for the target value to be valid.
62
65
  """
66
+ is_function: bool = False
67
+ """
68
+ Whether the case is a dict representing the arguments of an actual function or not,
69
+ most likely means it came from RDRDecorator, the the rdr takes function arguments and outputs the function output.
70
+ """
71
+ function_args_type_hints: Optional[Dict[str, Type]] = None
72
+ """
73
+ The type hints of the function arguments. This is used to recreate the function signature.
74
+ """
75
+
76
+ @property
77
+ def case_type(self) -> Type:
78
+ """
79
+ :return: The type of the case that the attribute belongs to.
80
+ """
81
+ return self.original_case._obj_type if isinstance(self.original_case, Case) else type(self.original_case)
63
82
 
64
83
  @property
65
84
  def case(self) -> Any:
@@ -83,22 +102,46 @@ class CaseQuery:
83
102
  raise ValueError("The case must be a Case or SQLTable object.")
84
103
  self._case = value
85
104
 
105
+ @property
106
+ def attribute_type_hint(self) -> str:
107
+ """
108
+ :return: The type hint of the attribute as a typing object.
109
+ """
110
+ if len(self.core_attribute_type) > 1:
111
+ attribute_types_str = f"Union[{', '.join([t.__name__ for t in self.core_attribute_type])}]"
112
+ else:
113
+ attribute_types_str = self.core_attribute_type[0].__name__
114
+ if list in self.attribute_type:
115
+ return f"List[{attribute_types_str}]"
116
+ else:
117
+ return attribute_types_str
118
+
86
119
  @property
87
120
  def core_attribute_type(self) -> Tuple[Type]:
88
121
  """
89
122
  :return: The core type of the attribute.
90
123
  """
91
- return (t for t in self.attribute_type if t not in (set, list))
124
+ return tuple(t for t in self.attribute_type if t not in (set, list))
92
125
 
93
126
  @property
94
127
  def attribute_type(self) -> Tuple[Type]:
95
128
  """
96
129
  :return: The type of the attribute.
97
130
  """
98
- if not self.mutually_exclusive and (set not in make_list(self._attribute_types)):
99
- self._attribute_types = tuple(set(make_list(self._attribute_types) + [set, list]))
100
- elif not isinstance(self._attribute_types, tuple):
101
- self._attribute_types = tuple(make_list(self._attribute_types))
131
+ if not isinstance(self._attribute_types, tuple):
132
+ self._attribute_types = tuple(make_set(self._attribute_types))
133
+ origin, args = get_origin_and_args_from_type_hint(self._attribute_types)
134
+ if origin is not None:
135
+ att_types = make_set(origin)
136
+ if origin in (list, set, tuple, List, Set, Union, Tuple):
137
+ att_types.update(make_set(args))
138
+ elif origin in (dict, Dict):
139
+ # ignore the key type
140
+ if args and len(args) > 1:
141
+ att_types.update(make_set(args[1]))
142
+ self._attribute_types = tuple(att_types)
143
+ if not self.mutually_exclusive and (list not in self._attribute_types):
144
+ self._attribute_types = tuple(make_list(self._attribute_types) + [set, list])
102
145
  return self._attribute_types
103
146
 
104
147
  @attribute_type.setter
@@ -129,7 +172,7 @@ class CaseQuery:
129
172
  """
130
173
  if (self._target is not None) and (not isinstance(self._target, CallableExpression)):
131
174
  self._target = CallableExpression(conclusion=self._target, conclusion_type=self.attribute_type,
132
- scope=self.scope)
175
+ scope=self.scope, mutually_exclusive=self.mutually_exclusive)
133
176
  return self._target
134
177
 
135
178
  @target.setter
@@ -173,4 +216,5 @@ class CaseQuery:
173
216
  return CaseQuery(self.original_case, self.attribute_name, self.attribute_type,
174
217
  self.mutually_exclusive, _target=self.target, default_value=self.default_value,
175
218
  scope=self.scope, _case=copy_case(self.case), _target_value=self.target_value,
176
- conditions=self.conditions)
219
+ conditions=self.conditions, is_function=self.is_function,
220
+ function_args_type_hints=self.function_args_type_hints)
@@ -2,11 +2,40 @@ from __future__ import annotations
2
2
 
3
3
  from enum import auto, Enum
4
4
 
5
- from typing_extensions import List, Dict, Any
5
+ from typing_extensions import List, Dict, Any, Type
6
6
 
7
7
  from ripple_down_rules.utils import SubclassJSONSerializer
8
8
 
9
9
 
10
+ class Editor(str, Enum):
11
+ """
12
+ The editor that is used to edit the rules.
13
+ """
14
+ Pycharm = "pycharm"
15
+ """
16
+ PyCharm editor.
17
+ """
18
+ Code = "code"
19
+ """
20
+ Visual Studio Code editor.
21
+ """
22
+ CodeServer = "code-server"
23
+ """
24
+ Visual Studio Code server editor.
25
+ """
26
+ @classmethod
27
+ def from_str(cls, editor: str) -> Editor:
28
+ """
29
+ Convert a string value to an Editor enum.
30
+
31
+ :param editor: The string that represents the editor name.
32
+ :return: The Editor enum.
33
+ """
34
+ if editor not in cls._value2member_map_:
35
+ raise ValueError(f"Editor {editor} is not supported.")
36
+ return cls._value2member_map_[editor]
37
+
38
+
10
39
  class Category(str, SubclassJSONSerializer, Enum):
11
40
 
12
41
  @classmethod
@@ -138,7 +138,8 @@ class Human(Expert):
138
138
  expert_input = self.all_expert_answers.pop(0)
139
139
  if expert_input is not None:
140
140
  expression = CallableExpression(expert_input, case_query.attribute_type,
141
- scope=case_query.scope)
141
+ scope=case_query.scope,
142
+ mutually_exclusive=case_query.mutually_exclusive)
142
143
  else:
143
144
  show_current_and_corner_cases(case_query.case)
144
145
  expert_input, expression = prompt_user_for_expression(case_query, PromptFor.Conclusion)