user-simulator 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.
- user_sim/__init__.py +0 -0
- user_sim/cli/__init__.py +0 -0
- user_sim/cli/gen_user_profile.py +34 -0
- user_sim/cli/init_project.py +65 -0
- user_sim/cli/sensei_chat.py +481 -0
- user_sim/cli/sensei_check.py +103 -0
- user_sim/cli/validation_check.py +143 -0
- user_sim/core/__init__.py +0 -0
- user_sim/core/ask_about.py +665 -0
- user_sim/core/data_extraction.py +260 -0
- user_sim/core/data_gathering.py +134 -0
- user_sim/core/interaction_styles.py +147 -0
- user_sim/core/role_structure.py +608 -0
- user_sim/core/user_simulator.py +302 -0
- user_sim/handlers/__init__.py +0 -0
- user_sim/handlers/asr_module.py +128 -0
- user_sim/handlers/html_parser_module.py +202 -0
- user_sim/handlers/image_recognition_module.py +139 -0
- user_sim/handlers/pdf_parser_module.py +123 -0
- user_sim/utils/__init__.py +0 -0
- user_sim/utils/config.py +47 -0
- user_sim/utils/cost_tracker.py +153 -0
- user_sim/utils/cost_tracker_v2.py +193 -0
- user_sim/utils/errors.py +15 -0
- user_sim/utils/exceptions.py +47 -0
- user_sim/utils/languages.py +78 -0
- user_sim/utils/register_management.py +62 -0
- user_sim/utils/show_logs.py +63 -0
- user_sim/utils/token_cost_calculator.py +338 -0
- user_sim/utils/url_management.py +60 -0
- user_sim/utils/utilities.py +568 -0
- user_simulator-0.1.0.dist-info/METADATA +733 -0
- user_simulator-0.1.0.dist-info/RECORD +37 -0
- user_simulator-0.1.0.dist-info/WHEEL +5 -0
- user_simulator-0.1.0.dist-info/entry_points.txt +6 -0
- user_simulator-0.1.0.dist-info/licenses/LICENSE.txt +21 -0
- user_simulator-0.1.0.dist-info/top_level.txt +1 -0
user_sim/__init__.py
ADDED
File without changes
|
user_sim/cli/__init__.py
ADDED
File without changes
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
from argparse import ArgumentParser
|
4
|
+
from technologies.taskyto import ChatbotSpecificationTaskyto
|
5
|
+
|
6
|
+
|
7
|
+
def generate(technology: str, chatbot: str, user: str):
|
8
|
+
if technology == 'taskyto':
|
9
|
+
chatbot_spec = ChatbotSpecificationTaskyto()
|
10
|
+
else:
|
11
|
+
raise Exception(f"Technology {technology} is not supported.")
|
12
|
+
|
13
|
+
user_profile = chatbot_spec.build_user_profile(chatbot_folder=chatbot)
|
14
|
+
chatbot_name = os.path.basename(chatbot)
|
15
|
+
test_name = f"{chatbot_name}_test"
|
16
|
+
user_profile.test_name = test_name
|
17
|
+
if user:
|
18
|
+
user_profile_name = f"{user}{os.sep}{chatbot_name}_user_profile.yaml"
|
19
|
+
else:
|
20
|
+
user_profile_name = f"{chatbot}{os.sep}{chatbot_name}_user_profile.yaml"
|
21
|
+
print(f"The following user profile has been created: {user_profile_name}")
|
22
|
+
user_profile.to_yaml(user_profile_name)
|
23
|
+
|
24
|
+
def main():
|
25
|
+
parser = ArgumentParser(description='User profile generator from a chatbot specification')
|
26
|
+
parser.add_argument('--technology', required=True, choices=['taskyto'], help='Technology the chatbot is implemented in')
|
27
|
+
parser.add_argument('--chatbot', required=True, help='Folder that contains the chatbot specification')
|
28
|
+
parser.add_argument('--user', required=False, help='Folder to store the user profile (the chatbot folder if none is given)')
|
29
|
+
args = parser.parse_args()
|
30
|
+
|
31
|
+
generate(args.technology, args.chatbot, args.user)
|
32
|
+
|
33
|
+
if __name__ == '__main__':
|
34
|
+
main()
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import os
|
2
|
+
from argparse import ArgumentParser
|
3
|
+
|
4
|
+
def generate_untitled_name(path):
|
5
|
+
i = 1
|
6
|
+
while True:
|
7
|
+
name = f"Untitled_{i}"
|
8
|
+
full_path = os.path.join(path, name)
|
9
|
+
if not os.path.exists(full_path):
|
10
|
+
return name
|
11
|
+
i += 1
|
12
|
+
|
13
|
+
def init_proj(project_name, path):
|
14
|
+
project_path = os.path.join(path, project_name)
|
15
|
+
os.makedirs(project_path, exist_ok=True)
|
16
|
+
|
17
|
+
run_yml_content = f"""\
|
18
|
+
project_folder: {project_name}
|
19
|
+
|
20
|
+
user_profile:
|
21
|
+
technology:
|
22
|
+
connector:
|
23
|
+
connector_parameters:
|
24
|
+
extract:
|
25
|
+
#execution_parameters:
|
26
|
+
# - verbose
|
27
|
+
# - clean_cache
|
28
|
+
# - update_cache
|
29
|
+
# - ignore_cache
|
30
|
+
"""
|
31
|
+
project_path = os.path.join(path, project_name)
|
32
|
+
|
33
|
+
folder_list = ["profiles", "rules", "types", "personalities"]
|
34
|
+
for folder in folder_list:
|
35
|
+
folder_path = os.path.join(project_path, folder)
|
36
|
+
os.makedirs(folder_path)
|
37
|
+
with open(f'{folder_path}/PlaceDataHere.txt', 'w') as f:
|
38
|
+
pass
|
39
|
+
|
40
|
+
run_yml_path = os.path.join(project_path, "run.yml")
|
41
|
+
if not os.path.exists(run_yml_path):
|
42
|
+
with open(run_yml_path, "w") as archivo:
|
43
|
+
archivo.write(run_yml_content)
|
44
|
+
|
45
|
+
return project_path
|
46
|
+
|
47
|
+
def main():
|
48
|
+
parser = ArgumentParser(description='Conversation generator for a chatbot')
|
49
|
+
parser.add_argument('--path', default='.',
|
50
|
+
help='Directory where the project will be created (default: current directory).')
|
51
|
+
parser.add_argument('--name', help='Name of the project (optional).')
|
52
|
+
args = parser.parse_args()
|
53
|
+
|
54
|
+
base_path = os.path.abspath(args.path)
|
55
|
+
|
56
|
+
if not args.name:
|
57
|
+
project_name = generate_untitled_name(base_path)
|
58
|
+
else:
|
59
|
+
project_name = args.name
|
60
|
+
|
61
|
+
final_path = init_proj(project_name, base_path)
|
62
|
+
print(f"--- Project created at: '{final_path}' ---")
|
63
|
+
|
64
|
+
if __name__ == "__main__":
|
65
|
+
main()
|
@@ -0,0 +1,481 @@
|
|
1
|
+
import timeit
|
2
|
+
import yaml
|
3
|
+
import pandas as pd
|
4
|
+
from collections import Counter
|
5
|
+
from argparse import ArgumentParser
|
6
|
+
from colorama import Fore, Style
|
7
|
+
from technologies.chatbot_connectors import (Chatbot, ChatbotRasa, ChatbotTaskyto, ChatbotMillionBot,
|
8
|
+
ChatbotServiceform)
|
9
|
+
from user_sim.core.data_extraction import DataExtraction
|
10
|
+
from user_sim.core.role_structure import *
|
11
|
+
from user_sim.core.user_simulator import UserSimulator
|
12
|
+
from user_sim.utils.show_logs import *
|
13
|
+
from user_sim.utils.utilities import *
|
14
|
+
from user_sim.utils.token_cost_calculator import create_cost_dataset
|
15
|
+
from user_sim.utils.register_management import clean_temp_files
|
16
|
+
|
17
|
+
check_keys(["OPENAI_API_KEY"])
|
18
|
+
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
19
|
+
root_path = os.path.abspath(os.path.join(current_script_dir, ".."))
|
20
|
+
|
21
|
+
def print_user(msg):
|
22
|
+
clean_text = re.sub(r'\(Web page content: [^)]*>>\)', '', msg)
|
23
|
+
clean_text = re.sub(r'\(PDF content: [^)]*>>\)', '', clean_text)
|
24
|
+
clean_text = re.sub(r'\(Image description[^)]*\)', '', clean_text)
|
25
|
+
print(f"{Fore.GREEN}User:{Style.RESET_ALL} {clean_text}")
|
26
|
+
|
27
|
+
|
28
|
+
def print_chatbot(msg):
|
29
|
+
clean_text = re.sub(r'\(Web page content:.*?\>\>\)', '', msg, flags=re.DOTALL)
|
30
|
+
clean_text = re.sub(r'\(PDF content:.*?\>\>\)', '', clean_text, flags=re.DOTALL)
|
31
|
+
clean_text = re.sub(r'\(Image description[^)]*\)', '', clean_text)
|
32
|
+
print(f"{Fore.LIGHTRED_EX}Chatbot:{Style.RESET_ALL} {clean_text}")
|
33
|
+
|
34
|
+
def load_yaml_arguments(project_path):
|
35
|
+
files = os.listdir(project_path)
|
36
|
+
|
37
|
+
run_file = next((f for f in files if f in ["run.yml", "run.yaml"]), None)
|
38
|
+
|
39
|
+
if not run_file:
|
40
|
+
raise FileNotFoundError(f"Couldn't find run.yml file.")
|
41
|
+
|
42
|
+
run_yaml_path = os.path.join(project_path, run_file)
|
43
|
+
|
44
|
+
with open(run_yaml_path, 'r', encoding='utf-8') as f:
|
45
|
+
yaml_args = yaml.safe_load(f)
|
46
|
+
|
47
|
+
if yaml_args:
|
48
|
+
if "execution_parameters" in yaml_args.keys():
|
49
|
+
parameters = yaml_args["execution_parameters"]
|
50
|
+
dict_parameters = {param: True for param in parameters}
|
51
|
+
del yaml_args["execution_parameters"]
|
52
|
+
yaml_args.update(dict_parameters)
|
53
|
+
|
54
|
+
yaml_args["project_path"] = project_path
|
55
|
+
|
56
|
+
return yaml_args or {}
|
57
|
+
|
58
|
+
|
59
|
+
def load_yaml_files_from_folder(folder_path, existing_keys=None):
|
60
|
+
types = {}
|
61
|
+
for filename in os.listdir(folder_path):
|
62
|
+
if filename.endswith((".yml", ".yaml")):
|
63
|
+
file_path = os.path.join(folder_path, filename)
|
64
|
+
try:
|
65
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
66
|
+
data = yaml.safe_load(f)
|
67
|
+
name = data.get("name")
|
68
|
+
if name:
|
69
|
+
if not existing_keys or name not in existing_keys:
|
70
|
+
types[name] = data
|
71
|
+
except yaml.YAMLError as e:
|
72
|
+
logger.error(f"Error reading {file_path}: {e}")
|
73
|
+
return types
|
74
|
+
|
75
|
+
|
76
|
+
def configure_project(project_path):
|
77
|
+
config.project_folder_path = project_path
|
78
|
+
config.profiles_path = os.path.join(project_path, "profiles")
|
79
|
+
config.custom_personalities_folder = os.path.join(project_path, "personalities")
|
80
|
+
|
81
|
+
custom_types_path = os.path.join(project_path, "types")
|
82
|
+
default_types_path = os.path.join(config.root_path, "config", "types")
|
83
|
+
|
84
|
+
custom_types = load_yaml_files_from_folder(custom_types_path)
|
85
|
+
default_types = load_yaml_files_from_folder(default_types_path, existing_keys=custom_types.keys())
|
86
|
+
config.types_dict = {**default_types, **custom_types}
|
87
|
+
|
88
|
+
def configure_connector(*args):
|
89
|
+
|
90
|
+
connec = args[0]
|
91
|
+
with open(connec, 'r', encoding='utf-8') as f:
|
92
|
+
con_yaml = yaml.safe_load(f)
|
93
|
+
|
94
|
+
|
95
|
+
if len(args)<2 or not con_yaml["parameters"]:
|
96
|
+
logger.warning("No parameters added for connector configuration. They may not have been set as input arguments "
|
97
|
+
"or declared as dynamic parameters in the connector file.")
|
98
|
+
return con_yaml
|
99
|
+
|
100
|
+
parameters = args[1]
|
101
|
+
if isinstance(parameters, str):
|
102
|
+
parameters = json.loads(parameters)
|
103
|
+
|
104
|
+
param_key_list = list(parameters.keys())
|
105
|
+
if Counter(con_yaml["parameters"]) != Counter(param_key_list):
|
106
|
+
raise UnmachedList("Parameters in yaml don't match parameters input in execution")
|
107
|
+
|
108
|
+
def replace_values(obj_dict, src_dict):
|
109
|
+
for key in obj_dict:
|
110
|
+
if isinstance(obj_dict[key], dict):
|
111
|
+
replace_values(obj_dict[key], src_dict)
|
112
|
+
elif key in src_dict:
|
113
|
+
obj_dict[key] = src_dict[key]
|
114
|
+
|
115
|
+
replace_values(con_yaml, parameters)
|
116
|
+
return con_yaml
|
117
|
+
|
118
|
+
|
119
|
+
def get_conversation_metadata(user_profile, the_user, serial=None):
|
120
|
+
def conversation_metadata(up):
|
121
|
+
interaction_style_list = []
|
122
|
+
conversation_list = []
|
123
|
+
|
124
|
+
for inter in up.interaction_styles:
|
125
|
+
interaction_style_list.append(inter.get_metadata())
|
126
|
+
|
127
|
+
conversation_list.append({'interaction_style': interaction_style_list})
|
128
|
+
|
129
|
+
if isinstance(up.yaml['conversation']['number'], int):
|
130
|
+
conversation_list.append({'number': up.yaml['conversation']['number']})
|
131
|
+
else:
|
132
|
+
conversation_list.append({'number': up.conversation_number})
|
133
|
+
|
134
|
+
if 'random steps' in up.yaml['conversation']['goal_style']:
|
135
|
+
conversation_list.append({'goal_style': {'steps': up.goal_style[1]}})
|
136
|
+
else:
|
137
|
+
conversation_list.append(up.yaml['conversation']['goal_style'])
|
138
|
+
|
139
|
+
return conversation_list
|
140
|
+
|
141
|
+
def ask_about_metadata(up):
|
142
|
+
if not up.ask_about.variable_list:
|
143
|
+
return up.ask_about.str_list
|
144
|
+
|
145
|
+
if user_profile.ask_about.picked_elements:
|
146
|
+
user_profile.ask_about.picked_elements = [
|
147
|
+
{clave: (valor[0] if isinstance(valor, list) and len(valor) == 1 else valor)
|
148
|
+
for clave, valor in dic.items()}
|
149
|
+
for dic in user_profile.ask_about.picked_elements
|
150
|
+
]
|
151
|
+
|
152
|
+
return user_profile.ask_about.str_list + user_profile.ask_about.picked_elements
|
153
|
+
|
154
|
+
def data_output_extraction(u_profile, user):
|
155
|
+
output_list = u_profile.output
|
156
|
+
data_list = []
|
157
|
+
for output in output_list:
|
158
|
+
var_name = list(output.keys())[0]
|
159
|
+
var_dict = output.get(var_name)
|
160
|
+
my_data_extract = DataExtraction(user.conversation_history,
|
161
|
+
var_name,
|
162
|
+
var_dict["type"],
|
163
|
+
var_dict["description"])
|
164
|
+
data_list.append(my_data_extract.get_data_extraction())
|
165
|
+
|
166
|
+
data_dict = {k: v for dic in data_list for k, v in dic.items()}
|
167
|
+
has_none = any(value is None for value in data_dict.values())
|
168
|
+
if has_none:
|
169
|
+
count_none = sum(1 for value in data_dict.values() if value is None)
|
170
|
+
config.errors.append({1001: f"{count_none} goals left to complete."})
|
171
|
+
|
172
|
+
return data_list
|
173
|
+
|
174
|
+
def total_cost_calculator():
|
175
|
+
encoding = get_encoding(config.cost_ds_path)["encoding"]
|
176
|
+
cost_df = pd.read_csv(config.cost_ds_path, encoding=encoding)
|
177
|
+
|
178
|
+
total_sum_cost = cost_df[cost_df["Conversation"]==config.conversation_name]['Total Cost'].sum()
|
179
|
+
total_sum_cost = round(float(total_sum_cost), 8)
|
180
|
+
|
181
|
+
return total_sum_cost
|
182
|
+
|
183
|
+
|
184
|
+
data_output = {'data_output': data_output_extraction(user_profile, the_user)}
|
185
|
+
context = {'context': user_profile.raw_context}
|
186
|
+
ask_about = {'ask_about': ask_about_metadata(user_profile)}
|
187
|
+
conversation = {'conversation': conversation_metadata(user_profile)}
|
188
|
+
language = {'language': user_profile.language}
|
189
|
+
serial_dict = {'serial': serial}
|
190
|
+
errors_dict = {'errors': config.errors}
|
191
|
+
total_cost = {'total_cost($)': total_cost_calculator()}
|
192
|
+
metadata = {**serial_dict,
|
193
|
+
**language,
|
194
|
+
**context,
|
195
|
+
**ask_about,
|
196
|
+
**conversation,
|
197
|
+
**data_output,
|
198
|
+
**errors_dict,
|
199
|
+
**total_cost
|
200
|
+
}
|
201
|
+
|
202
|
+
return metadata
|
203
|
+
|
204
|
+
|
205
|
+
def parse_profiles(user_path):
|
206
|
+
def is_yaml(file):
|
207
|
+
if not file.endswith(('.yaml', '.yml')):
|
208
|
+
return False
|
209
|
+
try:
|
210
|
+
with open(file, 'r') as f:
|
211
|
+
yaml.safe_load(f)
|
212
|
+
return True
|
213
|
+
except yaml.YAMLError:
|
214
|
+
return False
|
215
|
+
|
216
|
+
list_of_files = []
|
217
|
+
if os.path.isfile(user_path):
|
218
|
+
if is_yaml(user_path):
|
219
|
+
yaml_file = read_yaml(user_path)
|
220
|
+
return [yaml_file]
|
221
|
+
else:
|
222
|
+
raise Exception(f'The user profile file is not a yaml: {user_path}')
|
223
|
+
elif os.path.isdir(user_path):
|
224
|
+
for root, _, files in os.walk(user_path):
|
225
|
+
for file in files:
|
226
|
+
if is_yaml(os.path.join(root, file)):
|
227
|
+
path = root + '/' + file
|
228
|
+
yaml_file = read_yaml(path)
|
229
|
+
list_of_files.append(yaml_file)
|
230
|
+
|
231
|
+
return list_of_files
|
232
|
+
else:
|
233
|
+
raise Exception(f'Invalid path for user profile operation: {user_path}')
|
234
|
+
|
235
|
+
|
236
|
+
def build_chatbot(technology, connector) -> Chatbot:
|
237
|
+
chatbot_builder = {
|
238
|
+
'rasa': ChatbotRasa,
|
239
|
+
'taskyto': ChatbotTaskyto,
|
240
|
+
'serviceform': ChatbotServiceform,
|
241
|
+
'millionbot': ChatbotMillionBot
|
242
|
+
}
|
243
|
+
chatbot_class = chatbot_builder.get(technology, Chatbot)
|
244
|
+
return chatbot_class(connector)
|
245
|
+
|
246
|
+
|
247
|
+
|
248
|
+
def generate_conversation(technology, connector, user,
|
249
|
+
personality, extract, project_folder):
|
250
|
+
profiles = parse_profiles(user)
|
251
|
+
serial = generate_serial()
|
252
|
+
config.serial = serial
|
253
|
+
create_cost_dataset(serial, extract)
|
254
|
+
my_execution_stat = ExecutionStats(extract, serial)
|
255
|
+
the_chatbot = build_chatbot(technology, connector)
|
256
|
+
|
257
|
+
|
258
|
+
for profile in profiles:
|
259
|
+
user_profile = RoleData(profile, project_folder, personality)
|
260
|
+
test_name = user_profile.test_name
|
261
|
+
config.test_name = test_name
|
262
|
+
chat_format = user_profile.format_type
|
263
|
+
start_time_test = timeit.default_timer()
|
264
|
+
|
265
|
+
for i in range(user_profile.conversation_number):
|
266
|
+
config.conversation_name = f'{i}_{test_name}_{serial}.yml'
|
267
|
+
the_chatbot.fallback = user_profile.fallback
|
268
|
+
the_user = UserSimulator(user_profile, the_chatbot)
|
269
|
+
bot_starter = user_profile.is_starter
|
270
|
+
response_time = []
|
271
|
+
|
272
|
+
start_time_conversation = timeit.default_timer()
|
273
|
+
response = ''
|
274
|
+
|
275
|
+
if chat_format == "speech":
|
276
|
+
from user_sim.handlers.asr_module import STTModule
|
277
|
+
|
278
|
+
stt = STTModule(user_profile.format_config)
|
279
|
+
|
280
|
+
def send_user_message(user_msg):
|
281
|
+
print_user(user_msg)
|
282
|
+
stt.say(user_msg)
|
283
|
+
|
284
|
+
def get_chatbot_response(user_msg):
|
285
|
+
start_response_time = timeit.default_timer()
|
286
|
+
is_ok, response = stt.hear()
|
287
|
+
end_response_time = timeit.default_timer()
|
288
|
+
time_sec = timedelta(seconds=end_response_time - start_response_time).total_seconds()
|
289
|
+
response_time.append(time_sec)
|
290
|
+
return is_ok, response
|
291
|
+
|
292
|
+
def get_chatbot_starter_response():
|
293
|
+
is_ok, response = stt.hear()
|
294
|
+
return is_ok, response
|
295
|
+
|
296
|
+
else:
|
297
|
+
|
298
|
+
if user_profile.format_config:
|
299
|
+
logger.warning("Chat format is text, but an SR configuration was provided. This configuration will"
|
300
|
+
" be ignored.")
|
301
|
+
|
302
|
+
def send_user_message(user_msg):
|
303
|
+
print_user(user_msg)
|
304
|
+
|
305
|
+
def get_chatbot_response(user_msg):
|
306
|
+
start_response_time = timeit.default_timer()
|
307
|
+
is_ok, response = the_chatbot.execute_with_input(user_msg)
|
308
|
+
end_response_time = timeit.default_timer()
|
309
|
+
time_sec = timedelta(seconds=end_response_time - start_response_time).total_seconds()
|
310
|
+
response_time.append(time_sec)
|
311
|
+
return is_ok, response
|
312
|
+
|
313
|
+
def get_chatbot_starter_response():
|
314
|
+
is_ok, response = the_chatbot.execute_starter_chatbot()
|
315
|
+
return is_ok, response
|
316
|
+
|
317
|
+
start_loop = True
|
318
|
+
if bot_starter:
|
319
|
+
is_ok, response = get_chatbot_starter_response()
|
320
|
+
if not is_ok:
|
321
|
+
if response is not None:
|
322
|
+
the_user.update_history("Assistant", "Error: " + response)
|
323
|
+
else:
|
324
|
+
the_user.update_history("Assistant", "Error: The server did not respond.")
|
325
|
+
start_loop = False
|
326
|
+
print_chatbot(response)
|
327
|
+
user_msg = the_user.open_conversation()
|
328
|
+
if user_msg == "exit":
|
329
|
+
start_loop = False
|
330
|
+
|
331
|
+
else:
|
332
|
+
user_msg = the_user.open_conversation()
|
333
|
+
if user_msg == "exit":
|
334
|
+
start_loop = False
|
335
|
+
else:
|
336
|
+
send_user_message(user_msg)
|
337
|
+
is_ok, response = get_chatbot_response(user_msg)
|
338
|
+
if not is_ok:
|
339
|
+
if response is not None:
|
340
|
+
the_user.update_history("Assistant", "Error: " + response)
|
341
|
+
else:
|
342
|
+
the_user.update_history("Assistant", "Error: The server did not respond.")
|
343
|
+
start_loop = False
|
344
|
+
else:
|
345
|
+
print_chatbot(response)
|
346
|
+
|
347
|
+
if start_loop:
|
348
|
+
while True:
|
349
|
+
user_msg = the_user.get_response(response)
|
350
|
+
if user_msg == "exit":
|
351
|
+
break
|
352
|
+
send_user_message(user_msg)
|
353
|
+
is_ok, response = get_chatbot_response(user_msg)
|
354
|
+
if response == 'timeout':
|
355
|
+
break
|
356
|
+
print_chatbot(response)
|
357
|
+
if not is_ok:
|
358
|
+
if response is not None:
|
359
|
+
the_user.update_history("Assistant", "Error: " + response)
|
360
|
+
else:
|
361
|
+
the_user.update_history("Assistant", "Error: The server did not respond.")
|
362
|
+
break
|
363
|
+
|
364
|
+
if extract:
|
365
|
+
end_time_conversation = timeit.default_timer()
|
366
|
+
conversation_time = end_time_conversation - start_time_conversation
|
367
|
+
formatted_time_conv = timedelta(seconds=conversation_time).total_seconds()
|
368
|
+
print(f"Conversation Time: {formatted_time_conv} (s)")
|
369
|
+
|
370
|
+
history = the_user.conversation_history
|
371
|
+
metadata = get_conversation_metadata(user_profile, the_user, serial)
|
372
|
+
dg_dataframe = the_user.data_gathering.gathering_register
|
373
|
+
csv_extraction = the_user.goal_style[1] if the_user.goal_style[0] == 'all_answered' else False
|
374
|
+
answer_validation_data = (dg_dataframe, csv_extraction)
|
375
|
+
save_test_conv(history, metadata, test_name, extract, serial,
|
376
|
+
formatted_time_conv, response_time, answer_validation_data, counter=i)
|
377
|
+
|
378
|
+
config.total_individual_cost = 0
|
379
|
+
user_profile.reset_attributes()
|
380
|
+
|
381
|
+
if hasattr(the_chatbot, 'id'):
|
382
|
+
the_chatbot.id = None
|
383
|
+
|
384
|
+
end_time_test = timeit.default_timer()
|
385
|
+
execution_time = end_time_test - start_time_test
|
386
|
+
formatted_time = timedelta(seconds=execution_time).total_seconds()
|
387
|
+
print(f"Execution Time: {formatted_time} (s)")
|
388
|
+
print('------------------------------')
|
389
|
+
|
390
|
+
if user_profile.conversation_number > 0:
|
391
|
+
my_execution_stat.add_test_name(test_name)
|
392
|
+
my_execution_stat.show_last_stats()
|
393
|
+
|
394
|
+
if config.clean_cache:
|
395
|
+
clean_temp_files()
|
396
|
+
|
397
|
+
if extract and len(my_execution_stat.test_names) == len(profiles):
|
398
|
+
my_execution_stat.show_global_stats()
|
399
|
+
my_execution_stat.export_stats()
|
400
|
+
elif extract:
|
401
|
+
logger.warning("Stats export was enabled but couldn't retrieve all stats. No stats will be exported.")
|
402
|
+
else:
|
403
|
+
pass
|
404
|
+
|
405
|
+
end_alarm()
|
406
|
+
|
407
|
+
def main():
|
408
|
+
parser = ArgumentParser(description='Conversation generator for a chatbot')
|
409
|
+
|
410
|
+
parser.add_argument('--run_from_yaml', type=str, help='Carga los argumentos desde un archivo YAML')
|
411
|
+
|
412
|
+
parser.add_argument('--technology', required=False,
|
413
|
+
choices=['rasa', 'taskyto', 'ada-uam', 'millionbot', 'genion', 'lola', 'serviceform', 'kuki', 'julie', 'rivas_catalina', 'saic_malaga'],
|
414
|
+
help='Technology the chatbot is implemented in')
|
415
|
+
# parser.add_argument('--chatbot', required=False, help='URL where the chatbot is deployed')
|
416
|
+
parser.add_argument('--connector', required=False, help='path to the connector configuration file')
|
417
|
+
parser.add_argument('--connector_parameters', required=False, help='dynamic parameters for the selected chatbot connector')
|
418
|
+
parser.add_argument('--project_path', required=False, help='Project folder PATH where all testing data is stored')
|
419
|
+
parser.add_argument('--user_profile', required=False, help='User profile file or user profile folder to test the chatbot')
|
420
|
+
parser.add_argument('--personality', required=False, help='Personality file')
|
421
|
+
parser.add_argument("--extract", default=False, help='Path to store conversation user-chatbot')
|
422
|
+
parser.add_argument('--verbose', action='store_true', help='Shows debug prints')
|
423
|
+
parser.add_argument('--clean_cache', action='store_true', help='Deletes temporary files.')
|
424
|
+
parser.add_argument('--ignore_cache', action='store_true', help='Ignores cache for temporary files')
|
425
|
+
parser.add_argument('--update_cache', action='store_true', help='Overwrites temporary files in cache')
|
426
|
+
parser_args, unknown_args = parser.parse_known_args()
|
427
|
+
|
428
|
+
if parser_args.run_from_yaml:
|
429
|
+
if len(sys.argv) > 3: # sys.argv[0] is script, sys.argv[1] is --run_from_yaml, sys.argv[2] is YAML
|
430
|
+
parser.error("No other arguments can be provided when using --run_from_yaml.")
|
431
|
+
|
432
|
+
yaml_args = load_yaml_arguments(parser_args.run_from_yaml)
|
433
|
+
|
434
|
+
default_flags = {
|
435
|
+
"connector_parameters": None,
|
436
|
+
"personality": None,
|
437
|
+
"verbose": False,
|
438
|
+
"clean_cache": False,
|
439
|
+
"ignore_cache": False,
|
440
|
+
"update_cache": False
|
441
|
+
}
|
442
|
+
for flag, default in default_flags.items():
|
443
|
+
yaml_args.setdefault(flag, default)
|
444
|
+
|
445
|
+
class ArgsNamespace:
|
446
|
+
def __init__(self, **entries):
|
447
|
+
self.__dict__.update(entries)
|
448
|
+
|
449
|
+
parser_args = ArgsNamespace(**yaml_args)
|
450
|
+
|
451
|
+
else:
|
452
|
+
required_args = ['technology', 'user_profile', 'connector']
|
453
|
+
missing_args = [arg for arg in required_args if getattr(parser_args, arg) is None]
|
454
|
+
|
455
|
+
if missing_args:
|
456
|
+
parser.error(f"The following arguments are required when not using --run_from_yaml: {', '.join(missing_args)}")
|
457
|
+
|
458
|
+
configure_project(parser_args.project_path)
|
459
|
+
config.root_path = os.path.abspath(os.path.join(current_script_dir, "../../.."))
|
460
|
+
profile_path = os.path.join(config.profiles_path, parser_args.user_profile)
|
461
|
+
|
462
|
+
logger = create_logger(parser_args.verbose, 'Info Logger')
|
463
|
+
logger.info('Logs enabled!')
|
464
|
+
|
465
|
+
check_keys(["OPENAI_API_KEY"])
|
466
|
+
config.test_cases_folder = parser_args.extract
|
467
|
+
config.ignore_cache = parser_args.ignore_cache
|
468
|
+
config.update_cache = parser_args.update_cache
|
469
|
+
config.clean_cache = parser_args.clean_cache
|
470
|
+
|
471
|
+
if parser_args.connector_parameters:
|
472
|
+
connector = configure_connector(parser_args.connector, parser_args.connector_parameters)
|
473
|
+
else:
|
474
|
+
connector = configure_connector(parser_args.connector)
|
475
|
+
|
476
|
+
generate_conversation(parser_args.technology, connector, profile_path,
|
477
|
+
parser_args.personality, parser_args.extract, parser_args.project_path)
|
478
|
+
|
479
|
+
|
480
|
+
if __name__ == '__main__':
|
481
|
+
main()
|
@@ -0,0 +1,103 @@
|
|
1
|
+
import glob
|
2
|
+
|
3
|
+
import yaml
|
4
|
+
import sys
|
5
|
+
|
6
|
+
from pydantic import ValidationError
|
7
|
+
from argparse import ArgumentParser
|
8
|
+
|
9
|
+
from metamorphic.results import Result
|
10
|
+
from metamorphic.rules import *
|
11
|
+
from metamorphic.tests import Test
|
12
|
+
from user_sim.utils.utilities import check_keys
|
13
|
+
import user_sim.utils.errors as errors
|
14
|
+
|
15
|
+
def __get_object_from_yaml_files(file_or_dir, operation, name):
|
16
|
+
objects = []
|
17
|
+
if os.path.isfile(file_or_dir):
|
18
|
+
yaml_files = [file_or_dir]
|
19
|
+
else:
|
20
|
+
yaml_files = (glob.glob(os.path.join(file_or_dir, '**/*.yaml'), recursive=True) +
|
21
|
+
glob.glob(os.path.join(file_or_dir, '**/*.yml'), recursive=True))
|
22
|
+
|
23
|
+
for file_path in yaml_files:
|
24
|
+
if "__report__" in file_path:
|
25
|
+
continue
|
26
|
+
|
27
|
+
with open(file_path, 'r', encoding='utf-8') as file:
|
28
|
+
if name=='rule':
|
29
|
+
yaml_data = yaml.safe_load(file.read())
|
30
|
+
else:
|
31
|
+
yaml_data = yaml.safe_load_all(file.read())
|
32
|
+
try:
|
33
|
+
object = operation(file_path, yaml_data)
|
34
|
+
except ValidationError as e:
|
35
|
+
raise ValueError(f"Validation error for {name}:\n {e}")
|
36
|
+
objects.append(object)
|
37
|
+
return objects
|
38
|
+
|
39
|
+
|
40
|
+
def get_rules_from_yaml_files(directory):
|
41
|
+
return __get_object_from_yaml_files(directory, lambda file_path, data: Rule(**data), 'rule')
|
42
|
+
|
43
|
+
|
44
|
+
def get_tests_from_yaml_files(conversations):
|
45
|
+
return __get_object_from_yaml_files(conversations, lambda file_path, data: Test.build_test(file_path, data), 'test')
|
46
|
+
|
47
|
+
|
48
|
+
def check_rules(rules, conversations, verbose, csv_file):
|
49
|
+
"""
|
50
|
+
Processes metamorphic rules against a set of conversations
|
51
|
+
:param rules: the folder to the metamorphic rules
|
52
|
+
:param conversations: the folder to the conversations
|
53
|
+
:raises ValueError when paths rules or conversations do not exist
|
54
|
+
"""
|
55
|
+
for folder in [rules, conversations]:
|
56
|
+
if not os.path.exists(folder):
|
57
|
+
raise ValueError(f"Invalid path: {folder}.")
|
58
|
+
|
59
|
+
print(f"Testing rules at {rules} into conversations at {conversations}")
|
60
|
+
rules = get_rules_from_yaml_files(rules)
|
61
|
+
rules = [rule for rule in rules if rule.active] # filter the inactive rules
|
62
|
+
tests = get_tests_from_yaml_files(conversations)
|
63
|
+
result_store = Result()
|
64
|
+
for rule in rules:
|
65
|
+
results = rule.test(tests, verbose)
|
66
|
+
result_store.add(rule.name, results)
|
67
|
+
|
68
|
+
report_generic_error(result_store, tests)
|
69
|
+
|
70
|
+
if csv_file is not None:
|
71
|
+
result_store.to_csv(csv_file)
|
72
|
+
|
73
|
+
|
74
|
+
# Add to the report generic checks
|
75
|
+
def report_generic_error(result_store, tests):
|
76
|
+
by_error = {}
|
77
|
+
for e in errors.all_errors.keys():
|
78
|
+
by_error[e] = {'pass': [], 'fail': [], 'not_applicable': []}
|
79
|
+
for c in tests:
|
80
|
+
for name, error_code in errors.all_errors.items():
|
81
|
+
if error_code in [next(iter(e)) for e in c.errors]:
|
82
|
+
by_error[name]['fail'].append(c.file_name)
|
83
|
+
else:
|
84
|
+
by_error[name]['pass'].append(c.file_name)
|
85
|
+
for name, results in by_error.items():
|
86
|
+
result_store.add(name, results)
|
87
|
+
|
88
|
+
def main():
|
89
|
+
parser = ArgumentParser(description='Tester of conversations against metamorphic rules')
|
90
|
+
parser.add_argument('--rules', required=True, help='Folder with the yaml files containing the metamorphic rules')
|
91
|
+
parser.add_argument('--conversations', required=True, help='Folder with the conversations to analyse')
|
92
|
+
parser.add_argument('--verbose', default=False, action='store_true')
|
93
|
+
parser.add_argument('--dump', required=False, help='CSV file to store the statistics')
|
94
|
+
args = parser.parse_args()
|
95
|
+
check_keys(["OPENAI_API_KEY"])
|
96
|
+
|
97
|
+
try:
|
98
|
+
check_rules(args.rules, args.conversations, args.verbose, args.dump)
|
99
|
+
except ValueError as e:
|
100
|
+
print(f"Error: {e}", file=sys.stderr)
|
101
|
+
|
102
|
+
if __name__ == '__main__':
|
103
|
+
main()
|