目录

一、背景介绍

二、代码实现

1.导入必要的库

2.初始化OpenAI客户端

3.设置历史记录目录

4.创建ChatGUI类

4.1初始化方法

4.2构建用户界面

4.3创建聊天显示区域

4.4创建输入区域

4.5处理回车键事件

4.6添加消息到聊天区域

5.主程序

三、完整代码

四、总结


一、背景介绍

在人工智能日益普及的今天,利用深度学习模型构建聊天机器人已成为一种常见的应用。本文将介绍如何使用Python的Tkinter库结合OpenAI的API来创建一个图形用户界面的聊天应用。这个应用允许用户选择不同的模型进行对话,保存和加载对话历史,并在必要时切换到长文本输入模式。

二、代码实现

Linux界面请参考:Python接入deepseek API(官网和腾讯)

更多功能请参考:更多功能

1.导入必要的库

首先,我们需要导入实现这个功能所需的Python库:

from openai import OpenAI 
import json 
import glob 
import threading 
import tkinter as tk 
from tkinter import ttk, scrolledtext, messagebox, filedialog 
  • OpenAI:用于与OpenAI的API进行交互。
  • json:用于文件和数据处理。
  • glob:用于查找符合特定规则的文件路径名。
  • threading:用于在后台线程中处理API请求,避免阻塞主线程。
  • tkinter 及其子模块:用于构建图形用户界面。

2.初始化OpenAI客户端

client = OpenAI( 
    api_key="sk-xxxxx", 
    base_url="https://api.deepseek.com", 
)

我们从环境变量中获取OpenAI的API Key,并初始化客户端。这里假设你使用的是DeepSeek的API端点。

3.设置历史记录目录

HISTORY_FILE_PREFIX = "history_" 
HISTORY_FILE_DIR = "history_records" 

if not os.path.exists(HISTORY_FILE_DIR): 
    os.makedirs(HISTORY_FILE_DIR)

我们定义了历史记录文件的前缀和存储目录,并确保该目录存在。

4.创建ChatGUI类

接下来,我们创建一个ChatGUI类,用于构建和管理聊天应用的GUI。

4.1初始化方法
class ChatGUI: 
    def __init__(self, master): 
        self.master = master 
        self.model = "deepseek-chat" 
        self.long_text_mode = False 
        self.conversation_history = [{"role": "system", "content": "You are a helpful assistant"}] 
        self.is_streaming = False 
        self.setup_ui()

在初始化方法中,我们设置了默认模型、长文本模式、对话历史和流状态,并调用了setup_ui方法来构建用户界面。

4.2构建用户界面

在setup_ui方法中,我们创建了一个顶部控制栏,包含模型选择、保存历史、清空历史和加载历史的功能。

control_frame = ttk.Frame(self.master) # 创建一个顶部框架
control_frame.pack(fill=tk.X, padx=5, pady=5) # 将框架放置在窗口顶部,填充X方向

# 模型选择下拉框
self.model_var = tk.StringVar(value=self.model) # 创建一个字符串变量,用于存储当 前选择的模型
model_combobox = ttk.Combobox(control_frame, textvariable=self.model_var,
                              values=["deepseek-chat", "deepseek-reasoner", "deepseek-coder"], width=15)
model_combobox.pack(side=tk.LEFT, padx=5) # 将下拉框放置在左侧
model_combobox.bind("<<ComboboxSelected>>", self.on_model_change) # 绑定模型切换 事件


# 保存历史按钮
ttk.Button(control_frame, text="保存历史", command=self.save_history).pack(side=tk.LEFT, padx=5)


# 清空历史按钮
ttk.Button(control_frame, text="清空历史", command=self.clear_history).pack(side=tk.LEFT, padx=5)


# 加载历史按钮
ttk.Button(control_frame, text="加载历史", command=self.load_history_dialog).pack(side=tk.LEFT, padx=5)

ttk.Frame: 创建一个框架,用于容纳其他控件。

ttk.Combobox: 创建一个下拉框,用于选择模型。

ttk.Button: 创建按钮,分别用于保存、清空和加载历史记录。

pack: 将控件放置在窗口中,`side=tk.LEFT`表示控件从左到右排列。

4.3创建聊天显示区域

聊天显示区域是一个可滚动的文本框,用于显示用户和助手的对话。

self.chat_area = scrolledtext.ScrolledText(self.master, wrap=tk.WORD, state=tk.DISABLED)
self.chat_area.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

scrolledtext.ScrolledText: 创建一个带滚动条的文本框。

wrap=tk.WORD: 设置文本自动换行。

state=tk.DISABLED: 禁止用户直接编辑文本框内容。

fill=tk.BOTH, expand=True: 使文本框填充整个可用空间。

4.4创建输入区域

输入区域包括一个多行文本框和一个发送按钮,用户在这里输入消息。

input_frame = ttk.Frame(self.master) # 创建一个输入区域框架
input_frame.pack(fill=tk.X, padx=5, pady=5) # 将框架放置在窗口底部


# 输入文本框
self.input_text = tk.Text(input_frame, height=4) # 创建一个多行文本框,高度为4行
self.input_text.pack(fill=tk.X, pady=5) # 将文本框放置在输入区域中
self.input_text.bind("<Return>", self.on_enter_pressed) # 绑定回车键事件


# 底部按钮框架
btn_frame = ttk.Frame(input_frame) # 创建一个按钮框架
btn_frame.pack(fill=tk.X) # 将按钮框架放置在输入区域底部


# 发送按钮
ttk.Button(btn_frame, text="发送", command=self.send_message).pack(side=tk.RIGHT, padx=5)


# 长文本模式复选框
ttk.Checkbutton(btn_frame, text="长文本模式", command=self.toggle_long_text).pack(side=tk.LEFT, padx=5)

tk.Text: 创建一个多行文本框,用于用户输入消息。

bind("<Return>", self.on_enter_pressed): 绑定回车键事件,按下回车键时调用on_enter_pressed方法。

ttk.Checkbutton: 创建一个复选框,用于切换长文本模式。

4.5处理回车键事件

on_enter_pressed方法用于处理用户在输入框中按下回车键的事件。

def on_enter_pressed(self, event):
    if not event.state & 0x1: # 检查是否按下Control键
        if not self.long_text_mode: # 如果未启用长文本模式
            self.send_message() # 发送消息
            return "break" # 阻止默认的换行行为
    # 允许换行输入
    return None

event.state & 0x1: 检查是否按下了Control键。

return "break": 阻止默认的换行行为。

4.6添加消息到聊天区域

append_to_chat方法用于将消息添加到聊天显示区域。

def append_to_chat(self, role, content):
    self.chat_area.configure(state=tk.NORMAL) # 启用文本框编辑
    self.chat_area.insert(tk.END, f"\n{role}: {content}\n") # 插入消息
    self.chat_area.configure(state=tk.DISABLED) # 禁用文本框编辑
    self.chat_area.see(tk.END) # 滚动到文本框底部

configure(state=tk.NORMAL): 启用文本框编辑,以便插入消息。

insert(tk.END, ...): 在文本框的末尾插入消息。

see(tk.END): 自动滚动到文本框的底部。

5.主程序

if __name__ == "__main__": 
    root = tk.Tk() 
    app = ChatGUI(root) 
    root.mainloop()

三、完整代码

from openai import OpenAI
import os
import json
import glob
import threading
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, filedialog

# 初始化OpenAI客户端
client = OpenAI(
    api_key="sk-f4343d0d78d549ab8b789bc34e5239fa",
    base_url="https://api.deepseek.com",
)

# 定义历史记录文件前缀和目录
HISTORY_FILE_PREFIX = "history_"
HISTORY_FILE_DIR = "history_records"

# 确保历史记录目录存在
if not os.path.exists(HISTORY_FILE_DIR):
    os.makedirs(HISTORY_FILE_DIR)

class ChatGUI:
    def __init__(self, master):
        self.master = master
        self.model = "deepseek-chat"
        self.long_text_mode = False
        self.conversation_history = [{"role": "system", "content": "You are a helpful assistant"}]
        self.is_streaming = False
        self.setup_ui()

    def setup_ui(self):
        self.master.title("DeepSeek Chat GUI")
        self.master.geometry("800x600")

        # 顶部控制栏
        control_frame = ttk.Frame(self.master)
        control_frame.pack(fill=tk.X, padx=5, pady=5)

        self.model_var = tk.StringVar(value=self.model)
        model_combobox = ttk.Combobox(control_frame, textvariable=self.model_var,
                                    values=["deepseek-chat", "deepseek-reasoner", "deepseek-coder"], width=15)
        model_combobox.pack(side=tk.LEFT, padx=5)
        model_combobox.bind("<<ComboboxSelected>>", self.on_model_change)

        ttk.Button(control_frame, text="保存历史", command=self.save_history).pack(side=tk.LEFT, padx=5)
        ttk.Button(control_frame, text="清空历史", command=self.clear_history).pack(side=tk.LEFT, padx=5)
        ttk.Button(control_frame, text="加载历史", command=self.load_history_dialog).pack(side=tk.LEFT, padx=5)

        # 对话显示区域
        self.chat_area = scrolledtext.ScrolledText(self.master, wrap=tk.WORD, state=tk.DISABLED)
        self.chat_area.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        # 输入区域
        input_frame = ttk.Frame(self.master)
        input_frame.pack(fill=tk.X, padx=5, pady=5)

        self.input_text = tk.Text(input_frame, height=4)
        self.input_text.pack(fill=tk.X, pady=5)
        self.input_text.bind("<Return>", self.on_enter_pressed)

        # 底部按钮
        btn_frame = ttk.Frame(input_frame)
        btn_frame.pack(fill=tk.X)

        ttk.Button(btn_frame, text="发送", command=self.send_message).pack(side=tk.RIGHT, padx=5)
        ttk.Checkbutton(btn_frame, text="长文本模式", command=self.toggle_long_text).pack(side=tk.LEFT, padx=5)

    def on_model_change(self, event):
        self.model = self.model_var.get()
        self.append_to_chat("系统", f"模型已切换为: {self.model}")

    def toggle_long_text(self):
        self.long_text_mode = not self.long_text_mode
        status = "启用" if self.long_text_mode else "禁用"
        self.append_to_chat("系统", f"长文本模式已{status}")

    def on_enter_pressed(self, event):
        if not event.state & 0x1:  # 检查是否按下Control键
            if not self.long_text_mode:
                self.send_message()
                return "break"
        # 允许换行输入
        return None

    def append_to_chat(self, role, content):
        self.chat_area.configure(state=tk.NORMAL)
        self.chat_area.insert(tk.END, f"\n{role}: {content}\n")
        self.chat_area.configure(state=tk.DISABLED)
        self.chat_area.see(tk.END)

    def save_history(self):
        file_name = self.get_next_history_file_name()
        with open(file_name, 'w', encoding='utf-8') as f:
            json.dump(self.conversation_history, f, ensure_ascii=False, indent=4)
        self.append_to_chat("系统", "历史记录已保存")

    def clear_history(self):
        self.conversation_history = [{"role": "system", "content": "You are a helpful assistant"}]
        self.append_to_chat("系统", "对话历史已清空")

    def load_history_dialog(self):
        files = [f for f in os.listdir(HISTORY_FILE_DIR) if f.startswith(HISTORY_FILE_PREFIX)]
        if not files:
            messagebox.showinfo("提示", "没有历史记录文件")
            return

        dialog = tk.Toplevel(self.master)
        dialog.title("选择历史记录")

        lb = tk.Listbox(dialog, width=50, height=15)
        lb.pack(padx=10, pady=10)

        for f in files:
            lb.insert(tk.END, f)

        def on_select():
            selected = lb.curselection()
            if selected:
                file_name = os.path.join(HISTORY_FILE_DIR, lb.get(selected[0]))
                with open(file_name, 'r', encoding='utf-8') as f:
                    self.conversation_history = json.load(f)
                self.refresh_chat_display()
                dialog.destroy()

        ttk.Button(dialog, text="加载", command=on_select).pack(pady=5)

    def refresh_chat_display(self):
        self.chat_area.configure(state=tk.NORMAL)
        self.chat_area.delete(1.0, tk.END)
        for msg in self.conversation_history:
            if msg["role"] == "user":
                self.chat_area.insert(tk.END, f"\n用户: {msg['content']}\n")
            elif msg["role"] == "assistant":
                self.chat_area.insert(tk.END, f"\n助手: {msg['content']}\n")
        self.chat_area.configure(state=tk.DISABLED)

    def send_message(self):
        if self.is_streaming:
            return

        user_input = self.input_text.get("1.0", tk.END).strip()
        if not user_input:
            return

        self.input_text.delete("1.0", tk.END)
        self.conversation_history.append({"role": "user", "content": user_input})
        self.append_to_chat("用户", user_input)

        # 重置助手索引状态
        if hasattr(self, 'last_assistant_index'):
            del self.last_assistant_index

        # 在新线程中处理API请求
        self.is_streaming = True
        threading.Thread(target=self.process_stream, args=(user_input,)).start()

    def process_stream(self, user_input):
        try:
            stream = client.chat.completions.create(
                model=self.model,
                messages=self.conversation_history,
                stream=True
            )

            response = []
            reasoning = []
            for chunk in stream:
                if chunk.choices:
                    delta = chunk.choices[0].delta

                    if self.model == "deepseek-reasoner" and hasattr(delta, 'reasoning_content') and delta.reasoning_content:
                        reasoning.append(delta.reasoning_content)
                        self.update_reasoning(delta.reasoning_content)

                    if hasattr(delta, 'content') and delta.content:
                        response.append(delta.content)
                        self.update_response(delta.content)

            full_response = ''.join(response)
            self.conversation_history.append({"role": "assistant", "content": full_response})

        except Exception as e:
            self.append_to_chat("系统", f"错误: {str(e)}")
        finally:
            self.is_streaming = False

    def update_reasoning(self, content):
        self.master.after(0, lambda: self.append_to_chat("思考", content))

    def update_response(self, content):
        self.master.after(0, lambda: self.append_content_to_chat("助手", content))

    def append_content_to_chat(self, role, content):
        self.chat_area.configure(state=tk.NORMAL)
        if role == "助手":
            # 检查是否是当前轮次助手的第一个内容片段
            if not hasattr(self, 'last_assistant_index'):
                self.chat_area.insert(tk.END, f"\n助手: {content}")  # 首条带前缀
                self.last_assistant_index = self.chat_area.index(tk.INSERT)
            else:
                # 直接插入内容到上次位置(不带前缀)
                self.chat_area.insert(self.last_assistant_index, content)
                self.last_assistant_index = self.chat_area.index(tk.INSERT)
        else:
            self.chat_area.insert(tk.END, f"\n{role}: {content}\n")
        self.chat_area.configure(state=tk.DISABLED)
        self.chat_area.see(tk.END)

    # 历史文件管理相关方法
    def get_next_history_file_name(self):
        history_files = glob.glob(os.path.join(HISTORY_FILE_DIR, f"{HISTORY_FILE_PREFIX}*.json"))
        if not history_files:
            return os.path.join(HISTORY_FILE_DIR, f"{HISTORY_FILE_PREFIX}1.json")
        latest_file = max(history_files, key=os.path.getctime)
        latest_num = int(os.path.basename(latest_file).split("_")[1].split(".")[0])
        return os.path.join(HISTORY_FILE_DIR, f"{HISTORY_FILE_PREFIX}{latest_num + 1}.json")

if __name__ == "__main__":
    root = tk.Tk()
    app = ChatGUI(root)
    root.mainloop()

四、总结

本文介绍了一个使用Tkinter和OpenAI API构建的聊天GUI应用。这个应用提供了用户友好的界面,允许用户选择不同的模型进行对话,保存和加载对话历史,并切换到长文本输入模式。通过结合代码和非代码类型的注释,我们详细解析了应用的实现过程,包括初始化客户端、设置历史记录目录、创建ChatGUI类及其方法。希望这个应用能为你的项目或学习提供有用的参考。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐