Files
hq_bot/question.py
2020-06-23 23:17:05 -05:00

147 lines
6.3 KiB
Python
Raw Blame History

import itertools
import re
import time
from collections import defaultdict
from PyQt5.QtCore import pyqtSignal, QObject
from unidecode import unidecode
import search
punctuation_to_none = str.maketrans({key: None for key in "!\"#$%&\'()*+,-.:;<=>?@[\\]^_`{|}~<7E>"})
punctuation_to_space = str.maketrans({key: " " for key in "!\"#$%&\'()*+,-.:;<=>?@[\\]^_`{|}~<7E>"})
class QuestionHandler(QObject):
searching = pyqtSignal([str, str, list, list])
googleResults = pyqtSignal([dict, bool])
pageResults = pyqtSignal([dict, bool])
async def answer_question(self, qn, question, original_answers):
print("Searching")
start = time.time()
question = unidecode(question)
answers = []
for ans in original_answers:
ans = unidecode(ans)
answers.append(ans.translate(punctuation_to_none))
#answers.append(ans.translate(punctuation_to_space))
answers = list(dict.fromkeys(answers))
print('Possible Answers: {}'.format(answers))
self.searching.emit(str(qn), question, original_answers, answers)
reverse = True
if ("NOT" in question and '"NOT"' not in question):
question = re.sub(r'\bNOT\b', ' ', question)
elif ("least" in question.lower() and "at least" not in question.lower() and '"least"' not in question.lower()):
question = re.sub(r'\bleast\b', ' most ', question)
elif ("fewest" in question and '"fewest"' not in question):
question = re.sub(r'\bfewest\b', ' most ', question)
elif ("NEVER" in question and '"NEVER"' not in question):
question = re.sub(r'\bNOT\b', ' always ', question)
elif ("NON" in question and '"NON"' not in question):
question = re.sub(r'\bNON', ' ', question)
else:
reverse = False
question_lower = question.lower().replace('"', '').replace("'", '"')
question_lower = question_lower.replace('which of these', 'what').replace('which', 'what')
"""quoted = re.findall('"([^"]*)"', question_lower) # Get all words in quotes
no_quote = question_lower
for quote in quoted:
no_quote = no_quote.replace(f"\"{quote}\"", "1placeholder1")
question_keywords = search.find_keywords(no_quote)
for quote in quoted:
question_keywords[question_keywords.index("1placeholder1")] = quote"""
#if 'which' in question_lower:
#question_lower += ' ({})'.format(' OR '.join(answers))
print('Search: "{}"'.format(question_lower))
smart_data, search_results = await search.search_google(question_lower, 5)
print('Smart Data: {}'.format(smart_data))
print('Search Results: {}'.format(search_results))
print('Searching smart Google data and website previews...')
search_text = [search.clean_html(x).lower().translate(punctuation_to_none) for x in smart_data]
counts = await self._search_method1(search_text, answers, reverse)
if counts:
self.googleResults.emit(counts, reverse)
else:
counts = await self._search_method2(search_text, answers, reverse)
print(counts)
if counts:
self.googleResults.emit(counts, reverse)
print('GOOGLE THINKS: {}'.format(min(counts, key=counts.get) if reverse else max(counts, key=counts.get)))
else:
self.googleResults.emit({k: 0 for k in answers}, False)
print('GOOGLE is not sure.')
print()
print('Searching full websites provided by Google...')
search_text = [x.translate(punctuation_to_none) for x in await search.get_clean_texts(search_results)]
counts = await self._search_method1(search_text, answers, reverse)
if counts:
self.pageResults.emit(counts, reverse)
else:
counts = await self._search_method2(search_text, answers, reverse)
if counts:
self.pageResults.emit(counts, reverse)
print('WEBSITES THINK: {}'.format(min(counts, key=counts.get) if reverse else max(counts, key=counts.get)))
else:
self.pageResults.emit({k: 0 for k in answers}, False)
print('WEBSITES are not sure.')
print(f"Search took {time.time() - start} seconds")
return ""
async def _search_method1(self, texts, answers, reverse):
"""
Returns the answer with the maximum/minimum number of exact occurrences in the texts.
:param texts: List of text to analyze
:param answers: List of answers
:param reverse: True if the best answer occurs the least, False otherwise
:return: Answer that occurs the most/least in the texts, empty string if there is a tie
"""
print("Running method 1")
counts = {answer.lower(): 0 for answer in answers}
for text in texts:
for answer in counts:
counts[answer] += len(re.findall(f"\\b{answer}\\b", text))
print(counts)
# If not all answers have count of 0 and the best value doesn't occur more than once, return the best answer
best_value = min(counts.values()) if reverse else max(counts.values())
if not all(c == 0 for c in counts.values()) and list(counts.values()).count(best_value) == 1:
return counts
return ""
async def _search_method2(self, texts, answers, reverse):
"""
Return the answer with the maximum/minimum number of keyword occurrences in the texts.
:param texts: List of text to analyze
:param answers: List of answers
:param reverse: True if the best answer occurs the least, False otherwise
:return: Answer whose keywords occur most/least in the texts
"""
print("Running method 2")
counts = {answer: {keyword: 0 for keyword in search.find_keywords(answer)} for answer in answers}
for text in texts:
for keyword_counts in counts.values():
for keyword in keyword_counts:
keyword_counts[keyword] += len(re.findall(f" {keyword} ", text))
print(counts)
counts_sum = {answer: sum(keyword_counts.values()) for answer, keyword_counts in counts.items()}
if not all(c == 0 for c in counts_sum.values()):
return counts_sum
return ""