user-simulator 0.1.1__tar.gz → 0.1.3__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.
- {user_simulator-0.1.1/src/user_simulator.egg-info → user_simulator-0.1.3}/PKG-INFO +1 -1
- {user_simulator-0.1.1 → user_simulator-0.1.3}/pyproject.toml +2 -2
- user_simulator-0.1.3/src/metamorphic/__init__.py +9 -0
- user_simulator-0.1.3/src/metamorphic/results.py +62 -0
- user_simulator-0.1.3/src/metamorphic/rule_utils.py +482 -0
- user_simulator-0.1.3/src/metamorphic/rules.py +231 -0
- user_simulator-0.1.3/src/metamorphic/tests.py +83 -0
- user_simulator-0.1.3/src/metamorphic/text_comparison_utils.py +31 -0
- user_simulator-0.1.3/src/technologies/chatbot_connectors.py +567 -0
- user_simulator-0.1.3/src/technologies/chatbots.py +80 -0
- user_simulator-0.1.3/src/technologies/taskyto.py +110 -0
- user_simulator-0.1.3/src/user_sim/__init__.py +15 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/cli/sensei_chat.py +1 -1
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/core/role_structure.py +1 -1
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/handlers/pdf_parser_module.py +1 -1
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/utils/register_management.py +1 -1
- {user_simulator-0.1.1 → user_simulator-0.1.3/src/user_simulator.egg-info}/PKG-INFO +1 -1
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_simulator.egg-info/SOURCES.txt +10 -0
- user_simulator-0.1.3/src/user_simulator.egg-info/top_level.txt +3 -0
- user_simulator-0.1.1/src/user_simulator.egg-info/top_level.txt +0 -1
- {user_simulator-0.1.1 → user_simulator-0.1.3}/LICENSE.txt +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/README.md +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/setup.cfg +0 -0
- {user_simulator-0.1.1/src/user_sim → user_simulator-0.1.3/src/technologies}/__init__.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/cli/__init__.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/cli/gen_user_profile.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/cli/init_project.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/cli/sensei_check.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/cli/validation_check.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/core/__init__.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/core/ask_about.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/core/data_extraction.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/core/data_gathering.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/core/interaction_styles.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/core/user_simulator.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/handlers/__init__.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/handlers/asr_module.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/handlers/html_parser_module.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/handlers/image_recognition_module.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/utils/__init__.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/utils/config.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/utils/cost_tracker.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/utils/cost_tracker_v2.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/utils/errors.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/utils/exceptions.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/utils/languages.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/utils/show_logs.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/utils/token_cost_calculator.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/utils/url_management.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_sim/utils/utilities.py +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_simulator.egg-info/dependency_links.txt +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_simulator.egg-info/entry_points.txt +0 -0
- {user_simulator-0.1.1 → user_simulator-0.1.3}/src/user_simulator.egg-info/requires.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "user-simulator"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.3"
|
4
4
|
description = "LLM-based user simulator for chatbot testing."
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.12"
|
@@ -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,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
|