diff --git a/.gitignore b/.gitignore index b5e5ead4..3fa045b4 100644 --- a/.gitignore +++ b/.gitignore @@ -150,6 +150,10 @@ configs/* personalities/* !personalities/gpt4all_chatbot.yaml +# personalities other than the default one +databases/* +!databases/.keep + # extensions extensions/ !extensions/.keep diff --git a/app.py b/app.py index a5b89acc..beff611a 100644 --- a/app.py +++ b/app.py @@ -13,10 +13,9 @@ import argparse import json import re import traceback -from datetime import datetime from concurrent.futures import ThreadPoolExecutor import sys -from db import DiscussionsDB, Discussion +from pyGpt4All.db import DiscussionsDB, Discussion from flask import ( Flask, Response, @@ -26,34 +25,20 @@ from flask import ( stream_with_context, send_from_directory ) -from pyllamacpp.model import Model from queue import Queue from pathlib import Path import gc app = Flask("GPT4All-WebUI", static_url_path="/static", static_folder="static") import time -from config import load_config, save_config +from pyGpt4All.config import load_config, save_config +from pyGpt4All.api import GPT4AllAPI import shutil -class Gpt4AllWebUI: - +class Gpt4AllWebUI(GPT4AllAPI): def __init__(self, _app, config:dict, personality:dict, config_file_path) -> None: - self.config = config - self.config_file_path = config_file_path - self.personality = personality - self.current_discussion = None - self.current_message_id = 0 - self.app = _app - self.db_path = config["db_path"] - self.db = DiscussionsDB(self.db_path) - # If the database is empty, populate it with tables - self.db.populate() + super().__init__(config, personality, config_file_path) + + self.app = _app - # workaround for non interactive mode - self.full_message = "" - self.full_message_list = [] - self.prompt_message = "" - # This is the queue used to stream text to the ui as the bot spits out its response - self.text_queue = Queue(0) self.add_endpoint( "/list_models", "list_models", self.list_models, methods=["GET"] @@ -131,7 +116,6 @@ class Gpt4AllWebUI: "/help", "help", self.help, methods=["GET"] ) - self.prepare_a_new_chatbot() def list_models(self): models_dir = Path('./models') # replace with the actual path to the models folder @@ -161,63 +145,6 @@ class Gpt4AllWebUI: return jsonify(discussions) - def prepare_a_new_chatbot(self): - # Create chatbot - self.chatbot_bindings = self.create_chatbot() - - - def create_chatbot(self): - try: - return Model( - ggml_model=f"./models/{self.config['model']}", - n_ctx=self.config['ctx_size'], - seed=self.config['seed'], - ) - except Exception as ex: - print(f"Exception {ex}") - return None - def condition_chatbot(self, conditionning_message): - if self.current_discussion is None: - self.current_discussion = self.db.load_last_discussion() - - message_id = self.current_discussion.add_message( - "conditionner", - conditionning_message, - DiscussionsDB.MSG_TYPE_CONDITIONNING, - 0, - self.current_message_id - ) - self.current_message_id = message_id - if self.personality["welcome_message"]!="": - message_id = self.current_discussion.add_message( - self.personality["name"], self.personality["welcome_message"], - DiscussionsDB.MSG_TYPE_NORMAL, - 0, - self.current_message_id - ) - - self.current_message_id = message_id - return message_id - - def prepare_query(self): - self.bot_says = "" - self.full_text = "" - self.is_bot_text_started = False - #self.current_message = message - - def new_text_callback(self, text: str): - print(text, end="") - sys.stdout.flush() - self.full_text += text - if self.is_bot_text_started: - self.bot_says += text - self.full_message += text - self.text_queue.put(text) - - #if self.current_message in self.full_text: - if len(self.prompt_message) < len(self.full_text): - self.is_bot_text_started = True - def add_endpoint( self, endpoint=None, @@ -253,24 +180,6 @@ class Gpt4AllWebUI: def export_discussion(self): return jsonify(self.full_message) - def generate_message(self): - self.generating=True - self.text_queue=Queue() - gc.collect() - - self.chatbot_bindings.generate( - self.prompt_message,#self.full_message,#self.current_message, - new_text_callback=self.new_text_callback, - n_predict=len(self.current_message)+self.config['n_predict'], - temp=self.config['temp'], - top_k=self.config['top_k'], - top_p=self.config['top_p'], - repeat_penalty=self.config['repeat_penalty'], - repeat_last_n = self.config['repeat_last_n'], - #seed=self.config['seed'], - n_threads=8 - ) - self.generating=False @stream_with_context def parse_to_prompt_stream(self, message, message_id): @@ -281,7 +190,7 @@ class Gpt4AllWebUI: print(f"Received message : {message}") # First we need to send the new message ID to the client response_id = self.current_discussion.add_message( - self.personality["name"], "" + self.personality["name"], "", parent = message_id ) # first the content is empty, but we'll fill it at the end yield ( json.dumps( @@ -295,15 +204,9 @@ class Gpt4AllWebUI: ) ) - self.current_message = self.personality["message_prefix"] + message + self.personality["message_suffix"] - self.full_message += self.current_message - self.full_message_list.append(self.current_message) - - if len(self.full_message_list) > self.config["nb_messages_to_remember"]: - self.prompt_message = self.personality["personality_conditionning"]+ '\n'.join(self.full_message_list[-self.config["nb_messages_to_remember"]:]) - else: - self.prompt_message = self.full_message - self.prepare_query() + # prepare query and reception + self.discussion_messages = self.prepare_query(message_id) + self.prepare_reception() self.generating = True app.config['executor'].submit(self.generate_message) while self.generating or not self.text_queue.empty(): @@ -313,12 +216,8 @@ class Gpt4AllWebUI: except : time.sleep(1) - - self.current_discussion.update_message(response_id, self.bot_says) self.full_message_list.append(self.bot_says) - #yield self.bot_says# .encode('utf-8').decode('utf-8') - # TODO : change this to use the yield version in order to send text word by word return "\n".join(bot_says) @@ -331,11 +230,13 @@ class Gpt4AllWebUI: else: self.current_discussion = self.db.load_last_discussion() + message = request.json["message"] message_id = self.current_discussion.add_message( - "user", request.json["message"], parent=self.current_message_id + "user", message, parent=self.current_message_id ) message = f"{request.json['message']}" self.current_message_id = message_id + # Segmented (the user receives the output as it comes) # We will first send a json entry that contains the message id and so on, then the text as it goes return Response( @@ -348,19 +249,12 @@ class Gpt4AllWebUI: def run_to(self): data = request.get_json() message_id = data["id"] - self.stop = True - message_id = self.current_discussion.add_message( - "user", request.json["message"], parent=message_id - ) - - message = f"{request.json['message']}" - # Segmented (the user receives the output as it comes) # We will first send a json entry that contains the message id and so on, then the text as it goes return Response( stream_with_context( - self.parse_to_prompt_stream(message, message_id) + self.parse_to_prompt_stream("",message_id) ) ) @@ -370,24 +264,6 @@ class Gpt4AllWebUI: self.current_discussion.rename(title) return "renamed successfully" - def restore_discussion(self, full_message): - self.prompt_message = full_message - - if len(self.full_message_list)>5: - self.prompt_message = "\n".join(self.full_message_list[-5:]) - - self.chatbot_bindings.generate( - self.prompt_message,#full_message, - new_text_callback=self.new_text_callback, - n_predict=0,#len(full_message), - temp=self.config['temp'], - top_k=self.config['top_k'], - top_p=self.config['top_p'], - repeat_penalty= self.config['repeat_penalty'], - repeat_last_n = self.config['repeat_last_n'], - n_threads=8 - ) - def load_discussion(self): data = request.get_json() if "id" in data: @@ -402,15 +278,6 @@ class Gpt4AllWebUI: messages = self.current_discussion.get_messages() - self.full_message = "" - self.full_message_list = [] - for message in messages: - if message['sender']!="conditionner": - self.full_message += message['sender'] + ": " + message['content'] + "\n" - self.full_message_list.append(message['sender'] + ": " + message['content']) - self.current_message_id=message['id'] - app.config['executor'].submit(self.restore_discussion, self.full_message) - return jsonify(messages) def delete_discussion(self): @@ -445,17 +312,8 @@ class Gpt4AllWebUI: def new_discussion(self): title = request.args.get("title") - self.current_discussion = self.db.create_discussion(title) - # Get the current timestamp - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - app.config['executor'].submit(self.prepare_a_new_chatbot) - - self.full_message ="" - - # Chatbot conditionning - self.condition_chatbot(self.personality["personality_conditionning"]) - + timestamp = self.create_new_discussion(title) + app.config['executor'].submit(self.create_chatbot) # Return a success response return json.dumps({"id": self.current_discussion.discussion_id, "time": timestamp, "welcome_message":self.personality["welcome_message"]}) @@ -466,7 +324,7 @@ class Gpt4AllWebUI: if self.config['model'] != model: print("New model selected") self.config['model'] = model - self.prepare_a_new_chatbot() + self.create_chatbot() self.config['n_predict'] = int(data["nPredict"]) self.config['seed'] = int(data["seed"]) diff --git a/configs/default.yaml b/configs/default.yaml index 8f92bed8..c23af456 100644 --- a/configs/default.yaml +++ b/configs/default.yaml @@ -1,7 +1,8 @@ config: default ctx_size: 512 -db_path: database.db +db_path: databases/database.db debug: false +n_threads: 8 host: localhost language: en-US model: gpt4all-lora-quantized-ggml.bin diff --git a/databases/.keep b/databases/.keep new file mode 100644 index 00000000..e69de29b diff --git a/personalities/gpt4all_chatbot.yaml b/personalities/gpt4all_chatbot.yaml index 195461c5..527ab018 100644 --- a/personalities/gpt4all_chatbot.yaml +++ b/personalities/gpt4all_chatbot.yaml @@ -27,7 +27,7 @@ personality_conditionning: | welcome_message: "Welcome! I am GPT4All A free and open discussion AI. What can I do for you today?" # This prefix is added at the beginning of any message input by the user -message_prefix: "\nuser: " +message_prefix: "user: " # This suffix is added at the end of any message input by the user message_suffix: "\ngpt4all: " diff --git a/pyGpt4All/api.py b/pyGpt4All/api.py new file mode 100644 index 00000000..793b32a6 --- /dev/null +++ b/pyGpt4All/api.py @@ -0,0 +1,162 @@ +###### +# Project : GPT4ALL-UI +# File : api.py +# Author : ParisNeo with the help of the community +# Supported by Nomic-AI +# Licence : Apache 2.0 +# Description : +# A simple api to communicate with gpt4all-ui and its models. +###### +import gc +import sys +from queue import Queue +from datetime import datetime +from pyllamacpp.model import Model +from pyGpt4All.db import DiscussionsDB + +class GPT4AllAPI(): + def __init__(self, config:dict, personality:dict, config_file_path) -> None: + self.config = config + self.personality = personality + self.config_file_path = config_file_path + + # This is the queue used to stream text to the ui as the bot spits out its response + self.text_queue = Queue(0) + + # Keeping track of current discussion and message + self.current_discussion = None + self.current_message_id = 0 + + self.db_path = config["db_path"] + + # Create database object + self.db = DiscussionsDB(self.db_path) + + # If the database is empty, populate it with tables + self.db.populate() + + # This is used to keep track of messages + self.full_message_list = [] + + # Build chatbot + self.chatbot_bindings = self.create_chatbot() + print("Chatbot created successfully") + + # tests the model + """ + self.prepare_reception() + self.discussion_messages = "Instruction: Act as gpt4all. A kind and helpful AI bot built to help users solve problems.\nuser: how to build a water rocket?\ngpt4all:" + self.chatbot_bindings.generate( + self.discussion_messages, + new_text_callback=self.new_text_callback, + n_predict=372, + temp=self.config['temp'], + top_k=self.config['top_k'], + top_p=self.config['top_p'], + repeat_penalty=self.config['repeat_penalty'], + repeat_last_n = self.config['repeat_last_n'], + #seed=self.config['seed'], + n_threads=self.config['n_threads'] + ) + + """ + + # generation status + self.generating=False + + def create_chatbot(self): + try: + return Model( + ggml_model=f"./models/{self.config['model']}", + n_ctx=self.config['ctx_size'], + seed=self.config['seed'], + ) + except Exception as ex: + print(f"Exception {ex}") + return None + + def condition_chatbot(self, conditionning_message): + if self.current_discussion is None: + self.current_discussion = self.db.load_last_discussion() + + message_id = self.current_discussion.add_message( + "conditionner", + conditionning_message, + DiscussionsDB.MSG_TYPE_CONDITIONNING, + 0, + 0 + ) + self.current_message_id = message_id + if self.personality["welcome_message"]!="": + message_id = self.current_discussion.add_message( + self.personality["name"], self.personality["welcome_message"], + DiscussionsDB.MSG_TYPE_NORMAL, + 0, + self.current_message_id + ) + + self.current_message_id = message_id + return message_id + + def prepare_reception(self): + self.bot_says = "" + self.full_text = "" + self.is_bot_text_started = False + #self.current_message = message + + def create_new_discussion(self, title): + self.current_discussion = self.db.create_discussion(title) + # Get the current timestamp + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Chatbot conditionning + self.condition_chatbot(self.personality["personality_conditionning"]) + return timestamp + + def prepare_query(self, message_id=-1): + messages = self.current_discussion.get_messages() + self.full_message_list = [] + for message in messages: + if message["id"]<= message_id or message_id==-1: + if message["type"]!=self.db.MSG_TYPE_CONDITIONNING: + if message["sender"]==self.personality["name"]: + self.full_message_list.append(message["content"]) + else: + self.full_message_list.append(self.personality["message_prefix"] + message["content"] + self.personality["message_suffix"]) + + if len(self.full_message_list) > self.config["nb_messages_to_remember"]: + discussion_messages = self.personality["personality_conditionning"]+ '\n'.join(self.full_message_list[-self.config["nb_messages_to_remember"]:]) + else: + discussion_messages = self.personality["personality_conditionning"]+ '\n'.join(self.full_message_list) + return discussion_messages[:-1] # Removes the last return + + def new_text_callback(self, text: str): + print(text, end="") + sys.stdout.flush() + self.full_text += text + if self.is_bot_text_started: + self.bot_says += text + self.text_queue.put(text) + + #if self.current_message in self.full_text: + if len(self.discussion_messages) < len(self.full_text): + self.is_bot_text_started = True + + def generate_message(self): + self.generating=True + self.text_queue=Queue() + gc.collect() + total_n_predict = len(self.discussion_messages)+self.config['n_predict'] + self.chatbot_bindings.generate( + self.discussion_messages, + new_text_callback=self.new_text_callback, + n_predict=total_n_predict, + temp=self.config['temp'], + top_k=self.config['top_k'], + top_p=self.config['top_p'], + repeat_penalty=self.config['repeat_penalty'], + repeat_last_n = self.config['repeat_last_n'], + #seed=self.config['seed'], + n_threads=self.config['n_threads'] + ) + self.generating=False diff --git a/config.py b/pyGpt4All/config.py similarity index 100% rename from config.py rename to pyGpt4All/config.py diff --git a/db.py b/pyGpt4All/db.py similarity index 99% rename from db.py rename to pyGpt4All/db.py index d95b7d21..63444ff0 100644 --- a/db.py +++ b/pyGpt4All/db.py @@ -256,7 +256,7 @@ class Discussion: list: List of entries in the format {"id":message id, "sender":sender name, "content":message content, "type":message type, "rank": message rank} """ rows = self.discussions_db.select( - f"SELECT * FROM message WHERE discussion_id={self.discussion_id}" + "SELECT * FROM message WHERE discussion_id=?", (self.discussion_id,) ) return [{"id": row[0], "sender": row[1], "content": row[2], "type": row[3], "rank": row[4], "parent": row[5]} for row in rows] diff --git a/extension.py b/pyGpt4All/extension.py similarity index 100% rename from extension.py rename to pyGpt4All/extension.py diff --git a/static/js/chat.js b/static/js/chat.js index 6f9f30e8..4dc571b2 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -60,6 +60,10 @@ function addMessage(sender, message, id, rank = 0, can_edit = false) { sendbtn.style.display = "none"; waitAnimation.style.display = "block"; + // local stuff + let messageTextElement_ = undefined + let hiddenElement_ = undefined + fetch("/run_to", { method: 'POST', headers: { @@ -105,10 +109,9 @@ function addMessage(sender, message, id, rank = 0, can_edit = false) { // We parse it and infos = JSON.parse(text) console.log(infos) - addMessage('User', infos.message, infos.id, 0, can_edit = true); elements = addMessage(infos.sender, '', infos.response_id, 0, can_edit = true); - messageTextElement = elements['messageTextElement']; - hiddenElement = elements['hiddenElement']; + messageTextElement_ = elements['messageTextElement']; + hiddenElement_ = elements['hiddenElement']; entry_counter++; } else { @@ -117,8 +120,8 @@ function addMessage(sender, message, id, rank = 0, can_edit = false) { txt = hiddenElement.innerHTML; if (char != '\f') { txt += char - hiddenElement.innerHTML = txt - messageTextElement.innerHTML = txt.replace(/\n/g, "
") + hiddenElement_.innerHTML = txt + messageTextElement_.innerHTML = txt.replace(/\n/g, "
") } // scroll to bottom of chat window diff --git a/static/js/main.js b/static/js/main.js index e7905bf2..79c8acf7 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -16,6 +16,7 @@ function update_main(){ const waitAnimation = document.querySelector("#wait-animation") sendbtn.style.display="none"; waitAnimation.style.display="block"; + console.log("Sending message to bot") fetch('/bot', { method: 'POST', headers: {