ripple-down-rules 0.6.28__py3-none-any.whl → 0.6.30__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.
- ripple_down_rules/__init__.py +1 -1
- ripple_down_rules/datastructures/callable_expression.py +4 -2
- ripple_down_rules/datastructures/case.py +12 -11
- ripple_down_rules/datastructures/dataclasses.py +87 -9
- ripple_down_rules/experts.py +137 -44
- ripple_down_rules/rdr.py +44 -29
- ripple_down_rules/rules.py +15 -1
- ripple_down_rules/user_interface/gui.py +59 -40
- ripple_down_rules/user_interface/ipython_custom_shell.py +36 -7
- ripple_down_rules/user_interface/prompt.py +41 -26
- ripple_down_rules/user_interface/template_file_creator.py +10 -8
- ripple_down_rules/utils.py +77 -15
- {ripple_down_rules-0.6.28.dist-info → ripple_down_rules-0.6.30.dist-info}/METADATA +1 -1
- ripple_down_rules-0.6.30.dist-info/RECORD +24 -0
- ripple_down_rules-0.6.28.dist-info/RECORD +0 -24
- {ripple_down_rules-0.6.28.dist-info → ripple_down_rules-0.6.30.dist-info}/WHEEL +0 -0
- {ripple_down_rules-0.6.28.dist-info → ripple_down_rules-0.6.30.dist-info}/licenses/LICENSE +0 -0
- {ripple_down_rules-0.6.28.dist-info → ripple_down_rules-0.6.30.dist-info}/top_level.txt +0 -0
ripple_down_rules/__init__.py
CHANGED
@@ -120,7 +120,9 @@ class CallableExpression(SubclassJSONSerializer):
|
|
120
120
|
self.user_defined_name = user_input.split('(')[0].replace('def ', '')
|
121
121
|
else:
|
122
122
|
self.user_defined_name = user_input
|
123
|
-
self.
|
123
|
+
if f"def {self.encapsulating_function_name}" not in user_input:
|
124
|
+
user_input = encapsulate_user_input(user_input, self.get_encapsulating_function())
|
125
|
+
self._user_input: str = user_input
|
124
126
|
if conclusion_type is not None:
|
125
127
|
if is_iterable(conclusion_type):
|
126
128
|
conclusion_type = tuple(conclusion_type)
|
@@ -308,7 +310,7 @@ def parse_string_to_expression(expression_str: str) -> AST:
|
|
308
310
|
:param expression_str: The string which will be parsed.
|
309
311
|
:return: The parsed expression.
|
310
312
|
"""
|
311
|
-
if not expression_str.startswith(CallableExpression.
|
313
|
+
if not expression_str.startswith(f"def {CallableExpression.encapsulating_function_name}"):
|
312
314
|
expression_str = encapsulate_user_input(expression_str, CallableExpression.get_encapsulating_function())
|
313
315
|
mode = 'exec' if expression_str.startswith('def') else 'eval'
|
314
316
|
tree = ast.parse(expression_str, mode=mode)
|
@@ -20,19 +20,20 @@ if TYPE_CHECKING:
|
|
20
20
|
|
21
21
|
class Case(UserDict, SubclassJSONSerializer):
|
22
22
|
"""
|
23
|
-
A collection of attributes that represents a set of
|
24
|
-
the names of the attributes and the values are the attributes. All are stored in lower case
|
23
|
+
A collection of attributes that represents a set of attributes of a case. This is a dictionary where the keys are
|
24
|
+
the names of the attributes and the values are the attributes. All are stored in lower case, and can be accessed
|
25
|
+
using the dot notation as well as the dictionary access notation.
|
25
26
|
"""
|
26
27
|
|
27
28
|
def __init__(self, _obj_type: Type, _id: Optional[Hashable] = None,
|
28
29
|
_name: Optional[str] = None, original_object: Optional[Any] = None, **kwargs):
|
29
30
|
"""
|
30
|
-
Create a new
|
31
|
+
Create a new case.
|
31
32
|
|
32
|
-
:param _obj_type: The type of the object that the
|
33
|
-
:param _id: The id of the
|
34
|
-
:param _name: The semantic name that describes the
|
35
|
-
:param kwargs: The attributes of the
|
33
|
+
:param _obj_type: The original type of the object that the case represents.
|
34
|
+
:param _id: The id of the case.
|
35
|
+
:param _name: The semantic name that describes the case.
|
36
|
+
:param kwargs: The attributes of the case.
|
36
37
|
"""
|
37
38
|
super().__init__(kwargs)
|
38
39
|
self._original_object = original_object
|
@@ -43,12 +44,12 @@ class Case(UserDict, SubclassJSONSerializer):
|
|
43
44
|
@classmethod
|
44
45
|
def from_obj(cls, obj: Any, obj_name: Optional[str] = None, max_recursion_idx: int = 3) -> Case:
|
45
46
|
"""
|
46
|
-
Create a
|
47
|
+
Create a case from an object.
|
47
48
|
|
48
|
-
:param obj: The object to create a
|
49
|
+
:param obj: The object to create a case from.
|
49
50
|
:param max_recursion_idx: The maximum recursion index to prevent infinite recursion.
|
50
51
|
:param obj_name: The name of the object.
|
51
|
-
:return: The
|
52
|
+
:return: The case that represents the object.
|
52
53
|
"""
|
53
54
|
return create_case(obj, max_recursion_idx=max_recursion_idx, obj_name=obj_name)
|
54
55
|
|
@@ -129,7 +130,7 @@ class Case(UserDict, SubclassJSONSerializer):
|
|
129
130
|
@dataclass
|
130
131
|
class CaseAttributeValue(SubclassJSONSerializer):
|
131
132
|
"""
|
132
|
-
|
133
|
+
Encapsulates a single value of a case attribute, it adds an id to the value.
|
133
134
|
"""
|
134
135
|
id: Hashable
|
135
136
|
"""
|
@@ -1,19 +1,22 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import inspect
|
4
|
-
import
|
4
|
+
import uuid
|
5
5
|
from dataclasses import dataclass, field
|
6
6
|
|
7
|
-
import
|
7
|
+
from colorama import Fore, Style
|
8
8
|
from omegaconf import MISSING
|
9
9
|
from sqlalchemy.orm import DeclarativeBase as SQLTable
|
10
|
-
from typing_extensions import Any, Optional, Dict, Type, Tuple, Union, List,
|
10
|
+
from typing_extensions import Any, Optional, Dict, Type, Tuple, Union, List, Set, Callable, TYPE_CHECKING
|
11
11
|
|
12
|
-
from ..utils import get_method_name, get_function_import_data, get_function_representation
|
13
12
|
from .callable_expression import CallableExpression
|
14
13
|
from .case import create_case, Case
|
15
|
-
from ..utils import copy_case, make_list, make_set, get_origin_and_args_from_type_hint,
|
16
|
-
|
14
|
+
from ..utils import copy_case, make_list, make_set, get_origin_and_args_from_type_hint, render_tree, \
|
15
|
+
get_function_representation
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from ..rdr import RippleDownRules
|
19
|
+
from ..rules import Rule
|
17
20
|
|
18
21
|
|
19
22
|
@dataclass
|
@@ -92,6 +95,38 @@ class CaseQuery:
|
|
92
95
|
"""
|
93
96
|
The type hints of the function arguments. This is used to recreate the function signature.
|
94
97
|
"""
|
98
|
+
rdr: Optional[RippleDownRules] = None
|
99
|
+
"""
|
100
|
+
The Ripple Down Rules that was used to answer the case query.
|
101
|
+
"""
|
102
|
+
|
103
|
+
def render_rule_tree(self, filepath: Optional[str] = None, view: bool = False):
|
104
|
+
if self.rdr is None:
|
105
|
+
return
|
106
|
+
render_tree(self.rdr.start_rule, use_dot_exporter=True, filename=filepath, view=view)
|
107
|
+
|
108
|
+
@property
|
109
|
+
def current_value_str(self):
|
110
|
+
return (f"{Fore.MAGENTA}Current value of {Fore.CYAN}{self.name}{Fore.MAGENTA} of type(s) "
|
111
|
+
f"{Fore.CYAN}({self.core_attribute_type_str}){Fore.MAGENTA}: "
|
112
|
+
f"{Fore.WHITE}{self.current_value}{Style.RESET_ALL}")
|
113
|
+
|
114
|
+
@property
|
115
|
+
def current_value(self) -> Any:
|
116
|
+
"""
|
117
|
+
:return: The current value of the attribute.
|
118
|
+
"""
|
119
|
+
if not hasattr(self.case, self.attribute_name):
|
120
|
+
return None
|
121
|
+
|
122
|
+
attr_value = getattr(self.case, self.attribute_name)
|
123
|
+
|
124
|
+
if attr_value is None:
|
125
|
+
return attr_value
|
126
|
+
elif self.mutually_exclusive:
|
127
|
+
return attr_value
|
128
|
+
else:
|
129
|
+
return list({v for v in make_list(attr_value) if isinstance(v, self.core_attribute_type)})
|
95
130
|
|
96
131
|
@property
|
97
132
|
def case_type(self) -> Type:
|
@@ -145,7 +180,14 @@ class CaseQuery:
|
|
145
180
|
return attribute_types_str
|
146
181
|
|
147
182
|
@property
|
148
|
-
def
|
183
|
+
def core_attribute_type_str(self) -> str:
|
184
|
+
"""
|
185
|
+
:return: The names of the core types of the attribute.
|
186
|
+
"""
|
187
|
+
return ','.join([t.__name__ for t in self.core_attribute_type])
|
188
|
+
|
189
|
+
@property
|
190
|
+
def core_attribute_type(self) -> Tuple[Type, ...]:
|
149
191
|
"""
|
150
192
|
:return: The core type of the attribute.
|
151
193
|
"""
|
@@ -247,7 +289,7 @@ class CaseQuery:
|
|
247
289
|
conditions=self.conditions, is_function=self.is_function,
|
248
290
|
function_args_type_hints=self.function_args_type_hints,
|
249
291
|
case_factory=self.case_factory, case_factory_idx=self.case_factory_idx,
|
250
|
-
case_conf=self.case_conf, scenario=self.scenario)
|
292
|
+
case_conf=self.case_conf, scenario=self.scenario, rdr=self.rdr)
|
251
293
|
|
252
294
|
|
253
295
|
@dataclass
|
@@ -284,4 +326,40 @@ class CaseFactoryMetaData:
|
|
284
326
|
f" scenario={scenario_repr})")
|
285
327
|
|
286
328
|
def __str__(self):
|
287
|
-
return self.__repr__()
|
329
|
+
return self.__repr__()
|
330
|
+
|
331
|
+
|
332
|
+
@dataclass
|
333
|
+
class RDRConclusion:
|
334
|
+
"""
|
335
|
+
This dataclass represents a conclusion of a Ripple Down Rule.
|
336
|
+
It contains the conclusion expression, the type of the conclusion, and the scope in which it is evaluated.
|
337
|
+
"""
|
338
|
+
value: Any
|
339
|
+
"""
|
340
|
+
The conclusion value.
|
341
|
+
"""
|
342
|
+
frozen_case: Any
|
343
|
+
"""
|
344
|
+
The frozen case that the conclusion was made for.
|
345
|
+
"""
|
346
|
+
rule: Rule
|
347
|
+
"""
|
348
|
+
The rule that gave this conclusion.
|
349
|
+
"""
|
350
|
+
rdr: RippleDownRules
|
351
|
+
"""
|
352
|
+
The Ripple Down Rules that classified the case and produced this conclusion.
|
353
|
+
"""
|
354
|
+
id: int = field(default_factory=lambda: uuid.uuid4().int)
|
355
|
+
"""
|
356
|
+
The unique identifier of the conclusion.
|
357
|
+
"""
|
358
|
+
|
359
|
+
def __hash__(self):
|
360
|
+
return hash(self.id)
|
361
|
+
|
362
|
+
def __eq__(self, other):
|
363
|
+
if not isinstance(other, RDRConclusion):
|
364
|
+
return False
|
365
|
+
return self.id == other.id
|
ripple_down_rules/experts.py
CHANGED
@@ -1,19 +1,23 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import ast
|
4
|
+
import importlib
|
4
5
|
import json
|
5
|
-
import logging
|
6
6
|
import os
|
7
|
-
import
|
7
|
+
import sys
|
8
8
|
from abc import ABC, abstractmethod
|
9
|
+
from dataclasses import is_dataclass
|
10
|
+
from textwrap import dedent, indent
|
11
|
+
from typing import Tuple, Dict
|
9
12
|
|
10
13
|
from typing_extensions import Optional, TYPE_CHECKING, List
|
11
14
|
|
12
15
|
from .datastructures.callable_expression import CallableExpression
|
13
|
-
from .datastructures.enums import PromptFor
|
14
|
-
from .datastructures.dataclasses import CaseQuery
|
15
16
|
from .datastructures.case import show_current_and_corner_cases
|
16
|
-
from .
|
17
|
+
from .datastructures.dataclasses import CaseQuery
|
18
|
+
from .datastructures.enums import PromptFor
|
19
|
+
from .user_interface.template_file_creator import TemplateFileCreator
|
20
|
+
from .utils import extract_imports, extract_function_source, get_imports_from_scope, get_class_file_path
|
17
21
|
|
18
22
|
try:
|
19
23
|
from .user_interface.gui import RDRCaseViewer
|
@@ -46,14 +50,13 @@ class Expert(ABC):
|
|
46
50
|
answers_save_path: Optional[str] = None):
|
47
51
|
self.all_expert_answers = []
|
48
52
|
self.use_loaded_answers = use_loaded_answers
|
49
|
-
self.append = append
|
50
53
|
self.answers_save_path = answers_save_path
|
51
54
|
if answers_save_path is not None and os.path.exists(answers_save_path + '.py'):
|
52
55
|
if use_loaded_answers:
|
53
56
|
self.load_answers(answers_save_path)
|
54
|
-
|
57
|
+
if not append:
|
55
58
|
os.remove(answers_save_path + '.py')
|
56
|
-
|
59
|
+
self.append = True
|
57
60
|
|
58
61
|
@abstractmethod
|
59
62
|
def ask_for_conditions(self, case_query: CaseQuery, last_evaluated_rule: Optional[Rule] = None) \
|
@@ -86,31 +89,29 @@ class Expert(ABC):
|
|
86
89
|
"""
|
87
90
|
if path is None and self.answers_save_path is None:
|
88
91
|
raise ValueError("No path provided to clear expert answers, either provide a path or set the "
|
89
|
-
|
92
|
+
"answers_save_path attribute.")
|
90
93
|
if path is None:
|
91
94
|
path = self.answers_save_path
|
92
|
-
if os.path.exists(path + '.json'):
|
93
|
-
os.remove(path + '.json')
|
94
95
|
if os.path.exists(path + '.py'):
|
95
96
|
os.remove(path + '.py')
|
96
97
|
self.all_expert_answers = []
|
97
98
|
|
98
|
-
def save_answers(self, path: Optional[str] = None):
|
99
|
+
def save_answers(self, path: Optional[str] = None, expert_answers: Optional[List[Tuple[Dict, str]]] = None):
|
99
100
|
"""
|
100
101
|
Save the expert answers to a file.
|
101
102
|
|
102
103
|
:param path: The path to save the answers to.
|
104
|
+
:param expert_answers: The expert answers to save.
|
103
105
|
"""
|
106
|
+
expert_answers = expert_answers if expert_answers else self.all_expert_answers
|
107
|
+
if not any(expert_answers):
|
108
|
+
return
|
104
109
|
if path is None and self.answers_save_path is None:
|
105
110
|
raise ValueError("No path provided to save expert answers, either provide a path or set the "
|
106
|
-
|
111
|
+
"answers_save_path attribute.")
|
107
112
|
if path is None:
|
108
113
|
path = self.answers_save_path
|
109
|
-
|
110
|
-
if is_json:
|
111
|
-
self._save_to_json(path)
|
112
|
-
else:
|
113
|
-
self._save_to_python(path)
|
114
|
+
self._save_to_python(path, expert_answers=expert_answers)
|
114
115
|
|
115
116
|
def _save_to_json(self, path: str):
|
116
117
|
"""
|
@@ -127,12 +128,14 @@ class Expert(ABC):
|
|
127
128
|
with open(path + '.json', "w") as f:
|
128
129
|
json.dump(all_answers, f)
|
129
130
|
|
130
|
-
def _save_to_python(self, path: str):
|
131
|
+
def _save_to_python(self, path: str, expert_answers: Optional[List[Tuple[Dict, str]]] = None):
|
131
132
|
"""
|
132
133
|
Save the expert answers to a Python file.
|
133
134
|
|
134
135
|
:param path: The path to save the answers to.
|
136
|
+
:param expert_answers: The expert answers to save.
|
135
137
|
"""
|
138
|
+
expert_answers = expert_answers if expert_answers else self.all_expert_answers
|
136
139
|
dir_name = os.path.dirname(path)
|
137
140
|
if not os.path.exists(dir_name + '/__init__.py'):
|
138
141
|
os.makedirs(dir_name, exist_ok=True)
|
@@ -145,18 +148,13 @@ class Expert(ABC):
|
|
145
148
|
current_file_data = f.read()
|
146
149
|
action = 'a' if self.append and current_file_data is not None else 'w'
|
147
150
|
with open(path + '.py', action) as f:
|
148
|
-
for scope, func_source in
|
151
|
+
for scope, func_source in expert_answers:
|
149
152
|
if len(scope) > 0:
|
150
153
|
imports = '\n'.join(get_imports_from_scope(scope)) + '\n\n\n'
|
151
154
|
else:
|
152
155
|
imports = ''
|
153
|
-
if func_source is
|
154
|
-
uid = uuid.uuid4().hex
|
155
|
-
func_source = encapsulate_user_input(func_source, CallableExpression.get_encapsulating_function(f'_{uid}'))
|
156
|
-
else:
|
156
|
+
if func_source is None:
|
157
157
|
func_source = 'pass # No user input provided for this case.\n'
|
158
|
-
if current_file_data is not None and func_source[1:] in current_file_data:
|
159
|
-
continue
|
160
158
|
f.write(imports + func_source + '\n' + '\n\n\n\'===New Answer===\'\n\n\n')
|
161
159
|
|
162
160
|
def load_answers(self, path: Optional[str] = None):
|
@@ -167,14 +165,13 @@ class Expert(ABC):
|
|
167
165
|
"""
|
168
166
|
if path is None and self.answers_save_path is None:
|
169
167
|
raise ValueError("No path provided to load expert answers from, either provide a path or set the "
|
170
|
-
|
168
|
+
"answers_save_path attribute.")
|
171
169
|
if path is None:
|
172
170
|
path = self.answers_save_path
|
173
|
-
|
174
|
-
if is_json:
|
175
|
-
self._load_answers_from_json(path)
|
176
|
-
elif os.path.exists(path + '.py'):
|
171
|
+
if os.path.exists(path + '.py'):
|
177
172
|
self._load_answers_from_python(path)
|
173
|
+
elif os.path.exists(path + '.json'):
|
174
|
+
self._load_answers_from_json(path)
|
178
175
|
|
179
176
|
def _load_answers_from_json(self, path: str):
|
180
177
|
"""
|
@@ -195,19 +192,94 @@ class Expert(ABC):
|
|
195
192
|
file_path = path + '.py'
|
196
193
|
with open(file_path, "r") as f:
|
197
194
|
all_answers = f.read().split('\n\n\n\'===New Answer===\'\n\n\n')[:-1]
|
198
|
-
all_function_sources =
|
199
|
-
all_function_sources_names = list(extract_function_source(file_path, []).keys())
|
195
|
+
all_function_sources = extract_function_source(file_path, [], as_list=True)
|
200
196
|
for i, answer in enumerate(all_answers):
|
201
197
|
answer = answer.strip('\n').strip()
|
202
198
|
if 'def ' not in answer and 'pass' in answer:
|
203
199
|
self.all_expert_answers.append(({}, None))
|
204
200
|
continue
|
205
201
|
scope = extract_imports(tree=ast.parse(answer))
|
206
|
-
|
202
|
+
func_name = all_function_sources[i].split('def ')[1].split('(')[0]
|
203
|
+
function_source = all_function_sources[i].replace(func_name,
|
207
204
|
CallableExpression.encapsulating_function_name)
|
208
205
|
self.all_expert_answers.append((scope, function_source))
|
209
206
|
|
210
207
|
|
208
|
+
class AI(Expert):
|
209
|
+
"""
|
210
|
+
The AI Expert class, an expert that uses AI to provide differentiating features and conclusions.
|
211
|
+
"""
|
212
|
+
|
213
|
+
def __init__(self, **kwargs):
|
214
|
+
"""
|
215
|
+
Initialize the AI expert.
|
216
|
+
"""
|
217
|
+
super().__init__(**kwargs)
|
218
|
+
self.user_prompt = UserPrompt()
|
219
|
+
|
220
|
+
def ask_for_conditions(self, case_query: CaseQuery,
|
221
|
+
last_evaluated_rule: Optional[Rule] = None) \
|
222
|
+
-> CallableExpression:
|
223
|
+
prompt_str = self.get_prompt_for_ai(case_query, PromptFor.Conditions)
|
224
|
+
print(prompt_str)
|
225
|
+
sys.exit()
|
226
|
+
|
227
|
+
def ask_for_conclusion(self, case_query: CaseQuery) -> Optional[CallableExpression]:
|
228
|
+
prompt_str = self.get_prompt_for_ai(case_query, PromptFor.Conclusion)
|
229
|
+
output_type_source = self.get_output_type_class_source(case_query)
|
230
|
+
prompt_str = f"\n\n\nOutput type(s) class source:\n{output_type_source}\n\n" + prompt_str
|
231
|
+
print(prompt_str)
|
232
|
+
sys.exit()
|
233
|
+
|
234
|
+
def get_output_type_class_source(self, case_query: CaseQuery) -> str:
|
235
|
+
"""
|
236
|
+
Get the output type class source for the AI expert.
|
237
|
+
|
238
|
+
:param case_query: The case query containing the case to classify.
|
239
|
+
:return: The output type class source.
|
240
|
+
"""
|
241
|
+
output_types = case_query.core_attribute_type
|
242
|
+
|
243
|
+
def get_class_source(cls):
|
244
|
+
cls_source_file = get_class_file_path(cls)
|
245
|
+
found_class_source = extract_function_source(cls_source_file, function_names=[cls.__name__],
|
246
|
+
is_class=True,
|
247
|
+
as_list=True)[0]
|
248
|
+
class_signature = found_class_source.split('\n')[0]
|
249
|
+
if '(' in class_signature:
|
250
|
+
parent_class_names = list(map(lambda x: x.strip(),
|
251
|
+
class_signature.split('(')[1].split(')')[0].split(',')))
|
252
|
+
parent_classes = [importlib.import_module(cls.__module__).__dict__.get(cls_name.strip())
|
253
|
+
for cls_name in parent_class_names]
|
254
|
+
else:
|
255
|
+
parent_classes = []
|
256
|
+
if is_dataclass(cls):
|
257
|
+
found_class_source = f"@dataclass\n{found_class_source}"
|
258
|
+
return '\n'.join([get_class_source(pcls) for pcls in parent_classes] + [found_class_source])
|
259
|
+
|
260
|
+
found_class_sources = []
|
261
|
+
for output_type in output_types:
|
262
|
+
found_class_sources.append(get_class_source(output_type))
|
263
|
+
found_class_sources = '\n\n\n'.join(found_class_sources)
|
264
|
+
return found_class_sources
|
265
|
+
|
266
|
+
def get_prompt_for_ai(self, case_query: CaseQuery, prompt_for: PromptFor) -> str:
|
267
|
+
"""
|
268
|
+
Get the prompt for the AI expert.
|
269
|
+
|
270
|
+
:param case_query: The case query containing the case to classify.
|
271
|
+
:param prompt_for: The type of prompt to get.
|
272
|
+
:return: The prompt for the AI expert.
|
273
|
+
"""
|
274
|
+
# data_to_show = show_current_and_corner_cases(case_query.case)
|
275
|
+
data_to_show = f"\nCase ({case_query.case_name}):\n {case_query.case.__dict__}"
|
276
|
+
template_file_creator = TemplateFileCreator(case_query, prompt_for=prompt_for)
|
277
|
+
boilerplate_code = template_file_creator.build_boilerplate_code()
|
278
|
+
initial_prompt_str = data_to_show + "\n\n" + boilerplate_code + "\n\n"
|
279
|
+
return self.user_prompt.build_prompt_str_for_ai(case_query, prompt_for=prompt_for,
|
280
|
+
initial_prompt_str=initial_prompt_str)
|
281
|
+
|
282
|
+
|
211
283
|
class Human(Expert):
|
212
284
|
"""
|
213
285
|
The Human Expert class, an expert that asks the human to provide differentiating features and conclusions.
|
@@ -225,8 +297,9 @@ class Human(Expert):
|
|
225
297
|
-> CallableExpression:
|
226
298
|
data_to_show = None
|
227
299
|
if (not self.use_loaded_answers or len(self.all_expert_answers) == 0) and self.user_prompt.viewer is None:
|
228
|
-
data_to_show = show_current_and_corner_cases(case_query.case,
|
229
|
-
|
300
|
+
data_to_show = show_current_and_corner_cases(case_query.case,
|
301
|
+
{case_query.attribute_name: case_query.target_value},
|
302
|
+
last_evaluated_rule=last_evaluated_rule)
|
230
303
|
return self._get_conditions(case_query, data_to_show)
|
231
304
|
|
232
305
|
def _get_conditions(self, case_query: CaseQuery, data_to_show: Optional[str] = None) \
|
@@ -249,10 +322,13 @@ class Human(Expert):
|
|
249
322
|
if user_input is not None:
|
250
323
|
case_query.scope.update(loaded_scope)
|
251
324
|
condition = CallableExpression(user_input, bool, scope=case_query.scope)
|
325
|
+
if self.answers_save_path is not None and not any(loaded_scope):
|
326
|
+
self.convert_json_answer_to_python_answer(case_query, user_input, condition, PromptFor.Conditions)
|
252
327
|
else:
|
253
|
-
user_input, condition = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conditions,
|
254
|
-
|
255
|
-
|
328
|
+
user_input, condition = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conditions,
|
329
|
+
prompt_str=data_to_show)
|
330
|
+
if user_input in ['exit', 'quit']:
|
331
|
+
sys.exit()
|
256
332
|
if not self.use_loaded_answers:
|
257
333
|
self.all_expert_answers.append((condition.scope, user_input))
|
258
334
|
if self.answers_save_path is not None:
|
@@ -260,6 +336,19 @@ class Human(Expert):
|
|
260
336
|
case_query.conditions = condition
|
261
337
|
return condition
|
262
338
|
|
339
|
+
def convert_json_answer_to_python_answer(self, case_query: CaseQuery, user_input: str,
|
340
|
+
callable_expression: CallableExpression,
|
341
|
+
prompt_for: PromptFor):
|
342
|
+
tfc = TemplateFileCreator(case_query, prompt_for=prompt_for)
|
343
|
+
code = tfc.build_boilerplate_code()
|
344
|
+
if user_input.startswith('def'):
|
345
|
+
user_input = '\n'.join(user_input.split('\n')[1:])
|
346
|
+
user_input = indent(dedent(user_input), " " * 4).strip()
|
347
|
+
code = code.replace('pass', user_input)
|
348
|
+
else:
|
349
|
+
code = code.replace('pass', f"return {user_input}")
|
350
|
+
self.save_answers(expert_answers=[({}, code)])
|
351
|
+
|
263
352
|
def ask_for_conclusion(self, case_query: CaseQuery) -> Optional[CallableExpression]:
|
264
353
|
"""
|
265
354
|
Ask the expert to provide a conclusion for the case.
|
@@ -279,20 +368,24 @@ class Human(Expert):
|
|
279
368
|
expression = CallableExpression(expert_input, case_query.attribute_type,
|
280
369
|
scope=case_query.scope,
|
281
370
|
mutually_exclusive=case_query.mutually_exclusive)
|
371
|
+
if self.answers_save_path is not None and not any(loaded_scope):
|
372
|
+
self.convert_json_answer_to_python_answer(case_query, expert_input, expression,
|
373
|
+
PromptFor.Conclusion)
|
282
374
|
except IndexError:
|
283
375
|
self.use_loaded_answers = False
|
284
376
|
if not self.use_loaded_answers:
|
285
377
|
data_to_show = None
|
286
378
|
if self.user_prompt.viewer is None:
|
287
379
|
data_to_show = show_current_and_corner_cases(case_query.case)
|
288
|
-
expert_input, expression = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conclusion,
|
380
|
+
expert_input, expression = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conclusion,
|
381
|
+
prompt_str=data_to_show)
|
289
382
|
if expert_input is None:
|
290
383
|
self.all_expert_answers.append(({}, None))
|
291
|
-
elif expert_input
|
384
|
+
elif expert_input not in ['exit', 'quit']:
|
292
385
|
self.all_expert_answers.append((expression.scope, expert_input))
|
293
|
-
if self.answers_save_path is not None and expert_input
|
386
|
+
if self.answers_save_path is not None and expert_input not in ['exit', 'quit']:
|
294
387
|
self.save_answers()
|
295
|
-
if expert_input
|
296
|
-
exit()
|
388
|
+
if expert_input in ['exit', 'quit']:
|
389
|
+
sys.exit()
|
297
390
|
case_query.target = expression
|
298
391
|
return expression
|