New code struture with separated backend

This commit is contained in:
Saifeddine ALOUI 2023-04-15 13:30:08 +02:00
parent dd7479f786
commit 072f860bb7
11 changed files with 197 additions and 168 deletions

4
.gitignore vendored
View File

@ -150,6 +150,10 @@ configs/*
personalities/* personalities/*
!personalities/gpt4all_chatbot.yaml !personalities/gpt4all_chatbot.yaml
# personalities other than the default one
databases/*
!databases/.keep
# extensions # extensions
extensions/ extensions/
!extensions/.keep !extensions/.keep

178
app.py
View File

@ -13,10 +13,9 @@ import argparse
import json import json
import re import re
import traceback import traceback
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
import sys import sys
from db import DiscussionsDB, Discussion from pyGpt4All.db import DiscussionsDB, Discussion
from flask import ( from flask import (
Flask, Flask,
Response, Response,
@ -26,34 +25,20 @@ from flask import (
stream_with_context, stream_with_context,
send_from_directory send_from_directory
) )
from pyllamacpp.model import Model
from queue import Queue from queue import Queue
from pathlib import Path from pathlib import Path
import gc import gc
app = Flask("GPT4All-WebUI", static_url_path="/static", static_folder="static") app = Flask("GPT4All-WebUI", static_url_path="/static", static_folder="static")
import time import time
from config import load_config, save_config from pyGpt4All.config import load_config, save_config
from pyGpt4All.api import GPT4AllAPI
import shutil import shutil
class Gpt4AllWebUI: class Gpt4AllWebUI(GPT4AllAPI):
def __init__(self, _app, config:dict, personality:dict, config_file_path) -> None: def __init__(self, _app, config:dict, personality:dict, config_file_path) -> None:
self.config = config super().__init__(config, personality, config_file_path)
self.config_file_path = config_file_path
self.personality = personality self.app = _app
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()
# 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( self.add_endpoint(
"/list_models", "list_models", self.list_models, methods=["GET"] "/list_models", "list_models", self.list_models, methods=["GET"]
@ -131,7 +116,6 @@ class Gpt4AllWebUI:
"/help", "help", self.help, methods=["GET"] "/help", "help", self.help, methods=["GET"]
) )
self.prepare_a_new_chatbot()
def list_models(self): def list_models(self):
models_dir = Path('./models') # replace with the actual path to the models folder models_dir = Path('./models') # replace with the actual path to the models folder
@ -161,63 +145,6 @@ class Gpt4AllWebUI:
return jsonify(discussions) 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( def add_endpoint(
self, self,
endpoint=None, endpoint=None,
@ -253,24 +180,6 @@ class Gpt4AllWebUI:
def export_discussion(self): def export_discussion(self):
return jsonify(self.full_message) 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 @stream_with_context
def parse_to_prompt_stream(self, message, message_id): def parse_to_prompt_stream(self, message, message_id):
@ -281,7 +190,7 @@ class Gpt4AllWebUI:
print(f"Received message : {message}") print(f"Received message : {message}")
# First we need to send the new message ID to the client # First we need to send the new message ID to the client
response_id = self.current_discussion.add_message( 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 ) # first the content is empty, but we'll fill it at the end
yield ( yield (
json.dumps( json.dumps(
@ -295,15 +204,9 @@ class Gpt4AllWebUI:
) )
) )
self.current_message = self.personality["message_prefix"] + message + self.personality["message_suffix"] # prepare query and reception
self.full_message += self.current_message self.discussion_messages = self.prepare_query(message_id)
self.full_message_list.append(self.current_message) self.prepare_reception()
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()
self.generating = True self.generating = True
app.config['executor'].submit(self.generate_message) app.config['executor'].submit(self.generate_message)
while self.generating or not self.text_queue.empty(): while self.generating or not self.text_queue.empty():
@ -313,12 +216,8 @@ class Gpt4AllWebUI:
except : except :
time.sleep(1) time.sleep(1)
self.current_discussion.update_message(response_id, self.bot_says) self.current_discussion.update_message(response_id, self.bot_says)
self.full_message_list.append(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) return "\n".join(bot_says)
@ -331,11 +230,13 @@ class Gpt4AllWebUI:
else: else:
self.current_discussion = self.db.load_last_discussion() self.current_discussion = self.db.load_last_discussion()
message = request.json["message"]
message_id = self.current_discussion.add_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']}" message = f"{request.json['message']}"
self.current_message_id = message_id self.current_message_id = message_id
# Segmented (the user receives the output as it comes) # 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 # We will first send a json entry that contains the message id and so on, then the text as it goes
return Response( return Response(
@ -348,19 +249,12 @@ class Gpt4AllWebUI:
def run_to(self): def run_to(self):
data = request.get_json() data = request.get_json()
message_id = data["id"] message_id = data["id"]
self.stop = True 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) # 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 # We will first send a json entry that contains the message id and so on, then the text as it goes
return Response( return Response(
stream_with_context( 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) self.current_discussion.rename(title)
return "renamed successfully" 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): def load_discussion(self):
data = request.get_json() data = request.get_json()
if "id" in data: if "id" in data:
@ -402,15 +278,6 @@ class Gpt4AllWebUI:
messages = self.current_discussion.get_messages() 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) return jsonify(messages)
def delete_discussion(self): def delete_discussion(self):
@ -445,17 +312,8 @@ class Gpt4AllWebUI:
def new_discussion(self): def new_discussion(self):
title = request.args.get("title") title = request.args.get("title")
self.current_discussion = self.db.create_discussion(title) timestamp = self.create_new_discussion(title)
# Get the current timestamp app.config['executor'].submit(self.create_chatbot)
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"])
# Return a success response # Return a success response
return json.dumps({"id": self.current_discussion.discussion_id, "time": timestamp, "welcome_message":self.personality["welcome_message"]}) 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: if self.config['model'] != model:
print("New model selected") print("New model selected")
self.config['model'] = model self.config['model'] = model
self.prepare_a_new_chatbot() self.create_chatbot()
self.config['n_predict'] = int(data["nPredict"]) self.config['n_predict'] = int(data["nPredict"])
self.config['seed'] = int(data["seed"]) self.config['seed'] = int(data["seed"])

View File

@ -1,7 +1,8 @@
config: default config: default
ctx_size: 512 ctx_size: 512
db_path: database.db db_path: databases/database.db
debug: false debug: false
n_threads: 8
host: localhost host: localhost
language: en-US language: en-US
model: gpt4all-lora-quantized-ggml.bin model: gpt4all-lora-quantized-ggml.bin

0
databases/.keep Normal file
View File

View File

@ -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?" 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 # 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 # This suffix is added at the end of any message input by the user
message_suffix: "\ngpt4all: " message_suffix: "\ngpt4all: "

162
pyGpt4All/api.py Normal file
View File

@ -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

View File

@ -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} 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( 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] return [{"id": row[0], "sender": row[1], "content": row[2], "type": row[3], "rank": row[4], "parent": row[5]} for row in rows]

View File

@ -60,6 +60,10 @@ function addMessage(sender, message, id, rank = 0, can_edit = false) {
sendbtn.style.display = "none"; sendbtn.style.display = "none";
waitAnimation.style.display = "block"; waitAnimation.style.display = "block";
// local stuff
let messageTextElement_ = undefined
let hiddenElement_ = undefined
fetch("/run_to", { fetch("/run_to", {
method: 'POST', method: 'POST',
headers: { headers: {
@ -105,10 +109,9 @@ function addMessage(sender, message, id, rank = 0, can_edit = false) {
// We parse it and // We parse it and
infos = JSON.parse(text) infos = JSON.parse(text)
console.log(infos) console.log(infos)
addMessage('User', infos.message, infos.id, 0, can_edit = true);
elements = addMessage(infos.sender, '', infos.response_id, 0, can_edit = true); elements = addMessage(infos.sender, '', infos.response_id, 0, can_edit = true);
messageTextElement = elements['messageTextElement']; messageTextElement_ = elements['messageTextElement'];
hiddenElement = elements['hiddenElement']; hiddenElement_ = elements['hiddenElement'];
entry_counter++; entry_counter++;
} }
else { else {
@ -117,8 +120,8 @@ function addMessage(sender, message, id, rank = 0, can_edit = false) {
txt = hiddenElement.innerHTML; txt = hiddenElement.innerHTML;
if (char != '\f') { if (char != '\f') {
txt += char txt += char
hiddenElement.innerHTML = txt hiddenElement_.innerHTML = txt
messageTextElement.innerHTML = txt.replace(/\n/g, "<br>") messageTextElement_.innerHTML = txt.replace(/\n/g, "<br>")
} }
// scroll to bottom of chat window // scroll to bottom of chat window

View File

@ -16,6 +16,7 @@ function update_main(){
const waitAnimation = document.querySelector("#wait-animation") const waitAnimation = document.querySelector("#wait-animation")
sendbtn.style.display="none"; sendbtn.style.display="none";
waitAnimation.style.display="block"; waitAnimation.style.display="block";
console.log("Sending message to bot")
fetch('/bot', { fetch('/bot', {
method: 'POST', method: 'POST',
headers: { headers: {