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.
Files changed (37) hide show
  1. user_sim/__init__.py +0 -0
  2. user_sim/cli/__init__.py +0 -0
  3. user_sim/cli/gen_user_profile.py +34 -0
  4. user_sim/cli/init_project.py +65 -0
  5. user_sim/cli/sensei_chat.py +481 -0
  6. user_sim/cli/sensei_check.py +103 -0
  7. user_sim/cli/validation_check.py +143 -0
  8. user_sim/core/__init__.py +0 -0
  9. user_sim/core/ask_about.py +665 -0
  10. user_sim/core/data_extraction.py +260 -0
  11. user_sim/core/data_gathering.py +134 -0
  12. user_sim/core/interaction_styles.py +147 -0
  13. user_sim/core/role_structure.py +608 -0
  14. user_sim/core/user_simulator.py +302 -0
  15. user_sim/handlers/__init__.py +0 -0
  16. user_sim/handlers/asr_module.py +128 -0
  17. user_sim/handlers/html_parser_module.py +202 -0
  18. user_sim/handlers/image_recognition_module.py +139 -0
  19. user_sim/handlers/pdf_parser_module.py +123 -0
  20. user_sim/utils/__init__.py +0 -0
  21. user_sim/utils/config.py +47 -0
  22. user_sim/utils/cost_tracker.py +153 -0
  23. user_sim/utils/cost_tracker_v2.py +193 -0
  24. user_sim/utils/errors.py +15 -0
  25. user_sim/utils/exceptions.py +47 -0
  26. user_sim/utils/languages.py +78 -0
  27. user_sim/utils/register_management.py +62 -0
  28. user_sim/utils/show_logs.py +63 -0
  29. user_sim/utils/token_cost_calculator.py +338 -0
  30. user_sim/utils/url_management.py +60 -0
  31. user_sim/utils/utilities.py +568 -0
  32. user_simulator-0.1.0.dist-info/METADATA +733 -0
  33. user_simulator-0.1.0.dist-info/RECORD +37 -0
  34. user_simulator-0.1.0.dist-info/WHEEL +5 -0
  35. user_simulator-0.1.0.dist-info/entry_points.txt +6 -0
  36. user_simulator-0.1.0.dist-info/licenses/LICENSE.txt +21 -0
  37. user_simulator-0.1.0.dist-info/top_level.txt +1 -0
user_sim/__init__.py ADDED
File without changes
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()