Ever feel overwhelmed while learning something new? Like you're drowning in information but not actually absorbing anything? We've all been there. Wouldn't it be awesome to have a personalized study companion that understands your level and explains things in a way that clicks? That's exactly what we're going to build together.
This tutorial will show you how to combine the ). It also sets up a UserSession to keep track of the interaction. The generate_prompt method takes the user's request and transforms it into a prompt the API can understand. Finally, generate_text_response sends the prompt to the API and retrieves the AI-generated answer.
Smooth and Responsive: GenerateResponseThread
To avoid making the user wait while the AI is thinking, we'll use a separate thread for API calls:
class GenerateResponseThread(QThread):
finished = pyqtSignal(str)
def __init__(self, assistant, request):
super().__init__()
self.assistant = assistant
self.request = request
def run(self):
response = self.assistant.generate_text_response(self.request)
self.finished.emit(response)
This GenerateResponseThread, based on PyQt5's QThread, runs the API request in the background, ensuring that the user interface remains responsive.
Personalizing the Experience
Everyone learns differently. To cater to individual preferences, we'll create a PreferencesDialog:
class PreferencesDialog(QDialog):
def __init__(self, parent=None, preferences=None):
super().__init__(parent)
self.setWindowTitle("Preferences")
self.preferences = preferences or {}
layout = QVBoxLayout()
form_layout = QFormLayout()
self.tone_combo = QComboBox()
self.tone_combo.addItems(["Formal", "Informal"])
form_layout.addRow("Tone of Voice:", self.tone_combo)
self.length_spin = QSpinBox()
self.length_spin.setMinimum(50)
self.length_spin.setMaximum(1000)
self.length_spin.setSingleStep(50)
form_layout.addRow("Response Length (words):", self.length_spin)
self.examples_check = QCheckBox("Include Examples")
form_layout.addRow(self.examples_check)
layout.addLayout(form_layout)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
self.setLayout(layout)
self.set_initial_values()
def set_initial_values(self):
if self.preferences:
self.tone_combo.setCurrentText(self.preferences.get("tone_of_voice", "Formal"))
self.length_spin.setValue(self.preferences.get("response_length", 200))
self.examples_check.setChecked(self.preferences.get("include_examples", True))
def get_preferences(self):
return {
"tone_of_voice": self.tone_combo.currentText(),
"response_length": self.length_spin.value(),
"include_examples": self.examples_check.isChecked(),
"learning_style": self.preferences.get("learning_style", "Visual"),
"topics_of_interest": self.preferences.get("topics_of_interest", [])
}
This dialog allows users to customize settings like the AI's tone of voice, the desired response length, and whether to include examples. This level of customization ensures a more engaging and effective learning experience.
Building the Interface
Finally, let's create the user interface with the EducationalAssistantGUI class:
class EducationalAssistantGUI(QWidget):
def __init__(self):
super().__init__()
self.assistant = EducationalAssistant()
self.loading_movie = QMovie("path/to/loading.gif")
self.initUI()
self.history = []
self.history_file = "chat_history.json"
self.load_history()
self.assistant.session.preferences = self.load_preferences()
def initUI(self):
self.setWindowTitle("Educational Assistant")
layout = QVBoxLayout()
tabs = QTabWidget()
chat_tab = QWidget()
history_tab = QWidget()
tabs.addTab(chat_tab, "Chat")
tabs.addTab(history_tab, "History")
chat_layout = QVBoxLayout()
query_group = QGroupBox("Query")
query_layout = QVBoxLayout()
self.query_text = QTextEdit()
query_layout.addWidget(self.query_text)
query_group.setLayout(query_layout)
layout.addWidget(query_group)
level_group = QGroupBox("User Level")
level_layout = QHBoxLayout()
self.level_selection = QButtonGroup()
beginner_button = QRadioButton("Beginner")
beginner_button.setChecked(True)
intermediate_button = QRadioButton("Intermediate")
advanced_button = QRadioButton("Advanced")
self.level_selection.addButton(beginner_button)
self.level_selection.addButton(intermediate_button)
self.level_selection.addButton(advanced_button)
level_layout.addWidget(beginner_button)
level_layout.addWidget(intermediate_button)
level_layout.addWidget(advanced_button)
level_group.setLayout(level_layout)
layout.addWidget(level_group)
self.generate_button = QPushButton("Generate Response")
self.generate_button.clicked.connect(self.generate_response)
layout.addWidget(self.generate_button)
response_group = QGroupBox("Response")
response_layout = QVBoxLayout()
self.response_text = QTextEdit()
self.response_text.setReadOnly(True)
response_layout.addWidget(self.response_text)
self.loading_label = QLabel()
self.loading_label.setMovie(self.loading_movie)
self.loading_label.setAlignment(Qt.AlignCenter)
self.loading_label.hide()
response_layout.addWidget(self.loading_label)
response_group.setLayout(response_layout)
layout.addWidget(response_group)
preferences_group = QGroupBox("Preferences")
preferences_layout = QVBoxLayout()
learning_style_label = QLabel("Learning Style:")
self.learning_style_combo = QComboBox()
self.learning_style_combo.addItems(
["Visual", "Auditory", "Kinesthetic", "Reading/Writing"])
preferences_layout.addWidget(learning_style_label)
preferences_layout.addWidget(self.learning_style_combo)
self.learning_style_combo.currentIndexChanged.connect(self.update_preferences)
topics_label = QLabel("Topics of Interest:")
self.topics_checkboxes = {}
topics = ["Mathematics", "Science", "History", "Programming", "Art"]
for topic in topics:
checkbox = QCheckBox(topic)
self.topics_checkboxes[topic] = checkbox
checkbox.stateChanged.connect(self.update_preferences)
preferences_layout.addWidget(checkbox)
preferences_group.setLayout(preferences_layout)
chat_layout.addWidget(preferences_group)
preferences_button = QPushButton("Preferences")
preferences_button.clicked.connect(self.open_preferences_dialog)
chat_layout.addWidget(preferences_button)
chat_tab.setLayout(chat_layout)
history_layout = QVBoxLayout()
search_group = QGroupBox("Search/Filter")
search_layout = QHBoxLayout()
self.search_text = QLineEdit()
self.search_text.setPlaceholderText("Search...")
self.search_text.textChanged.connect(self.filter_history)
self.date_from = QDateEdit(calendarPopup=True)
self.date_from.setDate(datetime.date(2023, 1, 1))
self.date_to = QDateEdit(calendarPopup=True)
self.date_to.setDate(datetime.date.today())
self.date_from.dateChanged.connect(self.filter_history)
self.date_to.dateChanged.connect(self.filter_history)
search_layout.addWidget(QLabel("Keyword:"))
search_layout.addWidget(self.search_text)
search_layout.addWidget(QLabel("From:"))
search_layout.addWidget(self.date_from)
search_layout.addWidget(QLabel("To:"))
search_layout.addWidget(self.date_to)
search_group.setLayout(search_layout)
history_layout.addWidget(search_group)
self.history_list = QListWidget()
history_layout.addWidget(self.history_list)
history_tab.setLayout(history_layout)
self.history_list.itemDoubleClicked.connect(self.load_history_item)
save_button = QPushButton("Save History")
save_button.clicked.connect(self.save_history)
load_button = QPushButton("Load History")
load_button.clicked.connect(self.load_history_from_file)
history_layout.addWidget(save_button)
history_layout.addWidget(load_button)
export_button = QPushButton("Export History")
export_button.clicked.connect(self.export_history)
history_layout.addWidget(export_button)
history_tab.setLayout(history_layout)
self.loading_label = QLabel()
self.loading_label.setMovie(self.loading_movie)
self.loading_label.setAlignment(Qt.AlignCenter)
self.loading_label.hide()
response_layout.addWidget(self.loading_label)
layout.addWidget(tabs)
self.setLayout(layout)
def generate_response(self):
query = self.query_text.toPlainText()
if not query.strip():
QMessageBox.warning(self, "Error", "Please enter a query.")
return
level = 'beginner' if self.level_selection.buttons()[0].isChecked() else \
'intermediate' if self.level_selection.buttons()[1].isChecked() else "advanced"
preferences = self.assistant.session.preferences
request = UserRequest(query=query, user_level=level, preferences=preferences)
self.generate_button.setEnabled(False)
self.loading_label.show()
self.loading_movie.start()
self.thread = GenerateResponseThread(self.assistant, request)
self.thread.finished.connect(self.display_response)
self.thread.start()
self.response_text.setText("Loading...")
def display_response(self, response):
self.generate_button.setEnabled(True)
self.loading_label.hide()
self.loading_movie.stop()
if "Error generating text response:" in response:
QMessageBox.critical(self, "Error", response.replace("Error generating text response:", ""))
self.response_text.clear()
else:
self.response_text.setText(response)
self.update_history(self.query_text.toPlainText(), response)
self.query_text.clear()
def update_preferences(self):
preferences = {}
preferences["learning_style"] = self.learning_style_combo.currentText()
selected_topics = [topic for topic, checkbox in self.topics_checkboxes.items() if checkbox.isChecked()]
preferences["topics_of_interest"] = selected_topics
self.assistant.session.preferences = preferences
self.save_preferences()
def save_preferences(self):
try:
with open("user_preferences.json", "w") as f:
json.dump(self.assistant.session.preferences, f, indent=4)
except Exception as e:
print(f"Error saving preferences: {e}")
def load_preferences(self):
try:
with open("user_preferences.json", "r") as f:
return json.load(f)
except FileNotFoundError:
return {}
except json.JSONDecodeError as e:
print(f"Error loading preferences: {e}")
return {}
def open_preferences_dialog(self):
dialog = PreferencesDialog(self, self.assistant.session.preferences)
if dialog.exec_() == QDialog.Accepted:
self.assistant.session.preferences = dialog.get_preferences()
self.save_preferences()
def update_history(self, query, response):
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
item = QListWidgetItem(f"{timestamp} - {query[:20]}...")
item.setData(Qt.UserRole, {"query": query, "response": response})
self.history_list.addItem(item)
self.history.append({"query": query, "response": response, "timestamp": timestamp})
def load_history_item(self, item):
data = item.data(Qt.UserRole)
self.query_text.setText(data["query"])
self.response_text.setText(data["response"])
def save_history(self):
try:
with open(self.history_file, "w") as f:
json.dump(self.history, f, indent=4)
except Exception as e:
print(f"Error saving history: {e}")
def load_history(self):
try:
with open(self.history_file, "r") as f:
self.history = json.load(f)
self.update_history_list()
except FileNotFoundError:
pass
except json.JSONDecodeError as e:
print(f"Error loading history: Invalid JSON format: {e}")
def load_history_from_file(self):
options = QFileDialog.Options()
filename, _ = QFileDialog.getOpenFileName(self, "Load Chat History", "", "JSON Files (*.json);;All Files (*)",
options=options)
if filename:
try:
with open(filename, "r") as f:
self.history = json.load(f)
self.update_history_list()
except json.JSONDecodeError as e:
print(f"Error loading history from file: Invalid JSON format: {e}")
def filter_history(self):
self.update_history_list(filter_text=self.search_text.text(),
date_from=self.date_from.date().toPyDate(),
date_to=self.date_to.date().toPyDate())
def export_history(self):
options = QFileDialog.Options()
filename, _ = QFileDialog.getSaveFileName(self, "Export Chat History", "",
"Text Files (*.txt);;CSV Files (*.csv)", options=options)
if filename:
try:
with open(filename, "w", encoding="utf-8") as f:
if filename.endswith(".csv"):
f.write("Timestamp,Query,Response\n")
for item in self.history:
timestamp = item.get("timestamp", "")
query = item.get("query", "").replace(",", "\",\"")
response = item.get("response", "").replace(",", "\",\"")
f.write(f'"{timestamp}","{query}","{response}"\n')
else:
for item in self.history:
f.write(
f"{item.get('timestamp', '')}\nQuery: {item.get('query', '')}\nResponse: {item.get('response', '')}\n\n")
QMessageBox.information(self, "Success", "History exported successfully!")
except Exception as e:
QMessageBox.critical(self, "Error", f"Error exporting history: {str(e)}")
def update_history_list(self, filter_text="", date_from=None, date_to=None):
self.history_list.clear()
for item_data in self.history:
timestamp_str = item_data.get("timestamp", "")
query = item_data.get("query", "")
try:
timestamp = datetime.datetime.fromisoformat(timestamp_str).date()
if date_from and timestamp < date_from:
continue
if date_to and timestamp > date_to:
continue
except ValueError:
pass
if filter_text.lower() not in query.lower():
continue
item = QListWidgetItem(f"{timestamp_str} - {query[:20]}...")
item.setData(Qt.UserRole, item_data)
self.history_list.addItem(item)
This class builds the main window, which includes two tabs: "Chat" and "History." The "Chat" tab allows users to enter their queries, select their level, and see the AI's responses. The "History" tab displays past conversations, offering search and export functionalities.
Launching Your AI Study Buddy
Now, let's bring our creation to life:
if __name__ == "__main__":
app = QApplication([])
window = EducationalAssistantGUI()
window.show()
app.exec_()
Congratulations! You've built your own personalized AI learning assistant.
Now that you have a working app, think about how you could make it even better! The BotHub API offers a lot of flexibility. Instead of just text responses, you could integrate image generation or speech transcription. BotHub also gives you access to multiple AI models, allowing you to choose the best one for different tasks. Imagine your assistant being able to summarize complex topics, translate languages, or even generate practice quizzes! The possibilities are vast. You've built a solid foundation; now go forth and explore!
SOCIAL SHARE CARD GENERATOR