教程:创建你自己的AI学习助手

感觉在学习新知识时不知所措吗?就像在信息的海洋中溺水,却什么都吸收不了?我们都经历过这种情况。拥有一个了解你水平并以易于理解的方式解释事物的个性化学习伙伴,岂不是太棒了吗?这正是我们将要一起构建的。

本教程将向你展示如何将 BotHub API 与 PyQt5 结合起来,创建一个互动且适应性强的学习工具。这不仅仅是另一个聊天机器人;它更像是一个全天候可用的个人导师。

准备工作区

在开始构建之前,让我们收集我们的工具。我们需要几个关键的 Python 库:

[code]
import os
import datetime
import json
from dataclasses import dataclass
from typing import List, Dict
from openai import OpenAI
from dotenv import load_dotenv
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QMovie
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QTextEdit, QRadioButton, QButtonGroup, QPushButton, QGroupBox, QListWidget, QListWidgetItem, QTabWidget, QFileDialog, QComboBox, QCheckBox, QMessageBox, QDialogButtonBox, QSpinBox, QFormLayout, QDialog, QDateEdit)

[/code]

进入全屏模式 退出全屏模式

将这些库视为你工具箱中的不同部分。一些库处理基础功能,如文件管理(os)、时间管理(datetime)和数据处理(json)。其他库,如 dataclassestyping,帮助我们编写干净、组织良好的代码。真正的魔力在于 openai,它让我们能够利用 AI 的力量。dotenv 确保我们的敏感信息(如 API 密钥)安全。最后,PyQt5 帮助我们创建一个美观且直观的用户界面。

构建用户请求

为了与我们的 AI 进行沟通,我们将创建一个 UserRequest 类。这有助于组织用户提供的信息:

[code]
@dataclass
class UserRequest:
query: str
user_level: str
preferences: Dict

[/code]

进入全屏模式 退出全屏模式

使用方便的 @dataclass 装饰器,我们定义了三个关键信息:用户的 query(他们的提问)、他们的 user_level(初学者、中级或高级)和他们的 preferences(例如,他们希望响应的长度)。这将所有内容整齐地打包成一个对象。

记住用户会话

为了使学习体验真正个性化,我们需要记住用户所做的事情以及他们的学习偏好。这就是 UserSession 类的作用:

[code]
class UserSession:
def init(self):
self.history: List[Dict] = []
self.preferences: Dict = {}
self.level: str = “beginner”

def add_to_history(self, query, response):
self.history.append({“query”: query, “response”: response, “timestamp”: datetime.datetime.now().isoformat()})

def update_preferences(self, new_preferences):
self.preferences.update(new_preferences)

[/code]

进入全屏模式 退出全屏模式

UserSession 跟踪对话的 history、用户的 preferences 和他们当前的 level。就像有一个专门的助手,记住一切并适应用户的需求。

操作的核心:EducationalAssistant

EducationalAssistant 类是我们应用程序的核心。它负责与 BotHub API 进行交互:

[code]
class EducationalAssistant:
def init(self):
load_dotenv()
self.client = OpenAI(api_key=os.getenv(‘BOTHUB_API_KEY’), base_url=’https://bothub.chat/api/v2/openai/v1‘)
self.session = UserSession()

def generate_prompt(self, request):
prompt = f”””作为教育助手,为 {request.user_level} 水平的学生提供响应。
查询: {request.query}\n”””

if request.preferences:
prompt += “考虑这些偏好:\n”
for key, value in request.preferences.items():
if key == “response_length”:
prompt += f”期望长度: 大约 {value} 字\n”
elif key == “include_examples” and value:
prompt += “包含示例: 是\n”
else:
prompt += f”{key.capitalize()}: {value}\n”

prompt += “请提供详细的解释。”
return prompt
def generate_text_response(self, request):
try:
response = self.client.chat.completions.create(
model=”claude-3.5-sonnet”, // 你可以使用 BotHub 上“可用模型”中的任何模型
messages=[
{“role”: “system”, “content”: “你是一个教育助手。”},
{“role”: “user”, “content”: self.generate_prompt(request)}
]
)
return response.choices[0].message.content
except Exception as e:
return f”生成文本响应时出错: {e}”

[/code]

进入全屏模式 退出全屏模式

这个类处理几个关键任务。首先,它使用你的 API 密钥初始化与 BotHub 的连接(我们之前提到过 [这里](https://dev.to/bothubchat/building-a-simple-python-app-to-boost-productivity-using-ai-and-the-bothub-api-3oke))。它还设置了一个 UserSession 来跟踪交互。generate_prompt 方法将用户的请求转换为 API 可以理解的提示。最后,generate_text_response 将提示发送到 API 并检索 AI 生成的答案。

平滑和响应:GenerateResponseThread

为了避免在 AI 思考时让用户等待,我们将使用一个单独的线程来进行 API 调用:

[code]
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)

[/code]

进入全屏模式 退出全屏模式

这个 GenerateResponseThread 基于 PyQt5 的 QThread,在后台运行 API 请求,确保用户界面保持响应。

个性化体验

每个人的学习方式都不同。为了满足个人偏好,我们将创建一个 PreferencesDialog

[code]
class PreferencesDialog(QDialog):
def init(self, parent=None, preferences=None):
super().init(parent)
self.setWindowTitle(“偏好设置”)
self.preferences = preferences or {}

layout = QVBoxLayout()
form_layout = QFormLayout()

self.tone_combo = QComboBox()
self.tone_combo.addItems([“正式”, “非正式”])
form_layout.addRow(“语气:”, 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(“响应长度(字数):”, self.length_spin)

self.examples_check = QCheckBox(“包含示例”)
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”, “正式”))
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”, “视觉”),
“topics_of_interest”: self.preferences.get(“topics_of_interest”, [])
}

[/code]

进入全屏模式 退出全屏模式

这个对话框允许用户自定义设置,如 AI 的语气、期望的响应长度以及是否包含示例。这种程度的定制确保了更具吸引力和有效的学习体验。

构建界面

最后,让我们使用 EducationalAssistantGUI 类创建用户界面:

[code]
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(“教育助手”)
layout = QVBoxLayout()
tabs = QTabWidget()
chat_tab = QWidget()
history_tab = QWidget()
tabs.addTab(chat_tab, “聊天”)
tabs.addTab(history_tab, “历史记录”)
chat_layout = QVBoxLayout()

query_group = QGroupBox(“查询”)
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(“用户级别”)
level_layout = QHBoxLayout()
self.level_selection = QButtonGroup()

beginner_button = QRadioButton(“初学者”)
beginner_button.setChecked(True)
intermediate_button = QRadioButton(“中级”)
advanced_button = QRadioButton(“高级”)

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(“生成响应”)
self.generate_button.clicked.connect(self.generate_response)
layout.addWidget(self.generate_button)

response_group = QGroupBox(“响应”)
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_layout = QVBoxLayout()

learning_style_label = QLabel(“学习风格:”)
self.learning_style_combo = QComboBox()
self.learning_style_combo.addItems(
[“视觉”, “听觉”, “动手”, “阅读/写作”])
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(“感兴趣的主题:”)
self.topics_checkboxes = {}
topics = [“数学”, “科学”, “历史”, “编程”, “艺术”]

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_button.clicked.connect(self.open_preferences_dialog)
chat_layout.addWidget(preferences_button)

chat_tab.setLayout(chat_layout)

history_layout = QVBoxLayout()
search_group = QGroupBox(“搜索/过滤”)
search_layout = QHBoxLayout()
self.search_text = QLineEdit()
self.search_text.setPlaceholderText(“搜索…”)
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(“关键词:”))
search_layout.addWidget(self.search_text)
search_layout.addWidget(QLabel(“从:”))
search_layout.addWidget(self.date_from)
search_layout.addWidget(QLabel(“到:”))
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_button.clicked.connect(self.save_history)
load_button = QPushButton(“加载历史”)
load_button.clicked.connect(self.load_history_from_file)

history_layout.addWidget(save_button)
history_layout.addWidget(load_button)

export_button = QPushButton(“导出历史”)
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, “错误”, “请输入查询。”)
return
level = ‘初学者’ if self.level_selection.buttons()[0].isChecked() else \
‘中级’ if self.level_selection.buttons()[1].isChecked() else “高级”

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(“加载中…”)
def display_response(self, response):
self.generate_button.setEnabled(True)
self.loading_label.hide()
self.loading_movie.stop()

if “生成文本响应时出错:” in response:
QMessageBox.critical(self, “错误”, response.replace(“生成文本响应时出错:”, “”))
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”保存偏好设置时出错: {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”加载偏好设置时出错: {e}”)
return {}
def open_preferencesdialog(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”保存历史时出错: {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”加载历史时出错: 无效的 JSON 格式: {e}”)
def load_history_fromfile(self):
options = QFileDialog.Options()
filename,
= QFileDialog.getOpenFileName(self, “加载聊天历史”, “”, “JSON 文件 (.json);;所有文件 ()”,
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”从文件加载历史时出错: 无效的 JSON 格式: {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 exporthistory(self):
options = QFileDialog.Options()
filename,
= QFileDialog.getSaveFileName(self, “导出聊天历史”, “”,
“文本文件 (.txt);;CSV 文件 (.csv)”, options=options)
if filename:
try:
with open(filename, “w”, encoding=”utf-8″) as f:
if filename.endswith(“.csv”):
f.write(“时间戳,查询,响应\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’, ”)}\n查询: {item.get(‘query’, ”)}\n响应: {item.get(‘response’, ”)}\n\n”)
QMessageBox.information(self, “成功”, “历史导出成功!”)

except Exception as e:
QMessageBox.critical(self, “错误”, f”导出历史时出错: {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)

[/code]

进入全屏模式 退出全屏模式

这个类构建了主窗口,包括两个选项卡:“聊天”和“历史记录”。“聊天”选项卡允许用户输入查询、选择级别并查看 AI 的响应。“历史记录”选项卡显示过去的对话,提供搜索和导出功能。

启动你的 AI 学习伙伴

现在,让我们将我们的创作变为现实:

[code]
if name == “main“:
app = QApplication([])
window = EducationalAssistantGUI()
window.show()
app.exec_()

[/code]

进入全屏模式 退出全屏模式

恭喜你!你已经构建了自己的个性化 AI 学习助手。


现在你有了一个可用的应用程序,想想你如何可以使它变得更好!BotHub API 提供了很多灵活性。你可以集成图像生成或语音转录,而不仅仅是文本响应。BotHub 还让你可以访问多个 AI 模型,允许你为不同任务选择最佳模型。想象一下你的助手能够总结复杂主题、翻译语言,甚至生成练习测验!可能性是巨大的。你已经构建了一个坚实的基础;现在去探索吧!

更多