diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ef0784..f68d1e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 0.6.0 (2023-06-12) +* Enable users to chat with .csv documents. +* Enable users to customize chat settings (e.g. font size and background color). + ## 0.5.0 (2023-06-08) * Enable users to ask follow-up questions when chatting with documents. * Enable users to customize chatbot parameters by uploading a configuration file. diff --git a/README.md b/README.md index 79ce62d..8f02cd3 100644 --- a/README.md +++ b/README.md @@ -2,30 +2,71 @@ [![PyPI](https://badge.fury.io/py/kanu.svg)](https://badge.fury.io/py/kanu) -KANU is a minimalistic Python-based GUI for various chatbots. +Welcome to KANU, a minimalistic Python-based GUI for various chatbots. -## Chatbots +There are currently two chatbots available in KANU: -### ChatGPT +- [ChatGPT](#chatgpt) harnesses the power of ChatGPT, bringing it directly to your local computer +- [DocGPT](#docgpt) allows you to effortlessly interact with your documents and ask questions about them -![Alt Text](https://raw.githubusercontent.com/sbslee/kanu/main/images/chatgpt.gif) +Other features of KANU inclde: -### DocGPT - -![Alt Text](https://raw.githubusercontent.com/sbslee/kanu/main/images/docgpt.gif) +- Customize chat settings (e.g. font size and background color) +- Customize chatbot parameters (e.g. prompt, temperature, and chunk size) by directly using the GUI or uploading a configuration file ## Installation +The recommended way is via pip: + ``` $ pip install kanu ``` +KANU requires a different set of dependencies for each chatbot. You can find the dependencies specific to each chatbot in the [Chatbots](#chatbots) section. + ## Running ``` $ kanu ``` + +## Chatbots + + +### ChatGPT + +![Alt Text](https://raw.githubusercontent.com/sbslee/kanu/main/images/chatgpt.gif) + +The following packages are required to run ChatGPT: + +``` +openai # Required. +``` + + +### DocGPT + +![Alt Text](https://raw.githubusercontent.com/sbslee/kanu/main/images/docgpt.gif) + +The following document formats are supported by DocGPT: + +- .txt +- .pdf +- .doc and .docx +- .csv + +The following packages are required to run DocGPT: + +``` +langchain # Required. +chromadb # Required. +tiktoken # Required. +pdfminer.six # Optional. Only required for .pdf documents. +unstructured # Optional. Only required for .doc and .docx documents. +tabulate # Optional. Only required for .doc and .docx documents. +``` + ## Changelog See the [CHANGELOG.md](https://github.com/sbslee/kanu/blob/main/CHANGELOG.md) file for details. \ No newline at end of file diff --git a/kanu/__main__.py b/kanu/__main__.py index 0127342..924cb55 100644 --- a/kanu/__main__.py +++ b/kanu/__main__.py @@ -84,14 +84,20 @@ def config_chatgpt(self): def parse_chatgpt_config(self): config = configparser.ConfigParser() - config.read(filedialog.askopenfilename()) + file_path = filedialog.askopenfilename() + if not file_path: + return + config.read(file_path) self.deploy_agent("ChatGPT", config["USER"]["openai_key"], config["DEFAULT"]["model"], float(config["DEFAULT"]["temperature"]), config["DEFAULT"]["prompt"]) def template_chatgpt_config(self): + file_path = filedialog.asksaveasfilename() + if not file_path: + return config = configparser.ConfigParser() config["DEFAULT"] = {"model": "gpt-3.5-turbo", "temperature": "0.5", "prompt": CHATGPT_PROMPT} config["USER"] = {"openai_key": ""} - with open(filedialog.asksaveasfilename(), "w") as f: + with open(file_path, "w") as f: config.write(f) def config_docgpt(self): @@ -144,21 +150,27 @@ def config_docgpt(self): l.grid(row=18, column=0, columnspan=2) e = tk.Entry(self.container) e.grid(row=19, column=0, columnspan=2) - b = tk.Button(self.container, text="Submit", command=lambda: self.deploy_agent("DocGPT", e.get(), self.model.get(), self.prompt.get("1.0", "end-1c"), self.temperature.get())) + b = tk.Button(self.container, text="Submit", command=lambda: self.deploy_agent("DocGPT", e.get(), self.model.get(), self.prompt.get("1.0", "end-1c"), self.temperature.get(), 1000, 50)) b.grid(row=20, column=0) b = tk.Button(self.container, text="Go back", command=lambda: self.homepage()) b.grid(row=20, column=1) def parse_docgpt_config(self): config = configparser.ConfigParser() - config.read(filedialog.askopenfilename()) - self.deploy_agent("DocGPT", config["USER"]["openai_key"], config["DEFAULT"]["model"], float(config["DEFAULT"]["temperature"]), config["DEFAULT"]["prompt"]) + file_path = filedialog.askopenfilename() + if not file_path: + return + config.read(file_path) + self.deploy_agent("DocGPT", config["USER"]["openai_key"], config["DEFAULT"]["model"], float(config["DEFAULT"]["temperature"]), config["DEFAULT"]["prompt"], config["DEFAULT"]["chunk_size"], config["DEFAULT"]["chunk_overlap"]) def template_docgpt_config(self): + file_path = filedialog.asksaveasfilename() + if not file_path: + return config = configparser.ConfigParser() - config["DEFAULT"] = {"model": "gpt-3.5-turbo", "temperature": "0.5", "prompt": DOCGPT_PROMPT} + config["DEFAULT"] = {"model": "gpt-3.5-turbo", "temperature": "0.5", "prompt": DOCGPT_PROMPT, "chunk_size": 1000, "chunk_overlap": 50} config["USER"] = {"openai_key": ""} - with open(filedialog.asksaveasfilename(), "w") as f: + with open(file_path, "w") as f: config.write(f) def deploy_agent(self, agent, *args, **kwargs): diff --git a/kanu/chatgpt.py b/kanu/chatgpt.py index 978fc0a..ee705ac 100644 --- a/kanu/chatgpt.py +++ b/kanu/chatgpt.py @@ -2,6 +2,8 @@ import openai +from .utils import Settings + class ChatGPT: def __init__(self, kanu, openai_key, model, temperature, prompt): self.kanu = kanu @@ -9,24 +11,29 @@ def __init__(self, kanu, openai_key, model, temperature, prompt): self.temperature = temperature self.prompt = prompt openai.api_key = openai_key + self.settings = Settings(self) def run(self): self.kanu.container.pack_forget() self.kanu.container = tk.Frame(self.kanu.root) self.kanu.container.pack() l = tk.Label(self.kanu.container, text="ChatGPT") - l.grid(row=0, column=0, columnspan=3) + l.grid(row=0, column=0, columnspan=4) self.session = tk.Text(self.kanu.container, width=70, height=20) - self.session.grid(row=1, column=0, columnspan=3) - e = tk.Entry(self.kanu.container, width=54) - e.grid(row=2, column=0, columnspan=3) + self.session.grid(row=1, column=0, columnspan=4) + self.session.tag_config("user", **self.settings.get_user_kwargs()) + self.session.tag_config("bot", **self.settings.get_bot_kwargs()) + user_input = tk.Entry(self.kanu.container, width=54) + user_input.grid(row=2, column=0, columnspan=4) self.messages = [] - b = tk.Button(self.kanu.container, text="Send", command=lambda: self.send_message(e)) + b = tk.Button(self.kanu.container, text="Send", command=lambda: self.send_message(user_input)) b.grid(row=3, column=0) b = tk.Button(self.kanu.container, text="Clear", command=lambda: self.clear_session()) b.grid(row=3, column=1) b = tk.Button(self.kanu.container, text="Go back", command=lambda: self.kanu.config_chatgpt()) b.grid(row=3, column=2) + b = tk.Button(self.kanu.container, text="Settings", command=lambda: self.settings.page()) + b.grid(row=3, column=3) def send_message(self, entry): if not self.messages: @@ -39,8 +46,8 @@ def send_message(self, entry): ) response = bot_response["choices"][0]["message"]["content"] self.messages += [{"role": "assistant", "content": response}] - self.session.insert(tk.END, "You: " + entry.get() + "\n") - self.session.insert(tk.END, f"Bot: " + response + "\n") + self.session.insert(tk.END, "You: " + entry.get() + "\n", "user") + self.session.insert(tk.END, f"Bot: " + response + "\n", "bot") entry.delete(0, tk.END) def clear_session(self): diff --git a/kanu/docgpt.py b/kanu/docgpt.py index d29835e..7803419 100644 --- a/kanu/docgpt.py +++ b/kanu/docgpt.py @@ -13,25 +13,30 @@ from langchain.document_loaders import ( TextLoader, PDFMinerLoader, - UnstructuredWordDocumentLoader + UnstructuredWordDocumentLoader, + CSVLoader, ) -from .utils import Tooltip +from .utils import Tooltip, Settings DOCUMENT_LOADERS = { ".txt": (TextLoader, {"encoding": "utf8"}), ".pdf": (PDFMinerLoader, {}), ".doc": (UnstructuredWordDocumentLoader, {}), ".docx": (UnstructuredWordDocumentLoader, {}), + ".csv": (CSVLoader, {}), } class DocGPT: - def __init__(self, kanu, openai_key, model, temperature, prompt): + def __init__(self, kanu, openai_key, model, temperature, prompt, default_chunk_size, default_chunk_overlap): self.kanu = kanu self.model = model self.temperature = temperature self.prompt = prompt + self.default_chunk_size = default_chunk_size + self.default_chunk_overlap = default_chunk_overlap os.environ["OPENAI_API_KEY"] = openai_key + self.settings = Settings(self) def run(self): self.kanu.container.pack_forget() @@ -62,13 +67,13 @@ def run(self): l = tk.Label(self.kanu.container, text="Chunk size ⓘ:") Tooltip(l, "The maximum number of characters in each chunk.") l.grid(row=5, column=0) - self.chunk_size = tk.IntVar(self.kanu.container, value=1000) + self.chunk_size = tk.IntVar(self.kanu.container, value=self.default_chunk_size) e = tk.Entry(self.kanu.container, textvariable=self.chunk_size) e.grid(row=5, column=1, columnspan=2) l = tk.Label(self.kanu.container, text="Chunk overlap ⓘ:") Tooltip(l, "The number of overlapping characters between adjacent chunks.") l.grid(row=6, column=0) - self.chunk_overlap = tk.IntVar(self.kanu.container, value=50) + self.chunk_overlap = tk.IntVar(self.kanu.container, value=self.default_chunk_overlap) e = tk.Entry(self.kanu.container, textvariable=self.chunk_overlap) e.grid(row=6, column=1, columnspan=2) self.option1_button = tk.Button(self.kanu.container, text="Go with Option 1", command=self.go_with_option1) @@ -101,22 +106,26 @@ def query(self): self.kanu.container = tk.Frame(self.kanu.root) self.kanu.container.pack() l = tk.Label(self.kanu.container, text="DocGPT") - l.grid(row=0, column=0, columnspan=3) + l.grid(row=0, column=0, columnspan=4) self.session = tk.Text(self.kanu.container, width=70, height=20) - self.session.grid(row=1, column=0, columnspan=3) - e = tk.Entry(self.kanu.container, width=54) - e.grid(row=2, column=0, columnspan=3) - b = tk.Button(self.kanu.container, text="Send", command=lambda: self.send_message(e)) + self.session.grid(row=1, column=0, columnspan=4) + self.session.tag_config("user", **self.settings.get_user_kwargs()) + self.session.tag_config("bot", **self.settings.get_bot_kwargs()) + user_input = tk.Entry(self.kanu.container, width=54) + user_input.grid(row=2, column=0, columnspan=4) + b = tk.Button(self.kanu.container, text="Send", command=lambda: self.send_message(user_input)) b.grid(row=3, column=0) b = tk.Button(self.kanu.container, text="Clear", command=lambda: self.clear_session()) b.grid(row=3, column=1) b = tk.Button(self.kanu.container, text="Go back", command=lambda: self.run()) b.grid(row=3, column=2) + b = tk.Button(self.kanu.container, text="Settings", command=lambda: self.settings.page()) + b.grid(row=3, column=3) def send_message(self, entry): - self.session.insert(tk.END, "You: " + entry.get() + "\n") + self.session.insert(tk.END, "You: " + entry.get() + "\n", "user") response = self.qa(entry.get())["answer"] - self.session.insert(tk.END, "Bot: " + response + "\n") + self.session.insert(tk.END, "Bot: " + response + "\n", "bot") entry.delete(0, tk.END) def go_with_option1(self): @@ -129,12 +138,12 @@ def go_with_option1(self): continue loader_class, loader_kwargs = DOCUMENT_LOADERS[file_ext] loader = loader_class(file_path, **loader_kwargs) - document = loader.load()[0] - documents.append(document) + document = loader.load() + documents.extend(document) text_splitter = RecursiveCharacterTextSplitter(chunk_size=self.chunk_size.get(), chunk_overlap=self.chunk_overlap.get()) texts = text_splitter.split_documents(documents) db = Chroma.from_documents(texts, OpenAIEmbeddings(), persist_directory=self.database_directory) - db.add_documents(texts) + db.add_documents(texts) db.persist() db = None self.query() diff --git a/kanu/utils.py b/kanu/utils.py index 748a678..d0e4879 100644 --- a/kanu/utils.py +++ b/kanu/utils.py @@ -1,4 +1,109 @@ import tkinter as tk +from tkinter import font + +class Settings: + def __init__(self, agent): + self.default_font = font.nametofont("TkDefaultFont").actual() + self.default_user_background_color = "gray85" + self.default_user_foreground_color = "black" + self.default_user_font_family = self.default_font["family"] + self.default_user_font_size = self.default_font["size"] + self.default_bot_background_color = "white" + self.default_bot_foreground_color = "black" + self.default_bot_font_family = self.default_font["family"] + self.default_bot_font_size = self.default_font["size"] + self.agent = agent + self.user_background_color = tk.StringVar(self.agent.kanu.container, value=self.default_user_background_color) + self.user_foreground_color = tk.StringVar(self.agent.kanu.container, value=self.default_user_foreground_color) + self.user_font_family = tk.StringVar(self.agent.kanu.container, value=self.default_user_font_family) + self.user_font_size = tk.IntVar(self.agent.kanu.container, value=self.default_user_font_size) + self.bot_background_color = tk.StringVar(self.agent.kanu.container, value=self.default_bot_background_color) + self.bot_foreground_color = tk.StringVar(self.agent.kanu.container, value=self.default_bot_foreground_color) + self.bot_font_family = tk.StringVar(self.agent.kanu.container, value=self.default_bot_font_family) + self.bot_font_size = tk.IntVar(self.agent.kanu.container, value=self.default_bot_font_size) + + def get_user_kwargs(self): + return dict( + background=self.user_background_color.get(), + foreground=self.user_foreground_color.get(), + font=(self.user_font_family.get(), self.user_font_size.get()) + ) + + def get_bot_kwargs(self): + return dict( + background=self.bot_background_color.get(), + foreground=self.bot_foreground_color.get(), + font=(self.bot_font_family.get(), self.bot_font_size.get()) + ) + + def page(self): + self.agent.previous = self.agent.kanu.container + self.agent.kanu.container.pack_forget() + self.agent.kanu.container = tk.Frame(self.agent.kanu.root) + self.agent.kanu.container.pack() + l = tk.Label(self.agent.kanu.container, text=self.agent.__class__.__name__) + l.grid(row=0, column=0, columnspan=3) + l = tk.Label(self.agent.kanu.container, text="User background color") + l.grid(row=1, column=0) + e = tk.Entry(self.agent.kanu.container, textvariable=self.user_background_color) + e.grid(row=1, column=1, columnspan=2) + l = tk.Label(self.agent.kanu.container, text="User foreground color") + l.grid(row=2, column=0) + e = tk.Entry(self.agent.kanu.container, textvariable=self.user_foreground_color) + e.grid(row=2, column=1, columnspan=2) + l = tk.Label(self.agent.kanu.container, text="User font family") + l.grid(row=3, column=0) + e = tk.Entry(self.agent.kanu.container, textvariable=self.user_font_family) + e.grid(row=3, column=1, columnspan=2) + l = tk.Label(self.agent.kanu.container, text="User font size") + l.grid(row=4, column=0) + e = tk.Entry(self.agent.kanu.container, textvariable=self.user_font_size) + e.grid(row=4, column=1, columnspan=2) + l = tk.Label(self.agent.kanu.container, text="Bot background color") + l.grid(row=5, column=0) + e = tk.Entry(self.agent.kanu.container, textvariable=self.bot_background_color) + e.grid(row=5, column=1, columnspan=2) + l = tk.Label(self.agent.kanu.container, text="Bot foreground color") + l.grid(row=6, column=0) + e = tk.Entry(self.agent.kanu.container, textvariable=self.bot_foreground_color) + e.grid(row=6, column=1, columnspan=2) + l = tk.Label(self.agent.kanu.container, text="Bot font family") + l.grid(row=7, column=0) + e = tk.Entry(self.agent.kanu.container, textvariable=self.bot_font_family) + e.grid(row=7, column=1, columnspan=2) + l = tk.Label(self.agent.kanu.container, text="Bot font size") + l.grid(row=8, column=0) + e = tk.Entry(self.agent.kanu.container, textvariable=self.bot_font_size) + e.grid(row=8, column=1, columnspan=2) + b = tk.Button(self.agent.kanu.container, text="Apply", command=lambda: self.apply()) + b.grid(row=9, column=0) + b = tk.Button(self.agent.kanu.container, text="Reset", command=lambda: self.reset()) + b.grid(row=9, column=1) + b = tk.Button(self.agent.kanu.container, text="Go back", command=lambda: self.go_back()) + b.grid(row=9, column=2) + + def go_back(self): + self.agent.kanu.container.pack_forget() + self.agent.kanu.container = self.agent.previous + self.agent.kanu.container.pack() + + def apply(self): + self.agent.session.tag_config("user", **self.get_user_kwargs()) + self.agent.session.tag_config("bot", **self.get_bot_kwargs()) + self.agent.kanu.container.pack_forget() + self.agent.kanu.container = self.agent.previous + self.agent.kanu.container.pack() + + def reset(self): + self.user_background_color = tk.StringVar(self.agent.kanu.container, value=self.default_user_background_color) + self.user_foreground_color = tk.StringVar(self.agent.kanu.container, value=self.default_user_foreground_color) + self.user_font_family = tk.StringVar(self.agent.kanu.container, value=self.default_user_font_family) + self.user_font_size = tk.IntVar(self.agent.kanu.container, value=self.default_user_font_size) + self.bot_background_color = tk.StringVar(self.agent.kanu.container, value=self.default_bot_background_color) + self.bot_foreground_color = tk.StringVar(self.agent.kanu.container, value=self.default_bot_foreground_color) + self.bot_font_family = tk.StringVar(self.agent.kanu.container, value=self.default_bot_font_family) + self.bot_font_size = tk.IntVar(self.agent.kanu.container, value=self.default_bot_font_size) + self.apply() class Tooltip: def __init__(self, widget, text): diff --git a/kanu/version.py b/kanu/version.py index 5a28280..da74604 100644 --- a/kanu/version.py +++ b/kanu/version.py @@ -1 +1 @@ -__version__ = "0.5.0" \ No newline at end of file +__version__ = "0.6.0" \ No newline at end of file