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 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'}")