dynamic-learning-model 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.
- dlm/DLM.py +1372 -0
- dlm/__init__.py +1 -0
- dynamic_learning_model-1.0.dist-info/METADATA +55 -0
- dynamic_learning_model-1.0.dist-info/RECORD +7 -0
- dynamic_learning_model-1.0.dist-info/WHEEL +5 -0
- dynamic_learning_model-1.0.dist-info/licenses/LICENSE +21 -0
- dynamic_learning_model-1.0.dist-info/top_level.txt +1 -0
dlm/DLM.py
ADDED
|
@@ -0,0 +1,1372 @@
|
|
|
1
|
+
# Dynamic-Learning Model (DLM) bot that learns how to respond to questions by learning from user input/expectations, as well as computationally solve arithmetics
|
|
2
|
+
import difflib
|
|
3
|
+
import string
|
|
4
|
+
import random
|
|
5
|
+
import spacy
|
|
6
|
+
import time
|
|
7
|
+
import sqlite3
|
|
8
|
+
import re
|
|
9
|
+
import nltk
|
|
10
|
+
from better_profanity import profanity
|
|
11
|
+
from nltk.corpus import names
|
|
12
|
+
from word2number import w2n
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DLM:
|
|
16
|
+
__filename = None # knowledge-base (SQL)
|
|
17
|
+
__query = None # user-inputted query
|
|
18
|
+
__expectation = None # trainer-inputted expected answer to query
|
|
19
|
+
__category = None # categorizes each question for efficient retrieval and basic NLG in SQL DB
|
|
20
|
+
__nlp = None # Spacy NLP analysis
|
|
21
|
+
__tone = None # sentimental tone of user query
|
|
22
|
+
__trainingPwd = "371507" # password to enter training mode
|
|
23
|
+
__mode = None # either "training", "commercial", or "experimental"
|
|
24
|
+
__singlePassthrough = True # used to prevent multiple iterations of training prompt
|
|
25
|
+
__unsure_while_thinking = False # if uncertain while thinking, then it will let the user know that
|
|
26
|
+
__nlp_similarity_value = None # saves the similarity value by doing SpaCy calculation (for debugging)
|
|
27
|
+
__special_stripped_query = None # saves query without any special words for reduced interference while vector calculating
|
|
28
|
+
__nltk_names = set(name.lower() for name in names.words())
|
|
29
|
+
|
|
30
|
+
# personalized responses to let the user know that the bot doesn't know the answer
|
|
31
|
+
__fallback_responses = [
|
|
32
|
+
"Hmm, that's a great question! I might need more context or details to answer it.",
|
|
33
|
+
"I'm still training my brain on that topic. Could you clarify what you mean?",
|
|
34
|
+
"Oops! That one's not in my database yet, or maybe it's phrased in a way I don't recognize!",
|
|
35
|
+
"You got me this time! Could you try rewording it so I can understand better?",
|
|
36
|
+
"That's a tough one! I might need a bit more information to figure it out.",
|
|
37
|
+
"I don't have the answer just yet, but I bet it’s out there somewhere! Could you rephrase it?",
|
|
38
|
+
"Hmm... I’ll have to hit the books for that one! Or maybe I just need a little more context?",
|
|
39
|
+
"I haven't learned that yet, but I'm constantly improving! Maybe try a different wording?",
|
|
40
|
+
"You just stumped me! But no worries, I’m always evolving—maybe I misinterpreted the question?",
|
|
41
|
+
"That’s outside my knowledge base for now, or maybe I'm just not parsing it right!",
|
|
42
|
+
"I wish I had the answer! If it’s incomplete, could you add more details?",
|
|
43
|
+
"I’m not sure about that one. Maybe try breaking it down into smaller parts?",
|
|
44
|
+
"Hmm, I don’t have an answer yet. Could you reword or give more details?",
|
|
45
|
+
"Still learning this one! If something’s missing, feel free to add more context.",
|
|
46
|
+
"I don’t have that in my knowledge bank yet, or maybe I'm missing part of the question!"
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# words to be filtered from user input for better accuracy and fewer distractions
|
|
50
|
+
__filler_words = [
|
|
51
|
+
|
|
52
|
+
# articles & determiners (words that don't add meaning to sentence)
|
|
53
|
+
"the", "some", "any", "many", "every", "each", "either", "neither", "this", "that", "these", "those",
|
|
54
|
+
"certain", "another", "such", "whatsoever", "whichever", "whomever", "whatever", "all", "something", "possible",
|
|
55
|
+
|
|
56
|
+
# pronouns (general pronouns that don’t change meaning)
|
|
57
|
+
"i", "me", "my", "mine", "here",
|
|
58
|
+
"myself", "you", "your", "yours", "yourself", "he", "him", "his", "himself",
|
|
59
|
+
"she", "her", "hers", "herself", "it", "its", "itself", "we", "us", "our", "ours", "ourselves",
|
|
60
|
+
"they", "them", "their", "theirs", "themselves", "who", "whom", "whose", "which", "that",
|
|
61
|
+
"someone", "somebody", "anyone", "anybody", "everyone", "everybody", "nobody", "people", "person",
|
|
62
|
+
"whoever", "wherever", "whenever", "whosoever", "others", "oneself",
|
|
63
|
+
|
|
64
|
+
# auxiliary (helping) verbs (do not contribute meaning)
|
|
65
|
+
"get", "am", "is", "are", "was", "were", "be", "been", "being", "have", "has", "had", "having", "best", "do",
|
|
66
|
+
"does",
|
|
67
|
+
"did", "doing", "shall", "should", "will", "would", "can", "could", "may", "might", "must", "bad", "dare",
|
|
68
|
+
"need", "want",
|
|
69
|
+
"used", "shallnt", "shouldve", "wouldve", "couldve", "mustve", "mightve", "mustnt", "good",
|
|
70
|
+
|
|
71
|
+
# conjunctions (connectors that do not change meaning)
|
|
72
|
+
"and", "but", "or", "gotten",
|
|
73
|
+
"nor", "so", "for", "yet", "although", "though", "because", "since", "unless",
|
|
74
|
+
"while", "whereas", "either", "neither", "both", "whether", "not", "if", "even if", "even though", "common",
|
|
75
|
+
"as long as",
|
|
76
|
+
"provided that", "whereas", "therefore", "thus", "hence", "meanwhile", "besides", "furthermore",
|
|
77
|
+
|
|
78
|
+
# prepositions (location/relation words that are often unnecessary)
|
|
79
|
+
"about", "above", "across", "after", "against", "along", "among", "around", "as", "at",
|
|
80
|
+
"before", "behind", "below", "beneath", "beside", "between", "beyond", "by", "low", "high", "despite", "down",
|
|
81
|
+
"during", "happen",
|
|
82
|
+
"except", "for", "from", "in", "inside", "into",
|
|
83
|
+
"like", "near", "off", "on", "onto", "out", "outside", "over", "past",
|
|
84
|
+
"since", "through", "throughout", "till", "to", "toward", "under", "underneath",
|
|
85
|
+
"until", "up", "upon", "with", "within", "without", "aside from", "concerning", "regarding",
|
|
86
|
+
|
|
87
|
+
# common adverbs (time words and intensity words that add fluff)
|
|
88
|
+
"way", "ways", "again", "already", "also", "always", "ever", "never", "just", "now", "often",
|
|
89
|
+
"once", "only", "quite", "rather", "really", "seldom", "sometimes", "soon", "got",
|
|
90
|
+
"still", "then", "there", "therefore", "thus", "too", "very", "well", "anytime",
|
|
91
|
+
"hardly", "barely", "scarcely", "seriously", "truly", "frankly", "honestly", "basically", "literally",
|
|
92
|
+
"definitely", "obviously", "surely", "likely", "probably", "certainly", "clearly", "undoubtedly",
|
|
93
|
+
|
|
94
|
+
# question words (words that do not impact search meaning)
|
|
95
|
+
"what", "when", "where", "which", "who", "whom", "whose", "why", "how",
|
|
96
|
+
"whichever", "whomever", "whenever", "wherever", "whosoever", "however", "whence",
|
|
97
|
+
|
|
98
|
+
# informal/common fillers (spoken language fillers)
|
|
99
|
+
"gonna", "wanna", "gotta", "lemme", "dunno", "kinda", "sorta", "aint", "ya", "yeah", "nah",
|
|
100
|
+
"um", "uh", "hmm", "huh", "mmm", "uhh", "ahh", "err", "ugh", "tsk", "like", "okay", "ok", "alright",
|
|
101
|
+
"yo", "bruh", "dude", "bro", "sis", "mate", "fam", "nah", "yup", "nope", "welp",
|
|
102
|
+
|
|
103
|
+
# verbs commonly used in questions (but don’t change meaning)
|
|
104
|
+
"go", "do", "dont", "does", "did", "can", "can't", "could", "couldnt", "should", "shouldnt", "shall", "will",
|
|
105
|
+
"would", "wouldnt", "may", "might", "must", "use", "tell", "thinking",
|
|
106
|
+
"please", "say", "let", "know", "consider", "find", "show", "take", "working",
|
|
107
|
+
"list", "give", "provide", "make", "see", "mean", "understand", "point out", "stay", "look", "care", "work",
|
|
108
|
+
|
|
109
|
+
# contracted forms (casual writing contractions),
|
|
110
|
+
"ill", "im", "ive", "youd", "youll", "youre", "youve", "hed", "hell", "hes",
|
|
111
|
+
"shed", "shell", "shes", "wed", "well", "were", "weve", "theyd", "theyll", "theyre", "theyve",
|
|
112
|
+
"its", "thats", "whos", "whats", "wheres", "whens", "whys", "hows", "theres", "heres", "lets",
|
|
113
|
+
|
|
114
|
+
# conversational fillers (unnecessary words in casual speech)
|
|
115
|
+
"actually", "basically", "seriously", "literally", "obviously", "honestly", "frankly", "clearly",
|
|
116
|
+
"apparently", "probably", "definitely", "certainly", "most", "mostly", "mainly", "typically", "essentially",
|
|
117
|
+
"generally", "approximately", "virtually", "kind", "sort", "type", "whatever", "however",
|
|
118
|
+
"you know", "i mean", "you see", "by the way", "sort of", "kind of", "more or less",
|
|
119
|
+
"as far as i know", "in my opinion", "to be honest", "to be fair", "just saying",
|
|
120
|
+
"at the end of the day", "if you ask me", "truth be told", "the fact is", "long story short",
|
|
121
|
+
|
|
122
|
+
# internet slang, misspellings, and shortcuts
|
|
123
|
+
"lol", "lmao", "rofl", "omg", "idk", "fyi", "btw", "imo", "smh", "afk", "ttyl", "brb",
|
|
124
|
+
"thx", "pls", "ppl", "u", "ur", "r", "cuz", "coz", "cause", "gimme", "lemme", "wassup", "sup",
|
|
125
|
+
|
|
126
|
+
# placeholder & non-descriptive words
|
|
127
|
+
"thing", "stuff", "thingy", "whatchamacallit", "doohickey", "thingamajig", "thingamabob",
|
|
128
|
+
|
|
129
|
+
# words that don’t add meaning
|
|
130
|
+
"important", "necessary", "specific", "certain", "particular", "special", "exactly", "precisely",
|
|
131
|
+
"recently", "currently", "today", "tomorrow", "yesterday", "soon", "later", "eventually", "sometime",
|
|
132
|
+
|
|
133
|
+
# overused transitions
|
|
134
|
+
"so", "then", "therefore", "thus", "anyway", "besides", "moreover", "furthermore", "meanwhile"
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
# used for Chain-of-Thought (CoT) feature
|
|
138
|
+
__exception_fillers = [
|
|
139
|
+
"who", "whom", "whose", "what",
|
|
140
|
+
"which", "when", "where", "why",
|
|
141
|
+
"is", "are", "am", "was",
|
|
142
|
+
"were", "do", "does", "did",
|
|
143
|
+
"have", "has", "had", "can",
|
|
144
|
+
"could", "will", "would", "shall",
|
|
145
|
+
"should", "may", "might", "must",
|
|
146
|
+
"show", "list", "give", "how", "i"
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
# special words that the bot can mention while in "CoT"
|
|
150
|
+
__special_exception_fillers = ["define", "explain", "describe", "compare", "calculate", "translate", "mean"]
|
|
151
|
+
|
|
152
|
+
# advanced CoT computation identifiers
|
|
153
|
+
__computation_identifiers = {
|
|
154
|
+
# add
|
|
155
|
+
"+": [
|
|
156
|
+
"add", "plus", "sum", "total", "combined", "together",
|
|
157
|
+
"in all", "in total", "more", "increased by", "gain",
|
|
158
|
+
"got", "collected", "received", "add up", "accumulate",
|
|
159
|
+
"bring to", "rise by", "grow by", "earned", "pick", "+"
|
|
160
|
+
],
|
|
161
|
+
# subtract
|
|
162
|
+
"-": [
|
|
163
|
+
"subtract", "minus", "less", "difference", "left", "−",
|
|
164
|
+
"remain", "remaining", "take away", "remove", "lost",
|
|
165
|
+
"gave", "spent", "give away", "deduct", "decrease by",
|
|
166
|
+
"fell by", "drop by", "leftover", "popped", "ate", "paid",
|
|
167
|
+
"sold", "sells", "used", "use", "took", "absent", "broke off"
|
|
168
|
+
],
|
|
169
|
+
# multiply
|
|
170
|
+
"*": [
|
|
171
|
+
"multiply", "times", "multiplied by", "product",
|
|
172
|
+
"each", "every", "such", "per box", "per row", "per hour",
|
|
173
|
+
"per week", "half", "double", "triple", "quadruple", "quartet", "twice as many",
|
|
174
|
+
"thrice as many", "x", "such box", "*", "×"
|
|
175
|
+
],
|
|
176
|
+
# divide
|
|
177
|
+
"/": [
|
|
178
|
+
"divide", "divided by", "split", "shared equally",
|
|
179
|
+
"per", "share", "shared", "equal parts", "equal groups",
|
|
180
|
+
"ratio", "quotient", "for each", "out of",
|
|
181
|
+
"for every", "into", "average", "/", "÷"
|
|
182
|
+
],
|
|
183
|
+
# convert
|
|
184
|
+
"=": [
|
|
185
|
+
"inch", "inches",
|
|
186
|
+
"foot", "feet", "ft",
|
|
187
|
+
"yard", "yards", "yd",
|
|
188
|
+
"cm", "centimeter", "centimeters",
|
|
189
|
+
"m", "meter", "meters",
|
|
190
|
+
"mm", "millimeter", "millimeters",
|
|
191
|
+
"week", "weeks",
|
|
192
|
+
"second", "seconds", "minute", "minutes", "min",
|
|
193
|
+
"hour", "hours", "day", "days", "month", "months",
|
|
194
|
+
"year", "years", "yr",
|
|
195
|
+
"km", "kilometer", "kilometers",
|
|
196
|
+
"mile", "miles",
|
|
197
|
+
"ml", "milliliter", "milliliters",
|
|
198
|
+
"l", "liter", "liters",
|
|
199
|
+
"mg", "milligram", "milligrams",
|
|
200
|
+
"g", "gram", "grams",
|
|
201
|
+
"kg", "kilogram", "kilograms",
|
|
202
|
+
"lb", "pound", "pounds",
|
|
203
|
+
"oz", "ounce", "ounces",
|
|
204
|
+
"gallon", "gallons",
|
|
205
|
+
"quart", "quarts",
|
|
206
|
+
"pint", "pints",
|
|
207
|
+
"cup", "cups",
|
|
208
|
+
"dollar", "dollars",
|
|
209
|
+
"cent", "cents",
|
|
210
|
+
"penny", "pennies",
|
|
211
|
+
"nickel", "nickels",
|
|
212
|
+
"dime", "dimes",
|
|
213
|
+
"quarter", "quarters"
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# for SI conversion and CoT
|
|
218
|
+
__units = {
|
|
219
|
+
# distance units (base = meters)
|
|
220
|
+
"inch": 0.0254, "inches": 0.0254,
|
|
221
|
+
"foot": 0.3048, "feet": 0.3048, "ft": 0.3048,
|
|
222
|
+
"yard": 0.9144, "yards": 0.9144, "yd": 0.9144,
|
|
223
|
+
"cm": 0.01, "centimeter": 0.01, "centimeters": 0.01,
|
|
224
|
+
"m": 1.0, "meter": 1.0, "meters": 1.0,
|
|
225
|
+
"mm": 0.001, "millimeter": 0.001, "millimeters": 0.001,
|
|
226
|
+
"km": 1000.0, "kilometer": 1000.0, "kilometers": 1000.0,
|
|
227
|
+
"mile": 1609.344, "miles": 1609.344,
|
|
228
|
+
|
|
229
|
+
# time units (base = seconds)
|
|
230
|
+
"second": 1.0, "seconds": 1.0,
|
|
231
|
+
"minute": 60.0, "minutes": 60.0,
|
|
232
|
+
"hour": 3600.0, "hours": 3600.0,
|
|
233
|
+
"day": 86400.0, "days": 86400.0,
|
|
234
|
+
"week": 604800.0, "weeks": 604800.0,
|
|
235
|
+
"month": 2592000.0, "months": 2592000.0,
|
|
236
|
+
"year": 31536000.0, "years": 31536000.0, "yr": 31536000.0,
|
|
237
|
+
|
|
238
|
+
# mass units (base = kg)
|
|
239
|
+
"mg": 0.000001, "milligram": 0.000001, "milligrams": 0.000001,
|
|
240
|
+
"g": 0.001, "gram": 0.001, "grams": 0.001,
|
|
241
|
+
"kg": 1.0, "kilogram": 1.0, "kilograms": 1.0,
|
|
242
|
+
"lb": 0.45359237, "pound": 0.45359237, "pounds": 0.45359237,
|
|
243
|
+
"oz": 0.0283495231, "ounce": 0.0283495231, "ounces": 0.0283495231,
|
|
244
|
+
|
|
245
|
+
# volume units (base = liter)
|
|
246
|
+
"ml": 0.001, "milliliter": 0.001, "milliliters": 0.001,
|
|
247
|
+
"l": 1.0, "L": 1.0, "liter": 1.0, "liters": 1.0,
|
|
248
|
+
"gallon": 3.78541, "gallons": 3.78541,
|
|
249
|
+
"quart": 0.946353, "quarts": 0.946353,
|
|
250
|
+
"pint": 0.473176, "pints": 0.473176,
|
|
251
|
+
"cup": 0.236588, "cups": 0.236588,
|
|
252
|
+
|
|
253
|
+
# currency units (base = dollar)
|
|
254
|
+
"dollar": 1.0, "dollars": 1.0,
|
|
255
|
+
"cent": 0.01, "cents": 0.01,
|
|
256
|
+
"penny": 0.01, "pennies": 0.01,
|
|
257
|
+
"nickel": 0.05, "nickels": 0.05,
|
|
258
|
+
"dime": 0.10, "dimes": 0.10,
|
|
259
|
+
"quarter": 0.25, "quarters": 0.25
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
def __init__(self, db_filename): # initializes SQL database & SpaCy NLP
|
|
263
|
+
"""
|
|
264
|
+
Initialize the Dynamic-Learning Model (DLM) chatbot.
|
|
265
|
+
|
|
266
|
+
Parameters:
|
|
267
|
+
db_filename (str): The SQLite database file used to train and retrieve
|
|
268
|
+
question-answer-category triples.
|
|
269
|
+
|
|
270
|
+
Behavior:
|
|
271
|
+
- Loads the SpaCy NLP model ('en_core_web_lg').
|
|
272
|
+
- Loads Better-Profanity for profane phrase sensing.
|
|
273
|
+
- Connects to the specified SQLite database file.
|
|
274
|
+
- Ensures the required table structure exists (creates if missing).
|
|
275
|
+
"""
|
|
276
|
+
self.__nlp = spacy.load("en_core_web_lg")
|
|
277
|
+
profanity.load_censor_words()
|
|
278
|
+
self.__filename = db_filename
|
|
279
|
+
self.__create_table_if_missing()
|
|
280
|
+
|
|
281
|
+
def __create_table_if_missing(self): # no return, void
|
|
282
|
+
"""
|
|
283
|
+
Ensure the existence of the 'knowledge_base' table in the SQLite database; create or modify it if necessary.
|
|
284
|
+
|
|
285
|
+
Behavior:
|
|
286
|
+
- Establishes a connection to the SQLite database specified by self.__filename.
|
|
287
|
+
- Creates the 'knowledge_base' table if it does not exist, with the following columns:
|
|
288
|
+
- id (INTEGER, PRIMARY KEY, AUTOINCREMENT)
|
|
289
|
+
- question (TEXT, NOT NULL, UNIQUE)
|
|
290
|
+
- answer (TEXT, NOT NULL)
|
|
291
|
+
- category (TEXT, NOT NULL)
|
|
292
|
+
- If the table already exists but is missing the 'category' column, the method adds it with a default empty string.
|
|
293
|
+
- Used exclusively within the class constructor to ensure the database schema is properly initialized.
|
|
294
|
+
"""
|
|
295
|
+
conn = sqlite3.connect(self.__filename)
|
|
296
|
+
c = conn.cursor()
|
|
297
|
+
# Create table with identifier column if it doesn't exist
|
|
298
|
+
c.execute("""
|
|
299
|
+
CREATE TABLE IF NOT EXISTS knowledge_base (
|
|
300
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
301
|
+
question TEXT NOT NULL UNIQUE,
|
|
302
|
+
answer TEXT NOT NULL,
|
|
303
|
+
category TEXT NOT NULL
|
|
304
|
+
)
|
|
305
|
+
""")
|
|
306
|
+
# If the table existed already without identifier, add it now
|
|
307
|
+
c.execute("PRAGMA table_info(knowledge_base)")
|
|
308
|
+
cols = [row[1] for row in c.fetchall()]
|
|
309
|
+
if 'category' not in cols:
|
|
310
|
+
c.execute("""
|
|
311
|
+
ALTER TABLE knowledge_base
|
|
312
|
+
ADD COLUMN category TEXT NOT NULL DEFAULT ''
|
|
313
|
+
""")
|
|
314
|
+
conn.commit()
|
|
315
|
+
conn.close()
|
|
316
|
+
|
|
317
|
+
def __get_category(self, exact_question): # returns category as a string
|
|
318
|
+
"""
|
|
319
|
+
Retrieve the category (question type) associated with a specific question from the SQLite knowledge base.
|
|
320
|
+
|
|
321
|
+
Parameters:
|
|
322
|
+
exact_question (str): The exact question text used to search the database.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
str or None: The associated category if found (e.g., 'yesno', 'definition'); otherwise, None.
|
|
326
|
+
|
|
327
|
+
Behavior:
|
|
328
|
+
- Connects to the SQLite database.
|
|
329
|
+
- Performs a lookup for the given question.
|
|
330
|
+
- Returns the corresponding category tag if a match exists.
|
|
331
|
+
"""
|
|
332
|
+
conn = sqlite3.connect(self.__filename)
|
|
333
|
+
cursor = conn.cursor()
|
|
334
|
+
cursor.execute(
|
|
335
|
+
"SELECT category FROM knowledge_base WHERE question = ?",
|
|
336
|
+
(exact_question,)
|
|
337
|
+
)
|
|
338
|
+
row = cursor.fetchone()
|
|
339
|
+
conn.close()
|
|
340
|
+
if row:
|
|
341
|
+
return row[0] # this is the category/question_type
|
|
342
|
+
else:
|
|
343
|
+
return None # question not found
|
|
344
|
+
|
|
345
|
+
def __get_specific_question(self, exact_answer): # returns question as a string
|
|
346
|
+
"""
|
|
347
|
+
Retrieve the original question associated with a given answer from the SQLite knowledge base.
|
|
348
|
+
|
|
349
|
+
Parameters:
|
|
350
|
+
exact_answer (str): The exact answer text used to search the database.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
str or None: The corresponding question string if found; otherwise, None.
|
|
354
|
+
|
|
355
|
+
Behavior:
|
|
356
|
+
- Connects to the SQLite database.
|
|
357
|
+
- Searches for a question where the answer matches exactly.
|
|
358
|
+
- Returns the first matching question, or None if no match exists.
|
|
359
|
+
"""
|
|
360
|
+
conn = sqlite3.connect(self.__filename)
|
|
361
|
+
cursor = conn.cursor()
|
|
362
|
+
cursor.execute(
|
|
363
|
+
"SELECT question FROM knowledge_base WHERE answer = ?",
|
|
364
|
+
(exact_answer,)
|
|
365
|
+
)
|
|
366
|
+
row = cursor.fetchone()
|
|
367
|
+
conn.close()
|
|
368
|
+
if row:
|
|
369
|
+
return row[0] # this is the category/question_type
|
|
370
|
+
else:
|
|
371
|
+
return None # question not found
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
# ANSI escape for moving the cursor up N lines
|
|
375
|
+
def __move_cursor_up(lines): # no return, void
|
|
376
|
+
print(f"\033[{lines}A", end="")
|
|
377
|
+
|
|
378
|
+
@staticmethod
|
|
379
|
+
# loading animation for bot thought process
|
|
380
|
+
def __loadingAnimation(user_input, duration): # no return, void
|
|
381
|
+
for seconds in range(0, 3):
|
|
382
|
+
print(f"{'\033[33m'}\r{user_input}{'.' * (seconds + 1)} {'\033[0m'}", end="", flush=True)
|
|
383
|
+
time.sleep(duration)
|
|
384
|
+
print("\n")
|
|
385
|
+
|
|
386
|
+
def __filtered_input(self, userInput): # returns filtered string
|
|
387
|
+
"""
|
|
388
|
+
Filter out filler words from the user input while preserving important context.
|
|
389
|
+
|
|
390
|
+
Parameters:
|
|
391
|
+
userInput (str): The raw, lowercase-converted user query string.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
str: A filtered version of the input string with filler words removed and duplicates eliminated.
|
|
395
|
+
|
|
396
|
+
Behavior:
|
|
397
|
+
- Tokenizes the input into words.
|
|
398
|
+
- Removes filler words unless:
|
|
399
|
+
- It's the first word and part of the exception list.
|
|
400
|
+
- The current mode is 'experimental' and the word is a computation keyword.
|
|
401
|
+
- Preserves word order while removing duplicates.
|
|
402
|
+
- Joins the remaining words back into a single filtered string.
|
|
403
|
+
"""
|
|
404
|
+
# tokenize user input (split into words)
|
|
405
|
+
words = userInput.lower().split()
|
|
406
|
+
|
|
407
|
+
# remove filler words
|
|
408
|
+
filtered_words = []
|
|
409
|
+
for i, word in enumerate(words):
|
|
410
|
+
word_lowered = word.lower()
|
|
411
|
+
|
|
412
|
+
# allow exceptions ONLY in first position:
|
|
413
|
+
if i == 0 and word_lowered in self.__exception_fillers:
|
|
414
|
+
filtered_words.append(word)
|
|
415
|
+
|
|
416
|
+
# otherwise, only keep non-fillers
|
|
417
|
+
else:
|
|
418
|
+
# In experimental mode, keep any computation keyword even if it's also a filler
|
|
419
|
+
is_computation_kw = any(word_lowered == kw.lower()
|
|
420
|
+
for kws in self.__computation_identifiers.values()
|
|
421
|
+
for kw in kws)
|
|
422
|
+
|
|
423
|
+
if word_lowered not in self.__filler_words or (self.__mode == "experimental" and is_computation_kw):
|
|
424
|
+
filtered_words.append(word)
|
|
425
|
+
|
|
426
|
+
# remove duplicates while preserving order (numbers excluded)
|
|
427
|
+
seen = set()
|
|
428
|
+
unique_words = []
|
|
429
|
+
for word in filtered_words:
|
|
430
|
+
try:
|
|
431
|
+
float(word) # Try to treat as number
|
|
432
|
+
unique_words.append(word) # Keep numeric strings (duplicates allowed)
|
|
433
|
+
except ValueError:
|
|
434
|
+
if word not in seen:
|
|
435
|
+
seen.add(word)
|
|
436
|
+
unique_words.append(word)
|
|
437
|
+
|
|
438
|
+
# join the remaining words back into a string
|
|
439
|
+
return " ".join(unique_words)
|
|
440
|
+
|
|
441
|
+
def __set_sentiment_tone(self, orig_input): # no return, void
|
|
442
|
+
"""
|
|
443
|
+
Analyze the original user input and assign an appropriate emotional tone.
|
|
444
|
+
|
|
445
|
+
Parameters:
|
|
446
|
+
orig_input (str): The raw, unfiltered user query.
|
|
447
|
+
|
|
448
|
+
Behavior:
|
|
449
|
+
- Detects aggressive language using profanity filtering.
|
|
450
|
+
- Analyzes punctuation and casing to infer emotional tone such as:
|
|
451
|
+
- 'angry aggressive' for profane content
|
|
452
|
+
- 'angry frustrated' for all-uppercase text
|
|
453
|
+
- 'angry confused' for combined "?" and "!"
|
|
454
|
+
- 'angry excited' for "!" only
|
|
455
|
+
- 'confused unclear' for "?" only
|
|
456
|
+
- 'doubtful uncertain' for ellipses ("..." or "..")
|
|
457
|
+
- Stores the result in self.__tone as a string label.
|
|
458
|
+
"""
|
|
459
|
+
is_profane = profanity.contains_profanity(orig_input)
|
|
460
|
+
if is_profane:
|
|
461
|
+
self.__tone = "angry aggressive"
|
|
462
|
+
elif orig_input == orig_input.upper():
|
|
463
|
+
self.__tone = "angry frustrated"
|
|
464
|
+
elif orig_input.__contains__("?") and orig_input.__contains__("!"):
|
|
465
|
+
self.__tone = "angry confused"
|
|
466
|
+
elif orig_input.__contains__("!"):
|
|
467
|
+
self.__tone = "angry excited"
|
|
468
|
+
elif orig_input.__contains__("?"):
|
|
469
|
+
self.__tone = "confused unclear"
|
|
470
|
+
elif orig_input.__contains__("...") or orig_input.__contains__(".."):
|
|
471
|
+
self.__tone = "doubtful uncertain"
|
|
472
|
+
else:
|
|
473
|
+
self.__tone = ""
|
|
474
|
+
|
|
475
|
+
def __perform_advanced_CoT(self, filtered_query): # no return, void
|
|
476
|
+
"""
|
|
477
|
+
Perform advanced Chain-of-Thought (CoT) reasoning to solve arithmetic or unit conversion problems.
|
|
478
|
+
|
|
479
|
+
Parameters:
|
|
480
|
+
filtered_query (str): The cleaned user input, expected to be a math- or logic-based question.
|
|
481
|
+
|
|
482
|
+
Behavior:
|
|
483
|
+
- Simulates step-by-step reasoning to solve arithmetic word problems without relying on memorized answers.
|
|
484
|
+
- Extracts entities including person names, items, numbers, and operations using SpaCy, NLTK, and regex.
|
|
485
|
+
- Detects arithmetic operations via lexical and semantic matching with predefined keyword sets.
|
|
486
|
+
- Handles both numeric digits and text-based numbers (e.g., "three", "double").
|
|
487
|
+
- Supports simple arithmetic expressions and unit conversions (e.g., inches to cm).
|
|
488
|
+
- Prints the interpreted steps, logical inferences, and the final computed result with contextual explanations.
|
|
489
|
+
- Displays fallback messages if the query is incomplete or too ambiguous to solve.
|
|
490
|
+
"""
|
|
491
|
+
print(
|
|
492
|
+
f"{'\033[33m'}I am presented with a more involved query asking me to do some form of computation{'\033[0m'}")
|
|
493
|
+
self.__loadingAnimation("Let me think about this carefully and break it down so that I can solve it", 0.8)
|
|
494
|
+
self.__loadingAnimation(
|
|
495
|
+
f"I’ve trimmed away any extra words so I’m focusing on \"{filtered_query.title()}\" now", 0.8)
|
|
496
|
+
persons_mentioned = []
|
|
497
|
+
items_mentioned = []
|
|
498
|
+
keywords_mentioned = []
|
|
499
|
+
num_mentioned = []
|
|
500
|
+
operands_mentioned = []
|
|
501
|
+
arithmetic_ending_phrases = [
|
|
502
|
+
"total", "all", "left", "leftover", "remaining", "altogether", "together", "each", "spend", "per",
|
|
503
|
+
"sum", "combined", "add up", "accumulate", "bring to", "rise by", "grow by", "earned", "in all", "in total",
|
|
504
|
+
"difference", "deduct", "decrease by", "fell by", "drop by", "ate",
|
|
505
|
+
"multiply", "times", "product", "received", "pick", "paid", "gave", "pay",
|
|
506
|
+
"split", "shared equally", "equal parts", "equal groups", "ratio", "quotient", "average", "out of", "into"
|
|
507
|
+
]
|
|
508
|
+
filtered_query = filtered_query.title()
|
|
509
|
+
doc = self.__nlp(filtered_query)
|
|
510
|
+
|
|
511
|
+
# Have the bot pick out names mentioned (in order) using SpaCy and NLTK (for maximum coverage)
|
|
512
|
+
for ent in doc.ents:
|
|
513
|
+
if ent.label_ == "PERSON":
|
|
514
|
+
cleaned = re.sub(r'\d+', "", ent.text).strip()
|
|
515
|
+
if cleaned:
|
|
516
|
+
persons_mentioned.append(cleaned)
|
|
517
|
+
|
|
518
|
+
tokens = nltk.word_tokenize(filtered_query)
|
|
519
|
+
for tok in tokens:
|
|
520
|
+
cleaned = re.sub(r"[^a-zA-Z]", "", tok).lower()
|
|
521
|
+
if cleaned in self.__nltk_names:
|
|
522
|
+
persons_mentioned.append(cleaned.capitalize())
|
|
523
|
+
persons_mentioned = {name for name in set(persons_mentioned) if len(name.split()) == 1}
|
|
524
|
+
persons_mentioned = set(persons_mentioned)
|
|
525
|
+
|
|
526
|
+
# Have the bot pick out item names (in order) using SpaCy
|
|
527
|
+
for token in doc:
|
|
528
|
+
if token.pos_ == "PROPN":
|
|
529
|
+
cleaned = re.sub(r'\d+', "", token.text).strip()
|
|
530
|
+
if cleaned and cleaned not in persons_mentioned:
|
|
531
|
+
items_mentioned.append(cleaned)
|
|
532
|
+
items_mentioned = set(items_mentioned)
|
|
533
|
+
|
|
534
|
+
tokens_lower = filtered_query.lower().split()
|
|
535
|
+
last_two = set(tokens_lower[-2:]) # only the final 2 words from filtered input
|
|
536
|
+
|
|
537
|
+
# Then have it find all operand indicating keywords
|
|
538
|
+
found_operand = False
|
|
539
|
+
for fq in filtered_query.split():
|
|
540
|
+
fq_l = fq.lower()
|
|
541
|
+
# If this word is one of the ending phrases and sits among the last five, skip it
|
|
542
|
+
if fq_l in arithmetic_ending_phrases and fq_l in last_two:
|
|
543
|
+
continue
|
|
544
|
+
if fq_l in {"+", "-", "*", "/"}:
|
|
545
|
+
operands_mentioned.append(fq_l)
|
|
546
|
+
continue # move on to the next token
|
|
547
|
+
for operand, keywords in self.__computation_identifiers.items():
|
|
548
|
+
for kw in keywords:
|
|
549
|
+
p1 = self.__nlp(kw)
|
|
550
|
+
p2 = self.__nlp(fq_l)
|
|
551
|
+
word_num_surrounded = re.search(rf'\d+\s*{fq.lower()}\s*\d+', filtered_query.lower())
|
|
552
|
+
|
|
553
|
+
# Direct match or lemma match
|
|
554
|
+
if (kw.lower() == fq.lower()) or p1[0].lemma_ == p2[0].lemma_:
|
|
555
|
+
keywords_mentioned.append(kw.title())
|
|
556
|
+
if kw.lower() == "average":
|
|
557
|
+
operands_mentioned.append("+")
|
|
558
|
+
elif kw.lower() == "out of":
|
|
559
|
+
if word_num_surrounded:
|
|
560
|
+
operands_mentioned.append(operand)
|
|
561
|
+
found_operand = True
|
|
562
|
+
break # only break if 'out of' condition is satisfied
|
|
563
|
+
continue # skip adding 'out of' if not surrounded by numbers
|
|
564
|
+
else:
|
|
565
|
+
operands_mentioned.append(operand)
|
|
566
|
+
found_operand = True
|
|
567
|
+
break
|
|
568
|
+
|
|
569
|
+
# Vector + string similarity
|
|
570
|
+
if p1.vector_norm != 0 and p2.vector_norm != 0 and (
|
|
571
|
+
p1.similarity(p2) > 0.80 and difflib.SequenceMatcher(None, kw, fq_l).ratio() > 0.40):
|
|
572
|
+
keywords_mentioned.append(kw.title())
|
|
573
|
+
if kw.lower() == "average":
|
|
574
|
+
operands_mentioned.append("+")
|
|
575
|
+
elif kw.lower() == "out of":
|
|
576
|
+
if word_num_surrounded:
|
|
577
|
+
operands_mentioned.append(operand)
|
|
578
|
+
found_operand = True
|
|
579
|
+
break
|
|
580
|
+
continue
|
|
581
|
+
else:
|
|
582
|
+
operands_mentioned.append(operand)
|
|
583
|
+
found_operand = True
|
|
584
|
+
break
|
|
585
|
+
|
|
586
|
+
# Fallback: high string similarity
|
|
587
|
+
elif difflib.SequenceMatcher(None, kw, fq_l).ratio() > 0.80:
|
|
588
|
+
keywords_mentioned.append(kw.title())
|
|
589
|
+
if kw.lower() == "average":
|
|
590
|
+
operands_mentioned.append("+")
|
|
591
|
+
elif kw.lower() == "out of":
|
|
592
|
+
if word_num_surrounded:
|
|
593
|
+
operands_mentioned.append(operand)
|
|
594
|
+
found_operand = True
|
|
595
|
+
break
|
|
596
|
+
continue
|
|
597
|
+
else:
|
|
598
|
+
operands_mentioned.append(operand)
|
|
599
|
+
found_operand = True
|
|
600
|
+
break
|
|
601
|
+
|
|
602
|
+
if found_operand:
|
|
603
|
+
found_operand = False
|
|
604
|
+
break
|
|
605
|
+
|
|
606
|
+
# If no operands were found in the main pass, check ending phrases as a last resort
|
|
607
|
+
if not operands_mentioned:
|
|
608
|
+
for fq in filtered_query.split():
|
|
609
|
+
p_fq = self.__nlp(fq)
|
|
610
|
+
|
|
611
|
+
# Replace exact matching with a spaCy similarity check against ending_phrases
|
|
612
|
+
matched_ep = None
|
|
613
|
+
for ep in arithmetic_ending_phrases:
|
|
614
|
+
p_ep = self.__nlp(ep)
|
|
615
|
+
if p_ep.vector_norm != 0 and p_fq.vector_norm != 0 and p_ep.similarity(p_fq) > 0.50:
|
|
616
|
+
matched_ep = ep
|
|
617
|
+
break
|
|
618
|
+
|
|
619
|
+
if not matched_ep:
|
|
620
|
+
continue
|
|
621
|
+
|
|
622
|
+
# Now matched_ep roughly corresponds to an ending phrase; find its operand via spaCy
|
|
623
|
+
for operand, keywords in self.__computation_identifiers.items():
|
|
624
|
+
for kw in keywords:
|
|
625
|
+
p_kw = self.__nlp(kw)
|
|
626
|
+
if p_kw.vector_norm != 0 and p_fq.vector_norm != 0 and p_kw.similarity(p_fq) > 0.70:
|
|
627
|
+
keywords_mentioned.append(kw.title())
|
|
628
|
+
operands_mentioned.append(operand)
|
|
629
|
+
break
|
|
630
|
+
if operands_mentioned:
|
|
631
|
+
break
|
|
632
|
+
if operands_mentioned:
|
|
633
|
+
break
|
|
634
|
+
keywords_mentioned = list(dict.fromkeys(keywords_mentioned))
|
|
635
|
+
|
|
636
|
+
# Now have the bot pick out numbers (in order)
|
|
637
|
+
# additionally, "double", "triple", "quadruple", "half", "an" and "a" also count as numbers, in addition to text numbers (e.g. "three")
|
|
638
|
+
text_nums = ["a", "an", "half", "double", "triple", "quadruple"]
|
|
639
|
+
a_an_detected = False
|
|
640
|
+
|
|
641
|
+
# Combined regex and word match pass
|
|
642
|
+
tokens = filtered_query.lower().split()
|
|
643
|
+
for token in tokens:
|
|
644
|
+
# Check if it's a digit-based number (e.g. 600, 20.5)
|
|
645
|
+
if re.fullmatch(r"\d+(\.\d+)?", token):
|
|
646
|
+
num_mentioned.append(str(float(token)))
|
|
647
|
+
continue
|
|
648
|
+
|
|
649
|
+
# Check if it's a word-based number (e.g. 'three', 'double', 'a')
|
|
650
|
+
try:
|
|
651
|
+
num = w2n.word_to_num(token)
|
|
652
|
+
num_mentioned.append(str(float(num)))
|
|
653
|
+
continue
|
|
654
|
+
except ValueError:
|
|
655
|
+
pass # not a word2num-recognized word
|
|
656
|
+
|
|
657
|
+
# Check if it's in our custom list (a, an, half, double, etc.)
|
|
658
|
+
for t in text_nums:
|
|
659
|
+
p1 = self.__nlp(token)
|
|
660
|
+
p2 = self.__nlp(t)
|
|
661
|
+
if p1[0].lemma_ == p2[0].lemma_:
|
|
662
|
+
if t == "double":
|
|
663
|
+
num_mentioned.append(float(2).__str__())
|
|
664
|
+
elif t == "triple":
|
|
665
|
+
num_mentioned.append(float(3).__str__())
|
|
666
|
+
elif t == "half":
|
|
667
|
+
num_mentioned.append(float(0.5).__str__())
|
|
668
|
+
elif ("=" in operands_mentioned) and (t == "a" or t == "an"):
|
|
669
|
+
a_an_detected = True
|
|
670
|
+
num_mentioned.append(float(1.0).__str__())
|
|
671
|
+
elif t == "quadruple":
|
|
672
|
+
num_mentioned.append(float(4).__str__())
|
|
673
|
+
|
|
674
|
+
# Remove "1.0" if 'a'/'an' was used in an invalid context (like not following "=")
|
|
675
|
+
if a_an_detected and (num_mentioned.count("1.0") > 1 or len(num_mentioned) > 1):
|
|
676
|
+
num_mentioned.remove("1.0")
|
|
677
|
+
|
|
678
|
+
if ('=' in operands_mentioned) and (len(num_mentioned) < 2):
|
|
679
|
+
operands_mentioned.clear()
|
|
680
|
+
operands_mentioned.append('=')
|
|
681
|
+
else:
|
|
682
|
+
if '=' in operands_mentioned:
|
|
683
|
+
operands_mentioned = [op for op in operands_mentioned if op != '=']
|
|
684
|
+
operands_mentioned = list(dict.fromkeys(operands_mentioned))
|
|
685
|
+
|
|
686
|
+
print("\n")
|
|
687
|
+
if any(not lst for lst in (num_mentioned, operands_mentioned)) or (
|
|
688
|
+
'=' not in operands_mentioned and num_mentioned.__len__() < 2): # don't compute if parts are missing
|
|
689
|
+
print(
|
|
690
|
+
f"{self.__loadingAnimation('Hmm', 0.8) or ''}{'\033[34m'}It looks like some essential details are missing, so I can’t complete this calculation right now.{'\033[0m'}")
|
|
691
|
+
else: # else, the bot needs to explain what it has tokenized
|
|
692
|
+
self.__loadingAnimation(
|
|
693
|
+
f"1.) I see {', '.join(persons_mentioned) if persons_mentioned.__len__() >= 1 else 'no one'} mentioned as a person name; "
|
|
694
|
+
f"{'they’re likely key to this problem' if persons_mentioned.__len__() >= 1 else 'moving on'}", 0.2)
|
|
695
|
+
self.__loadingAnimation(
|
|
696
|
+
f"2.) Moreover, I see {', '.join(items_mentioned) if items_mentioned.__len__() >= 1 else 'no items'} mentioned as proper nouns; "
|
|
697
|
+
f"{'this might be a key thing to this problem' if items_mentioned.__len__() >= 1 else 'moving on'}",
|
|
698
|
+
0.2)
|
|
699
|
+
self.__loadingAnimation(
|
|
700
|
+
f"3.) I’ve also identified the numbers {' and '.join(num_mentioned)} that I need to compute with", 0.2)
|
|
701
|
+
self.__loadingAnimation(
|
|
702
|
+
f"4.) I see the keywords \"{'\" and \"'.join(keywords_mentioned)}\", meaning I need to perform a \"{'\" and \"'.join(operands_mentioned)}\" operation for this query; I’ll use that to guide my calculation",
|
|
703
|
+
0.2)
|
|
704
|
+
self.__loadingAnimation("Now I have the parts, so let me put it all together and solve", 0.3)
|
|
705
|
+
# Finally compute it and then give the response (if there is any)
|
|
706
|
+
|
|
707
|
+
# move "originally" numbers to the front
|
|
708
|
+
indicators = {"original", "originally", "initial", "initially", "at first", "to begin with", "had",
|
|
709
|
+
"savings", "saving", "of"}
|
|
710
|
+
|
|
711
|
+
tokens = filtered_query.split()
|
|
712
|
+
temp = None
|
|
713
|
+
# lowercase copy for matching
|
|
714
|
+
lower_tokens = [t.lower() for t in tokens]
|
|
715
|
+
|
|
716
|
+
for idx, token in enumerate(lower_tokens):
|
|
717
|
+
if token in indicators:
|
|
718
|
+
# check token before
|
|
719
|
+
if idx > 0 and token != "of":
|
|
720
|
+
candidate = lower_tokens[idx - 1]
|
|
721
|
+
try:
|
|
722
|
+
temp = (w2n.word_to_num(candidate))
|
|
723
|
+
except ValueError:
|
|
724
|
+
pass
|
|
725
|
+
# check token after
|
|
726
|
+
if idx < len(tokens) - 1:
|
|
727
|
+
candidate = lower_tokens[idx + 1]
|
|
728
|
+
try:
|
|
729
|
+
temp = (w2n.word_to_num(candidate))
|
|
730
|
+
except ValueError:
|
|
731
|
+
pass
|
|
732
|
+
if temp is not None:
|
|
733
|
+
if str(float(temp)) in num_mentioned:
|
|
734
|
+
num_mentioned.remove(str(float(temp)))
|
|
735
|
+
num_mentioned.insert(0, str(float(temp)))
|
|
736
|
+
|
|
737
|
+
# conversion problem
|
|
738
|
+
if len(num_mentioned) == 1 and len(operands_mentioned) == 1:
|
|
739
|
+
try:
|
|
740
|
+
tokens = filtered_query.lower().split()
|
|
741
|
+
num0 = float(num_mentioned[0])
|
|
742
|
+
num_idx = None
|
|
743
|
+
|
|
744
|
+
# redefine text_nums to be a dictionary instead
|
|
745
|
+
text_nums = {
|
|
746
|
+
"a": 1.0,
|
|
747
|
+
"an": 1.0,
|
|
748
|
+
"half": 0.5,
|
|
749
|
+
"double": 2.0,
|
|
750
|
+
"triple": 3.0,
|
|
751
|
+
"quadruple": 4.0
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
# Find index of the numeric token (either digit or w2n‐convertible)
|
|
755
|
+
for i, tok in enumerate(tokens):
|
|
756
|
+
lower_tok = tok.lower()
|
|
757
|
+
|
|
758
|
+
# Check if tok is one of the special words
|
|
759
|
+
if lower_tok in text_nums:
|
|
760
|
+
if text_nums[lower_tok] == num0:
|
|
761
|
+
num_idx = i
|
|
762
|
+
break
|
|
763
|
+
else:
|
|
764
|
+
continue # skip parsing this token further
|
|
765
|
+
|
|
766
|
+
# Otherwise try parsing as a standard float
|
|
767
|
+
try:
|
|
768
|
+
if float(tok) == num0:
|
|
769
|
+
num_idx = i
|
|
770
|
+
break
|
|
771
|
+
except ValueError:
|
|
772
|
+
# If that fails, try converting via w2n.word_to_num
|
|
773
|
+
try:
|
|
774
|
+
if float(w2n.word_to_num(tok)) == num0:
|
|
775
|
+
num_idx = i
|
|
776
|
+
break
|
|
777
|
+
except ValueError:
|
|
778
|
+
continue
|
|
779
|
+
|
|
780
|
+
source_key = None
|
|
781
|
+
target_key = None
|
|
782
|
+
|
|
783
|
+
# Look for the first unit immediately after the number for source key
|
|
784
|
+
if num_idx is not None:
|
|
785
|
+
for tok in tokens[num_idx + 1:]:
|
|
786
|
+
for key, val in self.__units.items():
|
|
787
|
+
p1 = self.__nlp(tok)
|
|
788
|
+
p2 = self.__nlp(key)
|
|
789
|
+
if p1[0].lemma_ == p2[0].lemma_:
|
|
790
|
+
source_key = key
|
|
791
|
+
break
|
|
792
|
+
if source_key:
|
|
793
|
+
break
|
|
794
|
+
|
|
795
|
+
# 3) Now scan the entire sentence for the target-key
|
|
796
|
+
for tok in tokens:
|
|
797
|
+
for key, val in self.__units.items():
|
|
798
|
+
p1 = self.__nlp(tok)
|
|
799
|
+
p2 = self.__nlp(key)
|
|
800
|
+
p3 = self.__nlp(source_key)
|
|
801
|
+
if (p1[0].lemma_ == p2[0].lemma_) and (p2[0].lemma_ != p3[0].lemma_):
|
|
802
|
+
target_key = key
|
|
803
|
+
break
|
|
804
|
+
if target_key:
|
|
805
|
+
break
|
|
806
|
+
|
|
807
|
+
# 4) Compute only if we have both source_key and target_key
|
|
808
|
+
if source_key and target_key:
|
|
809
|
+
result = (num0 * self.__units[source_key]) / self.__units[target_key]
|
|
810
|
+
self.__loadingAnimation(
|
|
811
|
+
f"I need to take {num0} and multiply it by {self.__units[source_key]}. Finally, I divide by {self.__units[target_key]} and I got my answer",
|
|
812
|
+
0.2)
|
|
813
|
+
expr = f"{num_mentioned[0]} {source_key}(s) ==> {round(result, 2)} {target_key}(s)"
|
|
814
|
+
print(f"{'\033[34m'}Conversion Answer: {expr} {'\033[0m'}")
|
|
815
|
+
else:
|
|
816
|
+
print(f"{'\033[33m'}Could not identify both source and target units.{'\033[0m'}")
|
|
817
|
+
except SyntaxError:
|
|
818
|
+
print("\033[33mOops! I still mix up conversions and arithmetic sometimes. Working on it!\033[0m")
|
|
819
|
+
# regular arithmetic operations
|
|
820
|
+
elif len(num_mentioned) >= 2 and (
|
|
821
|
+
len(operands_mentioned) == (len(num_mentioned) - 1) or len(operands_mentioned) == 1):
|
|
822
|
+
# Build a string like "n0 op0 n1 op1 n2 ... op_{N-2} n_{N-1}"
|
|
823
|
+
parts = []
|
|
824
|
+
for i, num in enumerate(num_mentioned):
|
|
825
|
+
parts.append(str(num))
|
|
826
|
+
if i < (len(num_mentioned) - 1) and ("average" in filtered_query.lower()):
|
|
827
|
+
parts.append("+")
|
|
828
|
+
elif i < (len(num_mentioned) - 1) and (len(operands_mentioned) == 1):
|
|
829
|
+
parts.append(operands_mentioned[0])
|
|
830
|
+
elif i < len(operands_mentioned):
|
|
831
|
+
parts.append(operands_mentioned[i])
|
|
832
|
+
expr = " ".join(parts)
|
|
833
|
+
|
|
834
|
+
try:
|
|
835
|
+
result = eval(expr)
|
|
836
|
+
if "average" in filtered_query.lower():
|
|
837
|
+
expr = "(" + expr + ") / " + str(len(num_mentioned))
|
|
838
|
+
result /= len(num_mentioned)
|
|
839
|
+
print(f"{'\033[34m'}Arithmetic Answer: {expr} = {result}{'\033[0m'}")
|
|
840
|
+
except SyntaxError:
|
|
841
|
+
print(
|
|
842
|
+
f"{'\033[34mAh'}, something about that stumped me. I’ll need to learn more to handle it properly.{'\033[0m'}")
|
|
843
|
+
else:
|
|
844
|
+
print(f"{'\033[34m'}{random.choice(self.__fallback_responses)}{'\033[0m'}")
|
|
845
|
+
print(
|
|
846
|
+
f"{'\033[34m'}However, while I was trying to understand the math, I ran into \"{'" and "'.join(keywords_mentioned)}\", which I use to connect keywords to math operations.{'\033[0m'}")
|
|
847
|
+
print(
|
|
848
|
+
f"{'\033[34m'}That might've confused me a bit, maybe try leaving one of those out or rephrase it to make it clearer?{'\033[0m'}")
|
|
849
|
+
|
|
850
|
+
def __generate_thought(self, filtered_query, best_match_question, best_match_answer,
|
|
851
|
+
highest_similarity): # no return, void
|
|
852
|
+
"""
|
|
853
|
+
Simulate a Chain-of-Thought (CoT) reasoning process by printing the bot's internal analysis.
|
|
854
|
+
|
|
855
|
+
Parameters:
|
|
856
|
+
filtered_query (str): The cleaned version of the user's question, stripped of filler or trigger words.
|
|
857
|
+
best_match_question (str): The closest matching question found in the knowledge base.
|
|
858
|
+
best_match_answer (str): The corresponding answer to the matched question.
|
|
859
|
+
highest_similarity (float): The calculated string similarity score (0 to 1) for the match.
|
|
860
|
+
|
|
861
|
+
Behavior:
|
|
862
|
+
- Outputs step-by-step reasoning in a conversational format (e.g., interpreting the question's structure and tone).
|
|
863
|
+
- In experimental mode, calls advanced reasoning (e.g., math parsing or CoT decomposition).
|
|
864
|
+
- Identifies the question's tone, topic, and potential intent based on interrogative words and SpaCy similarity.
|
|
865
|
+
- Displays confidence based on similarity metrics and sets flags for uncertain answers.
|
|
866
|
+
- Uses colorized terminal output and a loading animation to simulate reflective thought.
|
|
867
|
+
"""
|
|
868
|
+
print("\nThought Process (Yellow):")
|
|
869
|
+
if filtered_query is None or filtered_query == "":
|
|
870
|
+
print(
|
|
871
|
+
f"{'\033[33m'}I couldn't pick out any context or clear topic. If I see a match in my database I will respond with that, or else I have no clue!{'\033[0m'}")
|
|
872
|
+
else:
|
|
873
|
+
sentiment_tone = self.__tone.split()
|
|
874
|
+
|
|
875
|
+
if self.__tone != "":
|
|
876
|
+
print(
|
|
877
|
+
f"{'\033[33m'}Right off the bat, the user seems quite {sentiment_tone[0]} or {sentiment_tone[1]} by their query tone. Hopefully I won't disappoint!{'\033[0m'}")
|
|
878
|
+
if self.__mode == "experimental":
|
|
879
|
+
self.__perform_advanced_CoT(filtered_query)
|
|
880
|
+
else:
|
|
881
|
+
interrogative_start = filtered_query.split()[0]
|
|
882
|
+
identifier = filtered_query
|
|
883
|
+
special_start = ["definition", "explanation", "description", "comparison", "calculation", "translation",
|
|
884
|
+
"meaning"] # special word in different form
|
|
885
|
+
for word in special_start:
|
|
886
|
+
identifier = identifier.replace(word, "")
|
|
887
|
+
# collapse any extra spaces
|
|
888
|
+
identifier = " ".join(identifier.split())
|
|
889
|
+
identifier = identifier.split()
|
|
890
|
+
|
|
891
|
+
if " ".join(identifier) == "":
|
|
892
|
+
print(
|
|
893
|
+
f"{'\033[33m'}The user starts their query with \"{interrogative_start.title()}\", but I couldn't pick out a clear topic or context.{'\033[0m'}")
|
|
894
|
+
else:
|
|
895
|
+
print(
|
|
896
|
+
f"{'\033[33m'}The user starts their query with \"{interrogative_start.title()}\" and they are asking about \"{" ".join(identifier).title()}\".{'\033[0m'}")
|
|
897
|
+
self.__loadingAnimation("Let me think about this carefully", 0.8)
|
|
898
|
+
|
|
899
|
+
for s in special_start:
|
|
900
|
+
for u in filtered_query.split():
|
|
901
|
+
s_input = self.__nlp(s)
|
|
902
|
+
u_input = self.__nlp(u)
|
|
903
|
+
if (s_input.vector_norm != 0 and u_input.vector_norm != 0) and (
|
|
904
|
+
s_input.similarity(u_input) > 0.60):
|
|
905
|
+
print(
|
|
906
|
+
f"{'\033[33m'}It seems like they want a {s} of \"{" ".join(identifier).title()}\".{'\033[0m'}")
|
|
907
|
+
|
|
908
|
+
self.__semantic_similarity(self.__special_stripped_query, best_match_question)
|
|
909
|
+
spacy_proceed = self.__nlp_similarity_value is not None
|
|
910
|
+
if (best_match_answer is None) or (highest_similarity < 0.65 and (spacy_proceed and self.__nlp_similarity_value < 0.85)):
|
|
911
|
+
print(f"{'\033[33m'}The closest match is only {int(highest_similarity * 100)}% similar when I used sequence matching.{'\033[0m'}")
|
|
912
|
+
if spacy_proceed:
|
|
913
|
+
print(f"{'\033[33m'}Furthermore, an in-depth vector analysis revealed a similarity percentage of {int(self.__nlp_similarity_value * 100)}%.{'\033[0m'}")
|
|
914
|
+
print(f"{self.__loadingAnimation("Hmm", 0.8) or ''}{'\033[33m'}I don't think I know the answer, so I am going to let the user know that.{'\033[0m'}")
|
|
915
|
+
self.__unsure_while_thinking = True
|
|
916
|
+
else:
|
|
917
|
+
self.__unsure_while_thinking = False
|
|
918
|
+
DB_identifier = self.__get_specific_question(best_match_answer)
|
|
919
|
+
print(f"{'\033[33m'}Yes! I do remember learning about \"{DB_identifier}\" and I might have the right answer!")
|
|
920
|
+
print(f"This is because when I did a sequence similarity calculation to one of the closest match in my database, I found it to be {int(highest_similarity * 100)}% similar.")
|
|
921
|
+
if spacy_proceed:
|
|
922
|
+
print(f"Additionally, doing a more in-depth vector NLP analysis resulted in {int(self.__nlp_similarity_value * 100)}% similarity. Although there are room for error, we will see.{'\033[0m'}")
|
|
923
|
+
self.__loadingAnimation("Let me recall that answer", 0.8)
|
|
924
|
+
print("\n")
|
|
925
|
+
|
|
926
|
+
def __generate_response(self, best_match_answer, best_match_question): # no return, void
|
|
927
|
+
"""
|
|
928
|
+
Generate a dynamic natural language response based on the answer's category.
|
|
929
|
+
|
|
930
|
+
Parameters:
|
|
931
|
+
best_match_answer (str): The stored answer retrieved from the knowledge base.
|
|
932
|
+
best_match_question (str): The matched user question used to derive category context.
|
|
933
|
+
|
|
934
|
+
Behavior:
|
|
935
|
+
- Determines the question's category using internal tagging (e.g., 'yesno', 'process', etc.).
|
|
936
|
+
- Selects a category-specific response template to simulate Natural Language Generation (NLG).
|
|
937
|
+
- Reformats the answer with human-like phrasing and prints it in stylized terminal output.
|
|
938
|
+
- Handles special formatting for categories like definitions, processes, and deadlines.
|
|
939
|
+
- Gracefully handles unrecognized categories or missing data.
|
|
940
|
+
"""
|
|
941
|
+
identifier = self.__get_category(best_match_question)
|
|
942
|
+
BLUE = '\033[34m'
|
|
943
|
+
RESET = '\033[0m'
|
|
944
|
+
|
|
945
|
+
if identifier is None:
|
|
946
|
+
print("Sorry, I encountered an error on my end. Please try again later.")
|
|
947
|
+
return
|
|
948
|
+
|
|
949
|
+
if identifier == "generic":
|
|
950
|
+
print(f"\n{BLUE}{best_match_answer}{RESET}\n")
|
|
951
|
+
|
|
952
|
+
elif identifier == "yesno":
|
|
953
|
+
affirmative_templates = [
|
|
954
|
+
"Yes, {}",
|
|
955
|
+
"Absolutely, {}",
|
|
956
|
+
"Certainly, {}",
|
|
957
|
+
"Indeed, {}",
|
|
958
|
+
"That's right, {}",
|
|
959
|
+
"Correct, {}",
|
|
960
|
+
"You got it, {}",
|
|
961
|
+
"Sure thing, {}",
|
|
962
|
+
"Of course, {}",
|
|
963
|
+
"Definitely, {}",
|
|
964
|
+
"Without a doubt, {}",
|
|
965
|
+
"That's true, {}",
|
|
966
|
+
"Affirmative, {}",
|
|
967
|
+
"Right on, {}",
|
|
968
|
+
"You're spot on, {}",
|
|
969
|
+
"Exactly, {}",
|
|
970
|
+
"Totally, {}",
|
|
971
|
+
"No question about it, {}",
|
|
972
|
+
"100%, {}",
|
|
973
|
+
"I agree, {}"
|
|
974
|
+
]
|
|
975
|
+
negative_templates = [
|
|
976
|
+
"No, {}",
|
|
977
|
+
"Not at all, {}",
|
|
978
|
+
"Unfortunately, {}",
|
|
979
|
+
"Of course not, {}",
|
|
980
|
+
"That's not correct, {}",
|
|
981
|
+
"Actually, no, {}",
|
|
982
|
+
"I'm afraid not, {}",
|
|
983
|
+
"Nope, {}",
|
|
984
|
+
"Sorry, but no, {}",
|
|
985
|
+
"That’s not the case, {}",
|
|
986
|
+
"Negative, {}",
|
|
987
|
+
"Not quite, {}",
|
|
988
|
+
"That’s incorrect, {}",
|
|
989
|
+
"I'm sorry, {}",
|
|
990
|
+
"Absolutely not, {}",
|
|
991
|
+
"Nah, {}",
|
|
992
|
+
"Doesn’t seem so, {}",
|
|
993
|
+
"I wouldn't say that, {}",
|
|
994
|
+
"No way, {}",
|
|
995
|
+
"That’s a no, {}"
|
|
996
|
+
]
|
|
997
|
+
|
|
998
|
+
ans = best_match_answer.strip().lower()
|
|
999
|
+
if ans.startswith(("no", "not", "don't", "do not", "never", "cannot")):
|
|
1000
|
+
template = random.choice(negative_templates)
|
|
1001
|
+
# remove instances of "negative" words to remove redundancy
|
|
1002
|
+
if ans.__contains__("no, "):
|
|
1003
|
+
best_match_answer = best_match_answer.replace("no, ", "", 1)
|
|
1004
|
+
else:
|
|
1005
|
+
best_match_answer = best_match_answer.replace("no ", "", 1)
|
|
1006
|
+
else:
|
|
1007
|
+
template = random.choice(affirmative_templates)
|
|
1008
|
+
# remove instances of "affirmative" words to remove redundancy
|
|
1009
|
+
if ans.__contains__("yes, "):
|
|
1010
|
+
best_match_answer = best_match_answer.replace("yes, ", "", 1)
|
|
1011
|
+
else:
|
|
1012
|
+
best_match_answer = best_match_answer.replace("yes ", "", 1)
|
|
1013
|
+
response = template.format(best_match_answer)
|
|
1014
|
+
print(f"\n{BLUE}{response}{RESET}\n")
|
|
1015
|
+
|
|
1016
|
+
elif identifier == "process": # when training, make sure there are only 3 steps for "process"
|
|
1017
|
+
templates = [
|
|
1018
|
+
"To get started, {}. Then, {}. Finally, {}",
|
|
1019
|
+
"First, {}. Next, {}. Lastly, {}",
|
|
1020
|
+
"Begin by {}. After that, {}. Don't forget to {}.",
|
|
1021
|
+
"Start with {}. Continue by {}. Finish by {}.",
|
|
1022
|
+
"Initially, {}. Then proceed to {}. End with {}.",
|
|
1023
|
+
"Kick things off by {}. Follow it up with {}. Conclude by {}.",
|
|
1024
|
+
"Your first step is to {}. The second step is to {}. The final step is to {}.",
|
|
1025
|
+
"Commence by {}. Subsequently, {}. Ultimately, {}.",
|
|
1026
|
+
"Start off by {}. Then move on to {}. Finally, make sure you {}.",
|
|
1027
|
+
"Begin with {}. Then take care of {}. Lastly, ensure you {}."
|
|
1028
|
+
]
|
|
1029
|
+
steps = best_match_answer.split("; ") # steps must be separated by a semicolon
|
|
1030
|
+
response = random.choice(templates).format(*steps[:3])
|
|
1031
|
+
print(f"\n{BLUE}{response}{RESET}\n")
|
|
1032
|
+
|
|
1033
|
+
elif identifier == "definition":
|
|
1034
|
+
# extract just the term by filtering out common definition triggers
|
|
1035
|
+
raw = best_match_question # e.g. "what definition fafsa"
|
|
1036
|
+
triggers = {
|
|
1037
|
+
"what", "definition", "define", "meaning", "interpret",
|
|
1038
|
+
"what's", "whats", "what is", "what does", "mean", "means",
|
|
1039
|
+
"could", "you", "explain", "describe", "clarify", "tell",
|
|
1040
|
+
"me", "give", "the", "of", "in", "other", "words"
|
|
1041
|
+
}
|
|
1042
|
+
# split on whitespace, drop any trigger words (case‐insensitive)
|
|
1043
|
+
term_words = [w for w in raw.split() if w.lower() not in triggers]
|
|
1044
|
+
term = " ".join(term_words).strip()
|
|
1045
|
+
|
|
1046
|
+
templates = [
|
|
1047
|
+
"\"{0}\" refers to {1}",
|
|
1048
|
+
"By definition, \"{0}\" is {1}",
|
|
1049
|
+
"In simple terms, \"{0}\" means {1}",
|
|
1050
|
+
"\"{0}\" can be described as {1}",
|
|
1051
|
+
"The term \"{0}\" stands for {1}",
|
|
1052
|
+
"Essentially, \"{0}\" is {1}",
|
|
1053
|
+
"\"{0}\" is understood as {1}",
|
|
1054
|
+
"In other words, \"{0}\" is {1}",
|
|
1055
|
+
"To put it simply, \"{0}\" refers to {1}",
|
|
1056
|
+
"\"{0}\" typically means {1}",
|
|
1057
|
+
"When we say \"{0}\", we’re talking about {1}",
|
|
1058
|
+
"\"{0}\" represents {1}",
|
|
1059
|
+
"\"{0}\" is defined as {1}",
|
|
1060
|
+
"You can think of \"{0}\" as {1}"
|
|
1061
|
+
]
|
|
1062
|
+
response = random.choice(templates).format(term, best_match_answer)
|
|
1063
|
+
print(f"\n{BLUE}{response}{RESET}\n")
|
|
1064
|
+
|
|
1065
|
+
elif identifier == "deadline":
|
|
1066
|
+
raw = best_match_question
|
|
1067
|
+
triggers = {
|
|
1068
|
+
"when", "what", "what's", "whats", "when's", "whens",
|
|
1069
|
+
"is", "the", "a", "an",
|
|
1070
|
+
"deadline", "due", "due date", "cutoff", "closing", "closing date",
|
|
1071
|
+
"by", "before", "until",
|
|
1072
|
+
"date", "day", "last", "latest", "final", "damn"
|
|
1073
|
+
}
|
|
1074
|
+
words = raw.split()
|
|
1075
|
+
term_words = [w for w in words if w.lower() not in triggers]
|
|
1076
|
+
term = " ".join(term_words).strip()
|
|
1077
|
+
|
|
1078
|
+
templates = [
|
|
1079
|
+
"The deadline for \"{0}\" is {1}",
|
|
1080
|
+
"You need to submit \"{0}\" by {1}",
|
|
1081
|
+
"Make sure to complete \"{0}\" by {1}",
|
|
1082
|
+
"\"{0}\" is due on {1}",
|
|
1083
|
+
"Don’t forget, \"{0}\" must be done by {1}",
|
|
1084
|
+
"\"{0}\" has a due date of {1}",
|
|
1085
|
+
"Be sure to finish \"{0}\" before {1}",
|
|
1086
|
+
"Please submit \"{0}\" no later than {1}",
|
|
1087
|
+
"\"{0}\" needs to be turned in by {1}",
|
|
1088
|
+
"The final date to complete \"{0}\" is {1}",
|
|
1089
|
+
"Submission for \"{0}\" closes on {1}",
|
|
1090
|
+
"You have until {1} to complete \"{0}\"",
|
|
1091
|
+
"\"{0}\" is expected to be submitted by {1}",
|
|
1092
|
+
"\"{0}\" must be handed in by {1}",
|
|
1093
|
+
"The cutoff for \"{0}\" is {1}"
|
|
1094
|
+
]
|
|
1095
|
+
response = random.choice(templates).format(term, best_match_answer)
|
|
1096
|
+
print(f"\n{BLUE}{response}{RESET}\n")
|
|
1097
|
+
|
|
1098
|
+
elif identifier == "location":
|
|
1099
|
+
templates = [
|
|
1100
|
+
"You can find it at {0}",
|
|
1101
|
+
"It’s located at {0}",
|
|
1102
|
+
"Head over to {0} for more information",
|
|
1103
|
+
"Check it out at {0}",
|
|
1104
|
+
"Access it via {0}",
|
|
1105
|
+
"You’ll find it here: {0}",
|
|
1106
|
+
"It’s available at {0}",
|
|
1107
|
+
"Navigate to {0} to view it",
|
|
1108
|
+
"You can reach it at {0}",
|
|
1109
|
+
"Visit {0} to learn more",
|
|
1110
|
+
"Take a look at {0}",
|
|
1111
|
+
"More details can be found at {0}",
|
|
1112
|
+
"For further info, go to {0}",
|
|
1113
|
+
"To see it yourself, just go to {0}"
|
|
1114
|
+
]
|
|
1115
|
+
best_match_answer = best_match_answer.lower()
|
|
1116
|
+
response = random.choice(templates).format(best_match_answer)
|
|
1117
|
+
print(f"\n{BLUE}{response}{RESET}\n")
|
|
1118
|
+
|
|
1119
|
+
elif identifier == "eligibility":
|
|
1120
|
+
templates = [
|
|
1121
|
+
"Eligibility means {0}",
|
|
1122
|
+
"Eligibility requires that {0}",
|
|
1123
|
+
"Qualifications are met only if {0}",
|
|
1124
|
+
"To be eligible, {0}",
|
|
1125
|
+
"Meeting eligibility involves {0}",
|
|
1126
|
+
"You qualify only if {0}",
|
|
1127
|
+
"Eligibility is based on whether {0}",
|
|
1128
|
+
"In order to qualify, {0}",
|
|
1129
|
+
"You are eligible when {0}",
|
|
1130
|
+
"The requirements are satisfied if {0}",
|
|
1131
|
+
"Eligibility depends on {0}",
|
|
1132
|
+
"To meet the qualifications, {0}",
|
|
1133
|
+
"Being eligible implies that {0}",
|
|
1134
|
+
"You're considered eligible if {0}",
|
|
1135
|
+
"Eligibility conditions include {0}"
|
|
1136
|
+
]
|
|
1137
|
+
best_match_answer = best_match_answer.lower()
|
|
1138
|
+
response = random.choice(templates).format(best_match_answer)
|
|
1139
|
+
print(f"\n{BLUE}{response}{RESET}\n")
|
|
1140
|
+
|
|
1141
|
+
else:
|
|
1142
|
+
print("Cannot retrieve and generate response due to data in unfamiliar category. Please try again later.")
|
|
1143
|
+
|
|
1144
|
+
def __semantic_similarity(self, userInput, knowledgebaseData): # returns True/False
|
|
1145
|
+
"""
|
|
1146
|
+
Evaluate semantic similarity between user input and a stored question using SpaCy vectors.
|
|
1147
|
+
|
|
1148
|
+
Parameters:
|
|
1149
|
+
userInput (str): The cleaned or filtered user query.
|
|
1150
|
+
knowledgebaseData (str): A question stored in the knowledge base to compare against.
|
|
1151
|
+
|
|
1152
|
+
Returns:
|
|
1153
|
+
bool: True if semantic similarity exceeds 0.50 threshold, False otherwise.
|
|
1154
|
+
|
|
1155
|
+
Behavior:
|
|
1156
|
+
- Uses SpaCy's vector-based similarity to compare both texts.
|
|
1157
|
+
- Saves the similarity score internally for optional debugging or reporting.
|
|
1158
|
+
"""
|
|
1159
|
+
if userInput is None or knowledgebaseData is None:
|
|
1160
|
+
return False
|
|
1161
|
+
UI_doc = self.__nlp(userInput)
|
|
1162
|
+
KB_doc = self.__nlp(knowledgebaseData)
|
|
1163
|
+
if UI_doc.vector_norm != 0 and KB_doc.vector_norm != 0:
|
|
1164
|
+
self.__nlp_similarity_value = UI_doc.similarity(KB_doc)
|
|
1165
|
+
return self.__nlp_similarity_value > 0.50
|
|
1166
|
+
else:
|
|
1167
|
+
return False
|
|
1168
|
+
|
|
1169
|
+
def __learn(self, expectation, category): # no return, void
|
|
1170
|
+
"""
|
|
1171
|
+
Store a new question-answer-category entry in the SQLite knowledge base.
|
|
1172
|
+
|
|
1173
|
+
Parameters:
|
|
1174
|
+
expectation (str): The expected answer or response to the current user query.
|
|
1175
|
+
category (str): The type of question (e.g., 'yesno', 'definition', 'process', etc.).
|
|
1176
|
+
|
|
1177
|
+
Behavior:
|
|
1178
|
+
- Inserts the current stripped user query, along with its answer and category,
|
|
1179
|
+
into the SQLite database.
|
|
1180
|
+
- Uses 'INSERT OR IGNORE' to prevent duplicate entries.
|
|
1181
|
+
"""
|
|
1182
|
+
conn = sqlite3.connect(self.__filename)
|
|
1183
|
+
c = conn.cursor()
|
|
1184
|
+
c.execute(
|
|
1185
|
+
"INSERT OR IGNORE INTO knowledge_base (question, answer, category) VALUES (?, ?, ?)",
|
|
1186
|
+
(self.__special_stripped_query, expectation, category)
|
|
1187
|
+
)
|
|
1188
|
+
conn.commit()
|
|
1189
|
+
conn.close()
|
|
1190
|
+
|
|
1191
|
+
def __login_verification(self, mode): # no return, void
|
|
1192
|
+
"""
|
|
1193
|
+
Verify and initialize the selected access mode (Training, Commercial, or Experimental).
|
|
1194
|
+
|
|
1195
|
+
Parameters:
|
|
1196
|
+
mode (str): The access mode. Options are:
|
|
1197
|
+
't' - Training mode (requires password and shows training guidelines)
|
|
1198
|
+
'c' - Commercial mode (no training features, read-only usage)
|
|
1199
|
+
'e' - Experimental mode (enables reasoning features, no DB writes)
|
|
1200
|
+
|
|
1201
|
+
Behavior:
|
|
1202
|
+
- If mode is 't', prompts for a password and displays mandatory training instructions.
|
|
1203
|
+
- If mode is 'c', enters Commercial mode without training privileges.
|
|
1204
|
+
- If mode is 'e', proceeds with Experimental mode with reasoning capabilities.
|
|
1205
|
+
"""
|
|
1206
|
+
if mode.lower() == "t":
|
|
1207
|
+
password = input("Enter the password to enter Training Mode: ")
|
|
1208
|
+
while password != self.__trainingPwd:
|
|
1209
|
+
password = input(
|
|
1210
|
+
"Password is incorrect, try again or type 'stop' to enter in commercial mode instead: ")
|
|
1211
|
+
if password.lower() == "stop":
|
|
1212
|
+
self.__mode = "commercial"
|
|
1213
|
+
print("\n")
|
|
1214
|
+
self.__loadingAnimation("Logging in as Commercial User", 0.6)
|
|
1215
|
+
print("\n")
|
|
1216
|
+
break
|
|
1217
|
+
if password == self.__trainingPwd:
|
|
1218
|
+
# trainers must understand these rules as DLM can generate bad responses if these instructions are neglected
|
|
1219
|
+
print(
|
|
1220
|
+
f"\n\n{'\033[31m'}MAKE SURE TO UNDERSTAND THE FOLLOWING ANSWER FORMAT EXPECTED FOR EACH CATEGORY FOR THE BOT TO LEARN ACCURATELY:{'\033[0m'}\n")
|
|
1221
|
+
print("*'yesno': Make sure to start your answer responses with \"yes\" or \"no\" ONLY")
|
|
1222
|
+
print(
|
|
1223
|
+
"*'process': Each answer must have three steps for your responses, separated by \";\" (semicolon)")
|
|
1224
|
+
print(
|
|
1225
|
+
"*'definition': Make sure to not mention the WORD/PHRASE to be defined & always start your response here with \"the\" only")
|
|
1226
|
+
print("*'deadline': Only include the deadline date, as an example, \"March 31st 2025\"")
|
|
1227
|
+
print("*'location': Mention the location only, nothing else. For example, \"The FAFSA.Gov website\"")
|
|
1228
|
+
print("*'generic': Format doesn't matter for this, give your answer in any comprehensive format")
|
|
1229
|
+
print(
|
|
1230
|
+
"*'eligibility': Make sure to ONLY start the response with a pronoun like \"you\", \"they\", \"he\", \"she\", etc\n\n")
|
|
1231
|
+
|
|
1232
|
+
confirmation = input(
|
|
1233
|
+
"Make sure to understand and note these instructions somewhere as the generated responses would get corrupt otherwise.\nType 'Y' if you understood: ")
|
|
1234
|
+
while confirmation.lower() != "y": # trainers must understand the instructions above
|
|
1235
|
+
confirmation = input(
|
|
1236
|
+
"You cannot proceed to train without understanding the instructions aforementioned. Type 'Y' to continue: ")
|
|
1237
|
+
self.__mode = "training"
|
|
1238
|
+
print("\n")
|
|
1239
|
+
self.__loadingAnimation("Logging in as Trainer", 0.6)
|
|
1240
|
+
print("\n")
|
|
1241
|
+
elif mode.lower() == "c":
|
|
1242
|
+
self.__mode = "commercial"
|
|
1243
|
+
self.__loadingAnimation("Logging in as Commercial User", 0.6)
|
|
1244
|
+
print("Ask Non-Computational Queries (switch to experimental for computational queries)")
|
|
1245
|
+
else:
|
|
1246
|
+
self.__mode = "experimental"
|
|
1247
|
+
self.__loadingAnimation("Logging in as Experimental", 0.6)
|
|
1248
|
+
print("Ask Computational Problems (Arithmetics or Conversions)")
|
|
1249
|
+
|
|
1250
|
+
def ask(self, mode): # no return, void
|
|
1251
|
+
"""
|
|
1252
|
+
Handle a full user interaction loop with the DLM bot.
|
|
1253
|
+
NOTICE: To make the bot run continuously, implement a loop in your program
|
|
1254
|
+
|
|
1255
|
+
Parameters:
|
|
1256
|
+
mode (str): The access mode. Options:
|
|
1257
|
+
't' for Training mode,
|
|
1258
|
+
'c' for Commercial mode,
|
|
1259
|
+
'e' for Experimental mode.
|
|
1260
|
+
|
|
1261
|
+
Behavior:
|
|
1262
|
+
- Prompts the user for input.
|
|
1263
|
+
- Detects tone, filters input, searches knowledge base.
|
|
1264
|
+
- Performs Chain-of-Thought (CoT) while recalling learnt answer.
|
|
1265
|
+
- If match is found, generates a response.
|
|
1266
|
+
- If in training mode and answer is incorrect or not found, prompts user to teach the bot.
|
|
1267
|
+
- In experimental mode, performs reasoning or arithmetic without using database.
|
|
1268
|
+
"""
|
|
1269
|
+
if self.__singlePassthrough:
|
|
1270
|
+
self.__login_verification(mode)
|
|
1271
|
+
self.__singlePassthrough = False
|
|
1272
|
+
|
|
1273
|
+
if self.__mode == "training":
|
|
1274
|
+
print("\nTRAINING MODE")
|
|
1275
|
+
elif self.__mode == "commercial":
|
|
1276
|
+
print("\n\nCOMMERCIAL MODE")
|
|
1277
|
+
else:
|
|
1278
|
+
print("\n\nEXPERIMENTAL MODE") # for experimental, there are no data saving in DB
|
|
1279
|
+
self.__query = input("DLM Bot here, ask away: ")
|
|
1280
|
+
|
|
1281
|
+
while self.__query is None or self.__query == "":
|
|
1282
|
+
self.__query = input("Empty input is unacceptable. Please enter something: ")
|
|
1283
|
+
|
|
1284
|
+
self.__set_sentiment_tone(self.__query) # sets global variable sentiment tone
|
|
1285
|
+
|
|
1286
|
+
# storing the user-query (filtered, lower-case, no punctuation)
|
|
1287
|
+
if self.__mode == "experimental":
|
|
1288
|
+
# We want to keep the following
|
|
1289
|
+
keep = {".", "+", "-", "*", "/", "="}
|
|
1290
|
+
to_remove = "".join(ch for ch in string.punctuation if ch not in keep)
|
|
1291
|
+
else:
|
|
1292
|
+
to_remove = string.punctuation
|
|
1293
|
+
|
|
1294
|
+
translation_table = str.maketrans("", "", to_remove)
|
|
1295
|
+
filtered_query = self.__filtered_input(
|
|
1296
|
+
self.__query.lower().translate(translation_table)
|
|
1297
|
+
)
|
|
1298
|
+
|
|
1299
|
+
# match_query is the query without special words to prevent interference with SpaCy similarity
|
|
1300
|
+
self.__special_stripped_query = filtered_query
|
|
1301
|
+
special_exceptions = ["definition", "explanation", "description", "comparison", "calculation", "translation",
|
|
1302
|
+
"meaning"]
|
|
1303
|
+
for word in special_exceptions:
|
|
1304
|
+
self.__special_stripped_query = self.__special_stripped_query.replace(word, "")
|
|
1305
|
+
# collapse any extra spaces
|
|
1306
|
+
self.__special_stripped_query = " ".join(self.__special_stripped_query.split())
|
|
1307
|
+
|
|
1308
|
+
conn = sqlite3.connect(self.__filename)
|
|
1309
|
+
cursor = conn.cursor()
|
|
1310
|
+
cursor.execute("SELECT question, answer FROM knowledge_base")
|
|
1311
|
+
rows = cursor.fetchall()
|
|
1312
|
+
conn.close()
|
|
1313
|
+
|
|
1314
|
+
highest_similarity = 0.0
|
|
1315
|
+
best_match_question = None
|
|
1316
|
+
best_match_answer = None
|
|
1317
|
+
|
|
1318
|
+
for stored_question, stored_answer in rows:
|
|
1319
|
+
# compare against both versions of the user query
|
|
1320
|
+
sim_stripped = difflib.SequenceMatcher(None, stored_question, self.__special_stripped_query).ratio()
|
|
1321
|
+
sim_filtered = difflib.SequenceMatcher(None, stored_question, filtered_query).ratio()
|
|
1322
|
+
# pick the higher
|
|
1323
|
+
sim = max(sim_stripped, sim_filtered)
|
|
1324
|
+
|
|
1325
|
+
if sim > highest_similarity:
|
|
1326
|
+
highest_similarity = sim
|
|
1327
|
+
best_match_question = stored_question
|
|
1328
|
+
best_match_answer = stored_answer
|
|
1329
|
+
|
|
1330
|
+
# "Chain of Thought" (CoT) Feature
|
|
1331
|
+
self.__generate_thought(filtered_query, best_match_question, best_match_answer, highest_similarity)
|
|
1332
|
+
|
|
1333
|
+
# accept a match if highest_similarity is 65% or more, or if semantic similarity is recognized
|
|
1334
|
+
if self.__mode != "experimental":
|
|
1335
|
+
if (not self.__unsure_while_thinking) and ((highest_similarity >= 0.65) or (
|
|
1336
|
+
best_match_answer and self.__semantic_similarity(self.__special_stripped_query,
|
|
1337
|
+
best_match_question))):
|
|
1338
|
+
self.__unsure_while_thinking = False # reset this back to default for next iteration
|
|
1339
|
+
self.__generate_response(best_match_answer, best_match_question)
|
|
1340
|
+
if self.__mode == "training":
|
|
1341
|
+
self.__expectation = input("Is this what you expected (Y/N): ")
|
|
1342
|
+
|
|
1343
|
+
while not self.__expectation: # if nothing entered, ask until question answered
|
|
1344
|
+
self.__expectation = input("Empty input is unacceptable. Is this what you expected (Y/N): ")
|
|
1345
|
+
|
|
1346
|
+
if self.__expectation.lower() == "y":
|
|
1347
|
+
print("Great!")
|
|
1348
|
+
return
|
|
1349
|
+
else:
|
|
1350
|
+
return
|
|
1351
|
+
|
|
1352
|
+
# only executes if training option is TRUE
|
|
1353
|
+
if self.__mode == "training":
|
|
1354
|
+
self.__expectation = input(
|
|
1355
|
+
"I'm not sure. Train me with the expected response: ") # train DLM with answer
|
|
1356
|
+
while not self.__expectation:
|
|
1357
|
+
print("Nothing learnt. Moving on.")
|
|
1358
|
+
return
|
|
1359
|
+
self.__category = input(
|
|
1360
|
+
"Which category does that question/answer belong to (yesno, process, definition, deadline, location, generic, eligibility): ").lower()
|
|
1361
|
+
|
|
1362
|
+
# used for generated response template
|
|
1363
|
+
category_options = ["yesno", "process", "definition", "deadline", "location", "generic", "eligibility"]
|
|
1364
|
+
|
|
1365
|
+
while not self.__category or self.__category not in category_options:
|
|
1366
|
+
self.__category = input("You MUST give an appropriate category for the question/answer: ").lower()
|
|
1367
|
+
|
|
1368
|
+
self.__learn(self.__expectation,
|
|
1369
|
+
self.__category) # learn this new question and answer pair and add to knowledgebase
|
|
1370
|
+
print("I learned something new!") # confirmation that it went through the whole process
|
|
1371
|
+
else: # only executes when in commercial mode and bot cannot find the answer
|
|
1372
|
+
print(f"{'\033[34m'}{random.choice(self.__fallback_responses)}{'\033[0m'}")
|