user-simulator 0.1.1__tar.gz → 0.1.2__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 (54) hide show
  1. {user_simulator-0.1.1/src/user_simulator.egg-info → user_simulator-0.1.2}/PKG-INFO +1 -1
  2. {user_simulator-0.1.1 → user_simulator-0.1.2}/pyproject.toml +7 -7
  3. user_simulator-0.1.2/src/metamorphic/__init__.py +9 -0
  4. user_simulator-0.1.2/src/metamorphic/results.py +62 -0
  5. user_simulator-0.1.2/src/metamorphic/rule_utils.py +482 -0
  6. user_simulator-0.1.2/src/metamorphic/rules.py +231 -0
  7. user_simulator-0.1.2/src/metamorphic/tests.py +83 -0
  8. user_simulator-0.1.2/src/metamorphic/text_comparison_utils.py +31 -0
  9. user_simulator-0.1.2/src/technologies/chatbot_connectors.py +567 -0
  10. user_simulator-0.1.2/src/technologies/chatbots.py +80 -0
  11. user_simulator-0.1.2/src/technologies/taskyto.py +110 -0
  12. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/cli/sensei_chat.py +1 -1
  13. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/core/role_structure.py +1 -1
  14. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/handlers/pdf_parser_module.py +1 -1
  15. user_simulator-0.1.2/src/user_sim/utils/__init__.py +0 -0
  16. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/utils/register_management.py +1 -1
  17. {user_simulator-0.1.1 → user_simulator-0.1.2/src/user_simulator.egg-info}/PKG-INFO +1 -1
  18. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_simulator.egg-info/SOURCES.txt +10 -0
  19. user_simulator-0.1.2/src/user_simulator.egg-info/entry_points.txt +6 -0
  20. user_simulator-0.1.2/src/user_simulator.egg-info/top_level.txt +3 -0
  21. user_simulator-0.1.1/src/user_simulator.egg-info/entry_points.txt +0 -6
  22. user_simulator-0.1.1/src/user_simulator.egg-info/top_level.txt +0 -1
  23. {user_simulator-0.1.1 → user_simulator-0.1.2}/LICENSE.txt +0 -0
  24. {user_simulator-0.1.1 → user_simulator-0.1.2}/README.md +0 -0
  25. {user_simulator-0.1.1 → user_simulator-0.1.2}/setup.cfg +0 -0
  26. {user_simulator-0.1.1/src/user_sim → user_simulator-0.1.2/src/technologies}/__init__.py +0 -0
  27. {user_simulator-0.1.1/src/user_sim/cli → user_simulator-0.1.2/src/user_sim}/__init__.py +0 -0
  28. {user_simulator-0.1.1/src/user_sim/core → user_simulator-0.1.2/src/user_sim/cli}/__init__.py +0 -0
  29. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/cli/gen_user_profile.py +0 -0
  30. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/cli/init_project.py +0 -0
  31. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/cli/sensei_check.py +0 -0
  32. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/cli/validation_check.py +0 -0
  33. {user_simulator-0.1.1/src/user_sim/handlers → user_simulator-0.1.2/src/user_sim/core}/__init__.py +0 -0
  34. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/core/ask_about.py +0 -0
  35. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/core/data_extraction.py +0 -0
  36. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/core/data_gathering.py +0 -0
  37. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/core/interaction_styles.py +0 -0
  38. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/core/user_simulator.py +0 -0
  39. {user_simulator-0.1.1/src/user_sim/utils → user_simulator-0.1.2/src/user_sim/handlers}/__init__.py +0 -0
  40. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/handlers/asr_module.py +0 -0
  41. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/handlers/html_parser_module.py +0 -0
  42. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/handlers/image_recognition_module.py +0 -0
  43. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/utils/config.py +0 -0
  44. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/utils/cost_tracker.py +0 -0
  45. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/utils/cost_tracker_v2.py +0 -0
  46. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/utils/errors.py +0 -0
  47. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/utils/exceptions.py +0 -0
  48. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/utils/languages.py +0 -0
  49. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/utils/show_logs.py +0 -0
  50. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/utils/token_cost_calculator.py +0 -0
  51. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/utils/url_management.py +0 -0
  52. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_sim/utils/utilities.py +0 -0
  53. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_simulator.egg-info/dependency_links.txt +0 -0
  54. {user_simulator-0.1.1 → user_simulator-0.1.2}/src/user_simulator.egg-info/requires.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: user-simulator
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: LLM-based user simulator for chatbot testing.
5
5
  Author: Alejandro Del Pozzo Escalera, Juan de Lara Jaramillo, Esther Guerra Sánchez
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "user-simulator"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "LLM-based user simulator for chatbot testing."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -33,11 +33,11 @@ dependencies = [
33
33
  Homepage = "https://github.com/satori-chatbots/user-simulator"
34
34
 
35
35
  [project.scripts]
36
- sensei-init-project = "user_sim.cli.init_project:main"
37
- sensei-chat = "user_sim.cli.sensei_chat:main"
38
- sensei-check = "user_sim.cli.sensei_check:main"
39
- sensei-validation-check = "user_sim.cli.validation_check:main"
40
- sensei-gen-user-profile = "user_sim.cli.gen_user_profile:main"
36
+ sensei-init-project = "src.user_sim.cli.init_project:main"
37
+ sensei-chat = "src.user_sim.cli.sensei_chat:main"
38
+ sensei-check = "src.user_sim.cli.sensei_check:main"
39
+ sensei-validation-check = "src.user_sim.cli.validation_check:main"
40
+ sensei-gen-user-profile = "src.user_sim.cli.gen_user_profile:main"
41
41
 
42
42
  [build-system]
43
43
  requires = ["setuptools>=61.0"]
@@ -49,5 +49,5 @@ package-dir = {"" = "src"}
49
49
 
50
50
  [tool.setuptools.packages.find]
51
51
  where = ["src"]
52
- include = ["user_sim*"]
52
+ include = ["user_sim*", "metamorphic*", "technologies*"]
53
53
 
@@ -0,0 +1,9 @@
1
+ filtered_tests = []
2
+
3
+
4
+ def get_filtered_tests():
5
+ return filtered_tests
6
+
7
+
8
+ def empty_filtered_tests():
9
+ filtered_tests = []
@@ -0,0 +1,62 @@
1
+ import csv
2
+
3
+
4
+ def stat_to_str(rule:str, stats: dict) -> str:
5
+ return (f"\n - rule {rule}: "
6
+ f"{stats['checks']} checks, "
7
+ f"fail {stats['fail']} times, "
8
+ f"satisfied {stats['pass']} times. "
9
+ f"Fail rate = {stats['fail_rate']}")
10
+
11
+
12
+ class Result:
13
+
14
+ def __init__(self):
15
+ self.results_dict = dict()
16
+
17
+ def add(self, name, results):
18
+ self.results_dict[name] = results
19
+
20
+ def __str__(self):
21
+ result = 'Statistics:'
22
+ stats_dict = self.stats()
23
+ for rule in stats_dict:
24
+ result += stat_to_str(rule, stats_dict[rule])
25
+ return result
26
+
27
+ def stats(self) -> dict:
28
+ """
29
+ :return: dictionary with stats (checks, pass, fail, not_applicable, fail_rate) about each rule
30
+ """
31
+ stats_dict = dict()
32
+ for rule in self.results_dict:
33
+ total = sum(len(files) for files in self.results_dict[rule].values())
34
+ sat = len(self.results_dict[rule]['pass'])
35
+ fail = len(self.results_dict[rule]['fail'])
36
+ not_applic = len(self.results_dict[rule]['not_applicable'])
37
+ if (sat + fail) > 0:
38
+ fail_rate = 100.0 * fail / (sat + fail)
39
+ else:
40
+ fail_rate = 0.0
41
+ stats_dict[rule] = {
42
+ 'checks': total,
43
+ 'pass': sat,
44
+ 'fail': fail,
45
+ 'not_applicable': not_applic,
46
+ 'fail_rate': f"{fail_rate:.2f}%"
47
+ }
48
+ return stats_dict
49
+
50
+ def to_csv(self, file_name: str):
51
+ stats_dict = self.stats()
52
+ with open(file_name, mode='w', newline='') as file:
53
+ writer = csv.DictWriter(file, fieldnames=['rule', 'checks', 'pass', 'fail', 'not_applicable', 'fail_rate'])
54
+ writer.writeheader()
55
+ for key, value in stats_dict.items():
56
+ row = {'rule': key}
57
+ row.update(value)
58
+ writer.writerow(row)
59
+
60
+
61
+
62
+
@@ -0,0 +1,482 @@
1
+ import ast
2
+ import os
3
+ import re
4
+ from types import SimpleNamespace
5
+
6
+ import inflect
7
+ from openai import OpenAI
8
+
9
+ from metamorphic import get_filtered_tests
10
+ from metamorphic.text_comparison_utils import exact_similarity, tf_idf_cosine_similarity, jaccard_similarity, \
11
+ sequence_similarity
12
+
13
+ filtered_tests = []
14
+ interaction = []
15
+
16
+ def util_functions_to_dict() -> dict:
17
+ """
18
+ :return: a dict with all functions defined in this module
19
+ """
20
+ return {'extract_float': extract_float,
21
+ 'currency': currency,
22
+ 'language': language,
23
+ 'length': length,
24
+ 'tone': tone,
25
+ '_only_talks_about': _only_talks_about,
26
+ 'is_unique': is_unique,
27
+ 'exists': exists,
28
+ 'num_exist': num_exist,
29
+ '_data_collected': _data_collected,
30
+ '_utterance_index': _utterance_index,
31
+ '_conversation_length': _conversation_length,
32
+ '_chatbot_returns': _chatbot_returns,
33
+ '_repeated_answers': _repeated_answers,
34
+ '_missing_slots': _missing_slots,
35
+ '_responds_in_same_language': _responds_in_same_language,
36
+ 'semantic_content': semantic_content}
37
+
38
+ def util_to_wrapper_dict() -> dict:
39
+ return {'_conversation_length':
40
+ " def conversation_length(who = 'both'):\n return _conversation_length(interaction, who)\n",
41
+ '_only_talks_about':
42
+ " def only_talks_about(topics, fallback=None):\n return _only_talks_about(topics, interaction, fallback)\n",
43
+ '_utterance_index':
44
+ " def utterance_index(who, what):\n return _utterance_index(who, what, interaction)\n",
45
+ '_chatbot_returns':
46
+ " def chatbot_returns(what, and_not=None):\n return _chatbot_returns(what, and_not, interaction)\n",
47
+ '_repeated_answers':
48
+ " def repeated_answers(method = 'exact', threshold = 0.4):\n return _repeated_answers(interaction, method, threshold)\n",
49
+ '_data_collected':
50
+ " def data_collected():\n return _data_collected(conv)\n",
51
+ '_missing_slots':
52
+ " def missing_slots():\n return _missing_slots(conv)\n",
53
+ '_responds_in_same_language':
54
+ " def responds_in_same_language():\n return _responds_in_same_language(interaction)\n",
55
+ }
56
+
57
+ def semantic_content(variable, content) -> bool:
58
+ """
59
+ checks if variable contents semantically the content
60
+ :param variable:
61
+ :param content:
62
+ :return:
63
+ """
64
+ prompt = f"""Your task it to detect if the text below has the following semantic content: {content}.
65
+ TEXT: {variable}
66
+ Return only YES or NO"""
67
+ response = call_openai(prompt)
68
+ if response.lower() == 'yes':
69
+ return True
70
+ else:
71
+ return False
72
+
73
+ def _repeated_answers(interaction, method = 'exact', threshold = 0.4):
74
+ """
75
+ :param interaction: a list with the user-chatbot interactions
76
+ :return: a dictionary of repeated phrases by the chatbot (keys=phrase, value=list of step numbers)
77
+ """
78
+ comparator = build_comparator(method)
79
+ repeated_phrases = dict()
80
+ step_index = 0
81
+ for step in interaction:
82
+ for key, value in step.items():
83
+ if key.lower() in ['chatbot', 'assistant']:
84
+ similar = find_similar(comparator, value, repeated_phrases, threshold)
85
+ if similar is not None:
86
+ repeated_phrases[similar].append(value)
87
+ else:
88
+ repeated_phrases[value] = []
89
+ step_index += 1
90
+ for key in list(repeated_phrases.keys()): # remove phrases that occur only 1
91
+ if len(repeated_phrases[key])<1:
92
+ del repeated_phrases[key]
93
+
94
+ #print(f" REPEATED PHRASES {repeated_phrases}")
95
+ return repeated_phrases
96
+
97
+ def find_similar(comparator, phrase, phrases, threshold):
98
+ for key in phrases:
99
+ similarity = comparator(phrase, key)
100
+ if similarity >= threshold:
101
+ return key
102
+ return None
103
+
104
+ def build_comparator(method):
105
+ if method.lower() == 'tf-idf':
106
+ return tf_idf_cosine_similarity
107
+ elif method.lower() == 'jaccard':
108
+ return jaccard_similarity
109
+ elif method.lower() == 'sequence-matcher':
110
+ return sequence_similarity
111
+ else:
112
+ return exact_similarity
113
+
114
+ def _chatbot_returns(what, and_not=None, interaction=None):
115
+ """
116
+ :param what: pattern
117
+ :param and_not: additional pattern that must not be found after what
118
+ :param interaction: list of user-chatbot interactions
119
+ :return: a list with the interactions in which the assistant returns a message that contains the pattern
120
+ To-Do: we could add additional parameters, like and_then, etc
121
+ """
122
+
123
+ def contains_first_and_not_second(string, X, Y):
124
+ pos_X = string.find(X)
125
+
126
+ if pos_X != -1:
127
+ pos_Y = string.find(Y, pos_X + len(X))
128
+ if pos_Y == -1:
129
+ return True
130
+
131
+ return False
132
+
133
+ interactions = []
134
+ step_index = 0
135
+ for step in interaction: # step is a dict
136
+ for key, value in step.items():
137
+ if key.lower() in ['chatbot', 'assistant']:
138
+ if what in value:
139
+ if and_not is None:
140
+ interactions.append(step_index)
141
+ elif contains_first_and_not_second(value, what, and_not):
142
+ interactions.append(step_index)
143
+ step_index += 1
144
+ return interactions
145
+
146
+ def _conversation_length(interaction, who = 'both'):
147
+ who = who.lower()
148
+ if who not in ['user', 'chatbot', 'assistant', 'both']:
149
+ raise ValueError(f"Expected 'user', 'chatbot' or 'both', but got '{who}'")
150
+ if who.lower() == 'both':
151
+ return len(interaction)
152
+ else:
153
+ number = 0
154
+ for step in interaction: # step is a dict
155
+ for key, value in step.items():
156
+ if key.lower() == who:
157
+ number += 1
158
+ return number
159
+
160
+ def _data_collected(conv):
161
+ outputs = conv[0].data_output
162
+ for data in outputs:
163
+ for key, value in data.items():
164
+ if value is None or value == 'None':
165
+ return False
166
+ return True
167
+
168
+
169
+ def _missing_slots(conv):
170
+ missing = []
171
+ outputs = conv[0].data_output
172
+ for data in outputs:
173
+ for key, value in data.items():
174
+ if value is None or value == 'None':
175
+ missing.append(key)
176
+ return missing
177
+
178
+ def interaction_to_str(interaction, numbered=False):
179
+ result = ''
180
+ index = 1
181
+ for step in interaction: # step is a dict
182
+ for key, value in step.items():
183
+ if numbered:
184
+ result+=f"{index} - "
185
+ index += 1
186
+ result+=f"{key} : {value}\n"
187
+ return result
188
+
189
+
190
+ def _utterance_index(who, what, conversation) -> int:
191
+ """
192
+ :param who: 'user', 'assistant', 'chatbot'
193
+ :param what: what is to be checked
194
+ :return: the conversation turn where it happened
195
+ """
196
+ numbered_conversation = interaction_to_str(conversation, True)
197
+ prompt = f"""The following is a conversation between a user and an assistant chatbot.
198
+ Your task it to detect the conversation turn where the {who} talked about {what}".
199
+ Return only the conversation turn number, and nothing else, just the number.\n\n {numbered_conversation}"""
200
+ response = call_openai(prompt)
201
+ if response.isnumeric():
202
+ return int(response)
203
+ else:
204
+ return -1
205
+
206
+ def _only_talks_about(topics, conversation, fallback = None) -> bool:
207
+ """
208
+ checks if the chatbot only talks about certain topics
209
+ """
210
+ if isinstance(topics, str):
211
+ topics = [topics]
212
+ if not isinstance(topics, list):
213
+ raise ValueError(f"Expecting a list of strings, or a string, but got {topics}")
214
+
215
+ if not isinstance(conversation, list):
216
+ raise ValueError(f"Expecting a list with the chatbot-user interaction, but got {conversation}")
217
+
218
+ if fallback is not None:
219
+ fallback_phrase = f', using phrases like "{fallback}"'
220
+ else:
221
+ fallback_phrase = ''
222
+
223
+ talk_topics = ','.join(topics)
224
+ phrases = interaction_to_str(conversation)
225
+ prompt = f"""The following is a conversation between a user and an assistant chatbot:\n\n
226
+ {phrases}
227
+ ----
228
+ Your task it to detect if the assistant deviates from the following topics "{talk_topics}".
229
+ The assistant is not allowed to follow the conversation of the user, if the user tries to talk on topics different
230
+ from the previous ones. In that case, the chatbot should redirect the conversation to the previous topics.
231
+ The assistant is allowed to ask for clarifications or to express that it does not understand the user{fallback_phrase}.
232
+ Return the following:
233
+ - ONLY 'True' if the chatbot sticks to "{talk_topics}", even if the user tries to talk about another topic.
234
+ - ONLY the list of assistant answers that deviate from the previous topics."""
235
+ response = call_openai_o1(prompt)
236
+ if response.lower() == "true":
237
+ return True
238
+ else:
239
+ #error_phrases = ast.literal_eval(response)
240
+ return f"The following chatbot responses are out of scope: {response}"
241
+ #raise TestError(error_phrases, f"The following chatbot responses are out of scope: {error_phrases}")
242
+
243
+ def num_exist(condition: str) -> int:
244
+ num = 0
245
+ for test in get_filtered_tests():
246
+ test_dict = test.to_dict()
247
+ conv = [SimpleNamespace(**test_dict)]
248
+ test_dict['conv'] = conv
249
+ test_dict.update(util_functions_to_dict())
250
+ if eval(condition, test_dict):
251
+ num += 1
252
+ return num
253
+
254
+ def exists(condition: str) -> bool:
255
+ for test in get_filtered_tests():
256
+ test_dict = test.to_dict()
257
+ conv = [SimpleNamespace(**test_dict)]
258
+ test_dict['conv'] = conv
259
+ test_dict.update(util_functions_to_dict())
260
+ if eval(condition, test_dict):
261
+ # print(f" Satisfied on {test.file_name}")
262
+ return True
263
+ return False
264
+
265
+
266
+ def is_unique(property: str) -> bool:
267
+ values = dict() # a dictionary of property values to test file name
268
+ for test in get_filtered_tests():
269
+ var_dict = test.to_dict()
270
+ if property not in var_dict:
271
+ continue
272
+ if var_dict[property] in values:
273
+ print(f" Tests: {test.file_name} and {values[var_dict[property]]} have value {var_dict[property]} for {property}.")
274
+ return False
275
+ elif var_dict[property] is not None:
276
+ values[var_dict[property]] = test.file_name
277
+ return True
278
+
279
+ def extract_float(string: str) -> float:
280
+ """
281
+ Function that returns the first float number inside the string
282
+ :param string: the string the float number is to be extracted
283
+ :return: the first float number inside the string
284
+ """
285
+ # remove , as marker of thousands
286
+ pattern = r'(?<=\d),(?=\d)'
287
+ cleaned_text = re.sub(pattern, '', string)
288
+ pattern = r'[-+]?\d*\.\d+|\d+' # A regular expression pattern for a float number
289
+ match = re.search(pattern, cleaned_text)
290
+
291
+ if match:
292
+ return float(match.group())
293
+ else:
294
+ return None
295
+
296
+
297
+ def currency(string: str) -> str:
298
+ """
299
+ Extracts the first currency within the string, either as symbol (e.g. €), abbreviation (e.g. 'EUR') or full name
300
+ ('euro')
301
+ :param string:
302
+ :return:
303
+ """
304
+ for extractor in [currency_symbol, currency_abbreviation, currency_name]:
305
+ curr = extractor(string)
306
+ if curr is not None:
307
+ return curr
308
+ return None
309
+
310
+
311
+ def currency_symbol(string: str) -> str:
312
+ pattern = r'[$€£¥₹]'
313
+ map_currency = {'$': 'USD', '€': 'EUR', '£': 'GBP', '¥': 'JPY', '₹': 'INR'}
314
+ match = re.search(pattern, string)
315
+
316
+ if match:
317
+ return map_currency[match.group()]
318
+ else:
319
+ return None
320
+
321
+
322
+ def currency_abbreviation(string: str) -> str:
323
+ pattern = r'\b(USD|EUR|GBP|JPY|INR)\b'
324
+ match = re.search(pattern, string)
325
+
326
+ if match:
327
+ return match.group()
328
+ else:
329
+ return None
330
+
331
+
332
+ def currency_name(string: str):
333
+ pattern = r'\b(dollars|euros|pounds|yen|rupees)\b'
334
+ map_currency = {'dollars': 'USD', 'euros': 'EUR', 'pounds': 'GBP', 'yen': 'JPY', 'rupees': 'INR'}
335
+ match = re.search(pattern, string, re.IGNORECASE)
336
+
337
+ if match:
338
+ return map_currency[match.group()]
339
+ else:
340
+ return None
341
+
342
+
343
+ def length(item, kind='min'):
344
+ """
345
+ :param item: a string or a list of strings
346
+ :param kind: which length to provide. Accepted values are min, max or average
347
+ :return: the desired length
348
+ """
349
+ if isinstance(item, str):
350
+ item = [item]
351
+ if not isinstance(item, list):
352
+ raise ValueError(f"Expecting a list of strings, or a string, but got {item}")
353
+ kind = kind.lower()
354
+ if kind not in ['min', 'max', 'average']:
355
+ raise ValueError(f"Expecting one of min, max or average, but got {kind}")
356
+
357
+ inits = {
358
+ 'min': 100000000,
359
+ 'max': 0,
360
+ 'average': 0
361
+ }
362
+ current = inits[kind]
363
+ operations = {
364
+ 'min': lambda x, n: x if x < current else current,
365
+ 'max': lambda x, n: x if x > current else current,
366
+ 'average': lambda x, n: (current + x) / n
367
+ }
368
+ iteration = 1
369
+ for element in item:
370
+ current = operations[kind](len(element), iteration)
371
+ iteration += 1
372
+ return current
373
+
374
+
375
+ def language(string):
376
+ """
377
+ returns the language of the given string, or list of strings
378
+ :param string: a list of strings or a string
379
+ :return: One of the codes of the languages dictionary
380
+ """
381
+ languages = {'ENG': 'English',
382
+ 'ESP': 'Spanish',
383
+ 'FR': 'French',
384
+ 'GER': 'German',
385
+ 'IT': 'Italian',
386
+ 'POR': 'Portuguese',
387
+ 'CHI': 'Chinese',
388
+ 'JAP': 'Japanese',
389
+ 'OTHER': 'other language'}
390
+
391
+ p = inflect.engine()
392
+ lang_list = [f"{k} for {v}" for k, v in languages.items()]
393
+ prompt = f"""What is the language of the following text?: \n {string}. \n Return {p.join(tuple(lang_list))}."""
394
+ response = call_openai(prompt)
395
+ return response
396
+
397
+
398
+ def tone(item):
399
+ """
400
+ returns the tone ('POSITIVE', 'NEGATIVE', 'NEUTRAL') of each text in the parameter
401
+ :param string: the text
402
+ :return: a list with 'POSITIVE', 'NEGATIVE', 'NEUTRAL' for each text in item
403
+ """
404
+ if isinstance(item, str):
405
+ item = [item]
406
+ if not isinstance(item, list):
407
+ raise ValueError(f"Expecting a list of strings, or a string, but got {item}")
408
+
409
+ responses = []
410
+ for element in item:
411
+ prompt = f"""What is the tone of the following text: \n {element}. \n Return only POSTIVE, NEGATIVE or NEUTRAL."""
412
+ response = call_openai(prompt)
413
+ responses.append(response)
414
+ return responses
415
+
416
+
417
+ def call_openai(message: str, llm="gpt-4o", temp = 0, stream_mode = True):
418
+ """
419
+ Send a message to OpenAI and returns the answer.
420
+ :param str message: The message to send
421
+ :return The answer to the message
422
+ """
423
+ client = OpenAI()
424
+ stream = client.chat.completions.create(
425
+ # model="gpt-3.5-turbo",
426
+ model=llm,
427
+ messages=[{"role": "user", "content": message}],
428
+ temperature=temp,
429
+ stream=stream_mode)
430
+ chunks = ''
431
+ for chunk in stream:
432
+ for choice in chunk.choices:
433
+ if choice.delta.content is not None:
434
+ chunks += choice.delta.content
435
+ return chunks
436
+
437
+ def call_openai_o1(message: str):
438
+ """
439
+ Send a message to OpenAI o1 and returns the answer.
440
+ :param str message: The message to send
441
+ :return The answer to the message
442
+ """
443
+ client = OpenAI()
444
+ response = client.chat.completions.create(
445
+ # model="gpt-3.5-turbo",
446
+ model="o1-mini",
447
+ messages=[{"role": "user", "content": message}])
448
+ chunks = ''
449
+ for choice in response.choices:
450
+ chunks += choice.message.content
451
+ return chunks
452
+
453
+ def _responds_in_same_language(interaction):
454
+ """
455
+ returns if the chatbot responds in the same language as the user
456
+ :param interaction: a list with the interaction
457
+ :return: the turn in which the assistant responds in a different language
458
+ """
459
+ def filter_errors(interaction):
460
+ for step in interaction[:]: # step is a dict, we iterate over copu
461
+ for key, value in step.items():
462
+ if key.lower() in ['chatbot', 'assistant']:
463
+ if value.startswith('Error: The server'):
464
+ interaction.remove(step)
465
+ return interaction
466
+
467
+ # filter the errrors of the chatbot, which always are shown as English text
468
+ filtered_interaction = filter_errors(interaction)
469
+ if len(filtered_interaction) == 1: # nothing to do
470
+ return True
471
+ prompt = f"""You are an assistant that checks conversations between a chatbot and a human.
472
+ Your task is to assess if the chatbot always responds in the same language as the
473
+ human. For example, if the human speaks in Spanish, the chatbot should respond in Spanish, too.
474
+ Given the conversation below, return:
475
+ - YES if the chatbot responds in the same language.
476
+ - NO if the chatbot sometimes responds in a different language.
477
+ CONVERSATION:
478
+ {filtered_interaction}"""
479
+ response = call_openai(prompt)
480
+ if response.lower() == 'yes':
481
+ return True
482
+ return False