From e85a583f0ae109701096b08af5bcccffe3204ae4 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 26 Sep 2023 10:03:22 +0100 Subject: [PATCH] testings --- application/api/__init__.py | 0 application/api/answer/__init__.py | 0 application/api/answer/routes.py | 163 ++++++++++++ application/api/internal/__init__.py | 0 application/api/internal/routes.py | 66 +++++ application/api/user/__init__.py | 0 application/api/user/routes.py | 212 +++++++++++++++ application/app.py | 383 +-------------------------- application/celery.py | 10 + application/llm/__init__.py | 0 application/llm/base.py | 15 ++ application/llm/openai.py | 34 +++ application/vectorstore/__init__.py | 0 application/vectorstore/base.py | 0 frontend/.env.development | 3 +- 15 files changed, 514 insertions(+), 372 deletions(-) create mode 100644 application/api/__init__.py create mode 100644 application/api/answer/__init__.py create mode 100644 application/api/answer/routes.py create mode 100644 application/api/internal/__init__.py create mode 100644 application/api/internal/routes.py create mode 100644 application/api/user/__init__.py create mode 100644 application/api/user/routes.py create mode 100644 application/celery.py create mode 100644 application/llm/__init__.py create mode 100644 application/llm/base.py create mode 100644 application/llm/openai.py create mode 100644 application/vectorstore/__init__.py create mode 100644 application/vectorstore/base.py diff --git a/application/api/__init__.py b/application/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/application/api/answer/__init__.py b/application/api/answer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py new file mode 100644 index 00000000..5f04678d --- /dev/null +++ b/application/api/answer/routes.py @@ -0,0 +1,163 @@ +import os +from flask import Blueprint, request, jsonify, Response +import requests +import json +import datetime + +from langchain.chat_models import AzureChatOpenAI +from pymongo import MongoClient +from bson.objectid import ObjectId +from werkzeug.utils import secure_filename +import http.client + +from application.app import (logger, count_tokens, chat_combine_template, gpt_model, + api_key_set, embeddings_key_set, get_docsearch, get_vectorstore) +from application.core.settings import settings +from application.llm.openai import OpenAILLM + + +mongo = MongoClient(settings.MONGO_URI) +db = mongo["docsgpt"] +conversations_collection = db["conversations"] +vectors_collection = db["vectors"] +answer = Blueprint('answer', __name__) + +def is_azure_configured(): + return settings.OPENAI_API_BASE and settings.OPENAI_API_VERSION and settings.AZURE_DEPLOYMENT_NAME +def complete_stream(question, docsearch, chat_history, api_key, conversation_id): + + # openai.api_key = api_key + + if is_azure_configured(): + # logger.debug("in Azure") + # openai.api_type = "azure" + # openai.api_version = settings.OPENAI_API_VERSION + # openai.api_base = settings.OPENAI_API_BASE + # llm = AzureChatOpenAI( + # openai_api_key=api_key, + # openai_api_base=settings.OPENAI_API_BASE, + # openai_api_version=settings.OPENAI_API_VERSION, + # deployment_name=settings.AZURE_DEPLOYMENT_NAME, + # ) + llm = OpenAILLM(api_key=api_key) + else: + logger.debug("plain OpenAI") + llm = OpenAILLM(api_key=api_key) + # llm = ChatOpenAI(openai_api_key=api_key) + docs = docsearch.similarity_search(question, k=2) + # join all page_content together with a newline + docs_together = "\n".join([doc.page_content for doc in docs]) + p_chat_combine = chat_combine_template.replace("{summaries}", docs_together) + messages_combine = [{"role": "system", "content": p_chat_combine}] + source_log_docs = [] + for doc in docs: + if doc.metadata: + data = json.dumps({"type": "source", "doc": doc.page_content, "metadata": doc.metadata}) + source_log_docs.append({"title": doc.metadata['title'].split('/')[-1], "text": doc.page_content}) + else: + data = json.dumps({"type": "source", "doc": doc.page_content}) + source_log_docs.append({"title": doc.page_content, "text": doc.page_content}) + yield f"data:{data}\n\n" + + if len(chat_history) > 1: + tokens_current_history = 0 + # count tokens in history + chat_history.reverse() + for i in chat_history: + if "prompt" in i and "response" in i: + tokens_batch = count_tokens(i["prompt"]) + count_tokens(i["response"]) + if tokens_current_history + tokens_batch < settings.TOKENS_MAX_HISTORY: + tokens_current_history += tokens_batch + messages_combine.append({"role": "user", "content": i["prompt"]}) + messages_combine.append({"role": "system", "content": i["response"]}) + messages_combine.append({"role": "user", "content": question}) + # completion = openai.ChatCompletion.create(model=gpt_model, engine=settings.AZURE_DEPLOYMENT_NAME, + # messages=messages_combine, stream=True, max_tokens=500, temperature=0) + import sys + print(api_key) + reponse_full = "" + # for line in completion: + # if "content" in line["choices"][0]["delta"]: + # # check if the delta contains content + # data = json.dumps({"answer": str(line["choices"][0]["delta"]["content"])}) + # reponse_full += str(line["choices"][0]["delta"]["content"]) + # yield f"data: {data}\n\n" + # reponse_full = "" + print(llm) + completion = llm.gen_stream(model=gpt_model, engine=settings.AZURE_DEPLOYMENT_NAME, + messages=messages_combine) + for line in completion: + + data = json.dumps({"answer": str(line)}) + reponse_full += str(line) + yield f"data: {data}\n\n" + + + # save conversation to database + if conversation_id is not None: + conversations_collection.update_one( + {"_id": ObjectId(conversation_id)}, + {"$push": {"queries": {"prompt": question, "response": reponse_full, "sources": source_log_docs}}}, + ) + + else: + # create new conversation + # generate summary + messages_summary = [{"role": "assistant", "content": "Summarise following conversation in no more than 3 " + "words, respond ONLY with the summary, use the same " + "language as the system \n\nUser: " + question + "\n\n" + + "AI: " + + reponse_full}, + {"role": "user", "content": "Summarise following conversation in no more than 3 words, " + "respond ONLY with the summary, use the same language as the " + "system"}] + # completion = openai.ChatCompletion.create(model='gpt-3.5-turbo', engine=settings.AZURE_DEPLOYMENT_NAME, + # messages=messages_summary, max_tokens=30, temperature=0) + completion = llm.gen(model=gpt_model, engine=settings.AZURE_DEPLOYMENT_NAME, + messages=messages_combine, max_tokens=30) + conversation_id = conversations_collection.insert_one( + {"user": "local", + "date": datetime.datetime.utcnow(), + "name": completion["choices"][0]["message"]["content"], + "queries": [{"prompt": question, "response": reponse_full, "sources": source_log_docs}]} + ).inserted_id + + # send data.type = "end" to indicate that the stream has ended as json + data = json.dumps({"type": "id", "id": str(conversation_id)}) + yield f"data: {data}\n\n" + data = json.dumps({"type": "end"}) + yield f"data: {data}\n\n" + + +@answer.route("/stream", methods=["POST"]) +def stream(): + data = request.get_json() + # get parameter from url question + question = data["question"] + history = data["history"] + # history to json object from string + history = json.loads(history) + conversation_id = data["conversation_id"] + + # check if active_docs is set + + if not api_key_set: + api_key = data["api_key"] + else: + api_key = settings.API_KEY + if not embeddings_key_set: + embeddings_key = data["embeddings_key"] + else: + embeddings_key = settings.EMBEDDINGS_KEY + if "active_docs" in data: + vectorstore = get_vectorstore({"active_docs": data["active_docs"]}) + else: + vectorstore = "" + docsearch = get_docsearch(vectorstore, embeddings_key) + + # question = "Hi" + return Response( + complete_stream(question, docsearch, + chat_history=history, api_key=api_key, + conversation_id=conversation_id), mimetype="text/event-stream" + ) \ No newline at end of file diff --git a/application/api/internal/__init__.py b/application/api/internal/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/application/api/internal/routes.py b/application/api/internal/routes.py new file mode 100644 index 00000000..5092f9be --- /dev/null +++ b/application/api/internal/routes.py @@ -0,0 +1,66 @@ +import os +import datetime +from flask import Blueprint, request, jsonify, send_from_directory +import requests +from pymongo import MongoClient +from werkzeug.utils import secure_filename + + +from application.core.settings import settings +mongo = MongoClient(settings.MONGO_URI) +db = mongo["docsgpt"] +conversations_collection = db["conversations"] +vectors_collection = db["vectors"] + +internal = Blueprint('internal', __name__) +@internal.route("/api/download", methods=["get"]) +def download_file(): + user = secure_filename(request.args.get("user")) + job_name = secure_filename(request.args.get("name")) + filename = secure_filename(request.args.get("file")) + save_dir = os.path.join(app.config["UPLOAD_FOLDER"], user, job_name) + return send_from_directory(save_dir, filename, as_attachment=True) + + + +@internal.route("/api/upload_index", methods=["POST"]) +def upload_index_files(): + """Upload two files(index.faiss, index.pkl) to the user's folder.""" + if "user" not in request.form: + return {"status": "no user"} + user = secure_filename(request.form["user"]) + if "name" not in request.form: + return {"status": "no name"} + job_name = secure_filename(request.form["name"]) + if "file_faiss" not in request.files: + print("No file part") + return {"status": "no file"} + file_faiss = request.files["file_faiss"] + if file_faiss.filename == "": + return {"status": "no file name"} + if "file_pkl" not in request.files: + print("No file part") + return {"status": "no file"} + file_pkl = request.files["file_pkl"] + if file_pkl.filename == "": + return {"status": "no file name"} + + # saves index files + save_dir = os.path.join("indexes", user, job_name) + if not os.path.exists(save_dir): + os.makedirs(save_dir) + file_faiss.save(os.path.join(save_dir, "index.faiss")) + file_pkl.save(os.path.join(save_dir, "index.pkl")) + # create entry in vectors_collection + vectors_collection.insert_one( + { + "user": user, + "name": job_name, + "language": job_name, + "location": save_dir, + "date": datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S"), + "model": settings.EMBEDDINGS_NAME, + "type": "local", + } + ) + return {"status": "ok"} \ No newline at end of file diff --git a/application/api/user/__init__.py b/application/api/user/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/application/api/user/routes.py b/application/api/user/routes.py new file mode 100644 index 00000000..d661af41 --- /dev/null +++ b/application/api/user/routes.py @@ -0,0 +1,212 @@ +import os +from flask import Blueprint, request, jsonify +import requests +import json +from pymongo import MongoClient +from bson.objectid import ObjectId +from werkzeug.utils import secure_filename +import http.client + +from application.core.settings import settings +mongo = MongoClient(settings.MONGO_URI) +db = mongo["docsgpt"] +conversations_collection = db["conversations"] +vectors_collection = db["vectors"] +user = Blueprint('user', __name__) + +@user.route("/api/delete_conversation", methods=["POST"]) +def delete_conversation(): + # deletes a conversation from the database + conversation_id = request.args.get("id") + # write to mongodb + conversations_collection.delete_one( + { + "_id": ObjectId(conversation_id), + } + ) + + return {"status": "ok"} + +@user.route("/api/get_conversations", methods=["get"]) +def get_conversations(): + # provides a list of conversations + conversations = conversations_collection.find().sort("date", -1) + list_conversations = [] + for conversation in conversations: + list_conversations.append({"id": str(conversation["_id"]), "name": conversation["name"]}) + + #list_conversations = [{"id": "default", "name": "default"}, {"id": "jeff", "name": "jeff"}] + + return jsonify(list_conversations) + + +@user.route("/api/get_single_conversation", methods=["get"]) +def get_single_conversation(): + # provides data for a conversation + conversation_id = request.args.get("id") + conversation = conversations_collection.find_one({"_id": ObjectId(conversation_id)}) + return jsonify(conversation['queries']) + + +@user.route("/api/feedback", methods=["POST"]) +def api_feedback(): + data = request.get_json() + question = data["question"] + answer = data["answer"] + feedback = data["feedback"] + + print("-" * 5) + print("Question: " + question) + print("Answer: " + answer) + print("Feedback: " + feedback) + print("-" * 5) + response = requests.post( + url="https://86x89umx77.execute-api.eu-west-2.amazonaws.com/docsgpt-feedback", + headers={ + "Content-Type": "application/json; charset=utf-8", + }, + data=json.dumps({"answer": answer, "question": question, "feedback": feedback}), + ) + return {"status": http.client.responses.get(response.status_code, "ok")} + + +@user.route("/api/delete_old", methods=["get"]) +def delete_old(): + """Delete old indexes.""" + import shutil + + path = request.args.get("path") + dirs = path.split("/") + dirs_clean = [] + for i in range(1, len(dirs)): + dirs_clean.append(secure_filename(dirs[i])) + # check that path strats with indexes or vectors + if dirs[0] not in ["indexes", "vectors"]: + return {"status": "error"} + path_clean = "/".join(dirs) + vectors_collection.delete_one({"location": path}) + try: + shutil.rmtree(path_clean) + except FileNotFoundError: + pass + return {"status": "ok"} + +@user.route("/api/upload", methods=["POST"]) +def upload_file(): + """Upload a file to get vectorized and indexed.""" + if "user" not in request.form: + return {"status": "no user"} + user = secure_filename(request.form["user"]) + if "name" not in request.form: + return {"status": "no name"} + job_name = secure_filename(request.form["name"]) + # check if the post request has the file part + if "file" not in request.files: + print("No file part") + return {"status": "no file"} + file = request.files["file"] + if file.filename == "": + return {"status": "no file name"} + + if file: + filename = secure_filename(file.filename) + # save dir + save_dir = os.path.join(app.config["UPLOAD_FOLDER"], user, job_name) + # create dir if not exists + if not os.path.exists(save_dir): + os.makedirs(save_dir) + + file.save(os.path.join(save_dir, filename)) + task = ingest.delay("temp", [".rst", ".md", ".pdf", ".txt"], job_name, filename, user) + # task id + task_id = task.id + return {"status": "ok", "task_id": task_id} + else: + return {"status": "error"} + +@user.route("/api/task_status", methods=["GET"]) +def task_status(): + """Get celery job status.""" + task_id = request.args.get("task_id") + task = AsyncResult(task_id) + task_meta = task.info + return {"status": task.status, "result": task_meta} + + +@user.route("/api/combine", methods=["GET"]) +def combined_json(): + user = "local" + """Provide json file with combined available indexes.""" + # get json from https://d3dg1063dc54p9.cloudfront.net/combined.json + + data = [ + { + "name": "default", + "language": "default", + "version": "", + "description": "default", + "fullName": "default", + "date": "default", + "docLink": "default", + "model": settings.EMBEDDINGS_NAME, + "location": "local", + } + ] + # structure: name, language, version, description, fullName, date, docLink + # append data from vectors_collection + for index in vectors_collection.find({"user": user}): + data.append( + { + "name": index["name"], + "language": index["language"], + "version": "", + "description": index["name"], + "fullName": index["name"], + "date": index["date"], + "docLink": index["location"], + "model": settings.EMBEDDINGS_NAME, + "location": "local", + } + ) + + data_remote = requests.get("https://d3dg1063dc54p9.cloudfront.net/combined.json").json() + for index in data_remote: + index["location"] = "remote" + data.append(index) + + return jsonify(data) + + +@user.route("/api/docs_check", methods=["POST"]) +def check_docs(): + # check if docs exist in a vectorstore folder + data = request.get_json() + # split docs on / and take first part + if data["docs"].split("/")[0] == "local": + return {"status": "exists"} + vectorstore = "vectors/" + data["docs"] + base_path = "https://raw.githubusercontent.com/arc53/DocsHUB/main/" + if os.path.exists(vectorstore) or data["docs"] == "default": + return {"status": "exists"} + else: + r = requests.get(base_path + vectorstore + "index.faiss") + + if r.status_code != 200: + return {"status": "null"} + else: + if not os.path.exists(vectorstore): + os.makedirs(vectorstore) + with open(vectorstore + "index.faiss", "wb") as f: + f.write(r.content) + + # download the store + r = requests.get(base_path + vectorstore + "index.pkl") + with open(vectorstore + "index.pkl", "wb") as f: + f.write(r.content) + + return {"status": "loaded"} + + + + + diff --git a/application/app.py b/application/app.py index 39ac744f..c4b2c314 100644 --- a/application/app.py +++ b/application/app.py @@ -1,6 +1,5 @@ import asyncio import datetime -import http.client import json import logging import os @@ -40,6 +39,9 @@ from application.core.settings import settings from application.error import bad_request from application.worker import ingest_worker from bson.objectid import ObjectId +from application.api.user.routes import user +from application.api.answer.routes import answer +from transformers import GPT2TokenizerFast # os.environ["LANGCHAIN_HANDLER"] = "langchain" @@ -49,12 +51,11 @@ if settings.LLM_NAME == "gpt4": else: gpt_model = 'gpt-3.5-turbo' - if settings.SELF_HOSTED_MODEL: from langchain.llms import HuggingFacePipeline from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline - model_id = settings.LLM_NAME # hf model id (Arc53/docsgpt-7b-falcon, Arc53/docsgpt-14b) + model_id = settings.LLM_NAME # hf model id (Arc53/docsgpt-7b-falcon, Arc53/docsgpt-14b) tokenizer = AutoTokenizer.from_pretrained(model_id) model = AutoModelForCausalLM.from_pretrained(model_id) pipe = pipeline( @@ -96,6 +97,8 @@ api_key_set = settings.API_KEY is not None embeddings_key_set = settings.EMBEDDINGS_KEY is not None app = Flask(__name__) +app.register_blueprint(user) +app.register_blueprint(answer) app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER = "inputs" app.config["CELERY_BROKER_URL"] = settings.CELERY_BROKER_URL app.config["CELERY_RESULT_BACKEND"] = settings.CELERY_RESULT_BACKEND @@ -112,6 +115,10 @@ async def async_generate(chain, question, chat_history): result = await chain.arun({"question": question, "chat_history": chat_history}) return result +def count_tokens(string): + + tokenizer = GPT2TokenizerFast.from_pretrained('gpt2') + return len(tokenizer(string)['input_ids']) def run_async_chain(chain, question, chat_history): loop = asyncio.new_event_loop() @@ -179,124 +186,6 @@ def home(): return 'Welcome to DocsGPT Backend!' -def complete_stream(question, docsearch, chat_history, api_key, conversation_id): - openai.api_key = api_key - if is_azure_configured(): - logger.debug("in Azure") - openai.api_type = "azure" - openai.api_version = settings.OPENAI_API_VERSION - openai.api_base = settings.OPENAI_API_BASE - llm = AzureChatOpenAI( - openai_api_key=api_key, - openai_api_base=settings.OPENAI_API_BASE, - openai_api_version=settings.OPENAI_API_VERSION, - deployment_name=settings.AZURE_DEPLOYMENT_NAME, - ) - else: - logger.debug("plain OpenAI") - llm = ChatOpenAI(openai_api_key=api_key) - docs = docsearch.similarity_search(question, k=2) - # join all page_content together with a newline - docs_together = "\n".join([doc.page_content for doc in docs]) - p_chat_combine = chat_combine_template.replace("{summaries}", docs_together) - messages_combine = [{"role": "system", "content": p_chat_combine}] - source_log_docs = [] - for doc in docs: - if doc.metadata: - data = json.dumps({"type": "source", "doc": doc.page_content, "metadata": doc.metadata}) - source_log_docs.append({"title": doc.metadata['title'].split('/')[-1], "text": doc.page_content}) - else: - data = json.dumps({"type": "source", "doc": doc.page_content}) - source_log_docs.append({"title": doc.page_content, "text": doc.page_content}) - yield f"data:{data}\n\n" - - if len(chat_history) > 1: - tokens_current_history = 0 - # count tokens in history - chat_history.reverse() - for i in chat_history: - if "prompt" in i and "response" in i: - tokens_batch = llm.get_num_tokens(i["prompt"]) + llm.get_num_tokens(i["response"]) - if tokens_current_history + tokens_batch < settings.TOKENS_MAX_HISTORY: - tokens_current_history += tokens_batch - messages_combine.append({"role": "user", "content": i["prompt"]}) - messages_combine.append({"role": "system", "content": i["response"]}) - messages_combine.append({"role": "user", "content": question}) - completion = openai.ChatCompletion.create(model=gpt_model, engine=settings.AZURE_DEPLOYMENT_NAME, - messages=messages_combine, stream=True, max_tokens=500, temperature=0) - reponse_full = "" - for line in completion: - if "content" in line["choices"][0]["delta"]: - # check if the delta contains content - data = json.dumps({"answer": str(line["choices"][0]["delta"]["content"])}) - reponse_full += str(line["choices"][0]["delta"]["content"]) - yield f"data: {data}\n\n" - # save conversation to database - if conversation_id is not None: - conversations_collection.update_one( - {"_id": ObjectId(conversation_id)}, - {"$push": {"queries": {"prompt": question, "response": reponse_full, "sources": source_log_docs}}}, - ) - - else: - # create new conversation - # generate summary - messages_summary = [{"role": "assistant", "content": "Summarise following conversation in no more than 3 " - "words, respond ONLY with the summary, use the same " - "language as the system \n\nUser: " + question + "\n\n" + - "AI: " + - reponse_full}, - {"role": "user", "content": "Summarise following conversation in no more than 3 words, " - "respond ONLY with the summary, use the same language as the " - "system"}] - completion = openai.ChatCompletion.create(model='gpt-3.5-turbo', engine=settings.AZURE_DEPLOYMENT_NAME, - messages=messages_summary, max_tokens=30, temperature=0) - conversation_id = conversations_collection.insert_one( - {"user": "local", - "date": datetime.datetime.utcnow(), - "name": completion["choices"][0]["message"]["content"], - "queries": [{"prompt": question, "response": reponse_full, "sources": source_log_docs}]} - ).inserted_id - - # send data.type = "end" to indicate that the stream has ended as json - data = json.dumps({"type": "id", "id": str(conversation_id)}) - yield f"data: {data}\n\n" - data = json.dumps({"type": "end"}) - yield f"data: {data}\n\n" - - -@app.route("/stream", methods=["POST"]) -def stream(): - data = request.get_json() - # get parameter from url question - question = data["question"] - history = data["history"] - # history to json object from string - history = json.loads(history) - conversation_id = data["conversation_id"] - - # check if active_docs is set - - if not api_key_set: - api_key = data["api_key"] - else: - api_key = settings.API_KEY - if not embeddings_key_set: - embeddings_key = data["embeddings_key"] - else: - embeddings_key = settings.EMBEDDINGS_KEY - if "active_docs" in data: - vectorstore = get_vectorstore({"active_docs": data["active_docs"]}) - else: - vectorstore = "" - docsearch = get_docsearch(vectorstore, embeddings_key) - - # question = "Hi" - return Response( - complete_stream(question, docsearch, - chat_history=history, api_key=api_key, - conversation_id=conversation_id), mimetype="text/event-stream" - ) def is_azure_configured(): @@ -352,7 +241,7 @@ def api_answer(): history.reverse() for i in history: if "prompt" in i and "response" in i: - tokens_batch = llm.get_num_tokens(i["prompt"]) + llm.get_num_tokens(i["response"]) + tokens_batch = count_tokens(i["prompt"]) + count_tokens(i["response"]) if tokens_current_history + tokens_batch < settings.TOKENS_MAX_HISTORY: tokens_current_history += tokens_batch messages_combine.append(HumanMessagePromptTemplate.from_template(i["prompt"])) @@ -439,7 +328,6 @@ def api_answer(): "respond ONLY with the summary, use the same language as the " + "system")] - # completion = openai.ChatCompletion.create(model='gpt-3.5-turbo', engine=settings.AZURE_DEPLOYMENT_NAME, # messages=messages_summary, max_tokens=30, temperature=0) completion = llm.predict_messages(messages_summary) @@ -465,260 +353,13 @@ def api_answer(): return bad_request(500, str(e)) -@app.route("/api/docs_check", methods=["POST"]) -def check_docs(): - # check if docs exist in a vectorstore folder - data = request.get_json() - # split docs on / and take first part - if data["docs"].split("/")[0] == "local": - return {"status": "exists"} - vectorstore = "vectors/" + data["docs"] - base_path = "https://raw.githubusercontent.com/arc53/DocsHUB/main/" - if os.path.exists(vectorstore) or data["docs"] == "default": - return {"status": "exists"} - else: - r = requests.get(base_path + vectorstore + "index.faiss") - - if r.status_code != 200: - return {"status": "null"} - else: - if not os.path.exists(vectorstore): - os.makedirs(vectorstore) - with open(vectorstore + "index.faiss", "wb") as f: - f.write(r.content) - - # download the store - r = requests.get(base_path + vectorstore + "index.pkl") - with open(vectorstore + "index.pkl", "wb") as f: - f.write(r.content) - - return {"status": "loaded"} - - -@app.route("/api/feedback", methods=["POST"]) -def api_feedback(): - data = request.get_json() - question = data["question"] - answer = data["answer"] - feedback = data["feedback"] - - print("-" * 5) - print("Question: " + question) - print("Answer: " + answer) - print("Feedback: " + feedback) - print("-" * 5) - response = requests.post( - url="https://86x89umx77.execute-api.eu-west-2.amazonaws.com/docsgpt-feedback", - headers={ - "Content-Type": "application/json; charset=utf-8", - }, - data=json.dumps({"answer": answer, "question": question, "feedback": feedback}), - ) - return {"status": http.client.responses.get(response.status_code, "ok")} - - -@app.route("/api/combine", methods=["GET"]) -def combined_json(): - user = "local" - """Provide json file with combined available indexes.""" - # get json from https://d3dg1063dc54p9.cloudfront.net/combined.json - - data = [ - { - "name": "default", - "language": "default", - "version": "", - "description": "default", - "fullName": "default", - "date": "default", - "docLink": "default", - "model": settings.EMBEDDINGS_NAME, - "location": "local", - } - ] - # structure: name, language, version, description, fullName, date, docLink - # append data from vectors_collection - for index in vectors_collection.find({"user": user}): - data.append( - { - "name": index["name"], - "language": index["language"], - "version": "", - "description": index["name"], - "fullName": index["name"], - "date": index["date"], - "docLink": index["location"], - "model": settings.EMBEDDINGS_NAME, - "location": "local", - } - ) - - data_remote = requests.get("https://d3dg1063dc54p9.cloudfront.net/combined.json").json() - for index in data_remote: - index["location"] = "remote" - data.append(index) - - return jsonify(data) - - -@app.route("/api/upload", methods=["POST"]) -def upload_file(): - """Upload a file to get vectorized and indexed.""" - if "user" not in request.form: - return {"status": "no user"} - user = secure_filename(request.form["user"]) - if "name" not in request.form: - return {"status": "no name"} - job_name = secure_filename(request.form["name"]) - # check if the post request has the file part - if "file" not in request.files: - print("No file part") - return {"status": "no file"} - file = request.files["file"] - if file.filename == "": - return {"status": "no file name"} - - if file: - filename = secure_filename(file.filename) - # save dir - save_dir = os.path.join(app.config["UPLOAD_FOLDER"], user, job_name) - # create dir if not exists - if not os.path.exists(save_dir): - os.makedirs(save_dir) - - file.save(os.path.join(save_dir, filename)) - task = ingest.delay("temp", [".rst", ".md", ".pdf", ".txt"], job_name, filename, user) - # task id - task_id = task.id - return {"status": "ok", "task_id": task_id} - else: - return {"status": "error"} - - -@app.route("/api/task_status", methods=["GET"]) -def task_status(): - """Get celery job status.""" - task_id = request.args.get("task_id") - task = AsyncResult(task_id) - task_meta = task.info - return {"status": task.status, "result": task_meta} - - -### Backgound task api -@app.route("/api/upload_index", methods=["POST"]) -def upload_index_files(): - """Upload two files(index.faiss, index.pkl) to the user's folder.""" - if "user" not in request.form: - return {"status": "no user"} - user = secure_filename(request.form["user"]) - if "name" not in request.form: - return {"status": "no name"} - job_name = secure_filename(request.form["name"]) - if "file_faiss" not in request.files: - print("No file part") - return {"status": "no file"} - file_faiss = request.files["file_faiss"] - if file_faiss.filename == "": - return {"status": "no file name"} - if "file_pkl" not in request.files: - print("No file part") - return {"status": "no file"} - file_pkl = request.files["file_pkl"] - if file_pkl.filename == "": - return {"status": "no file name"} - - # saves index files - save_dir = os.path.join("indexes", user, job_name) - if not os.path.exists(save_dir): - os.makedirs(save_dir) - file_faiss.save(os.path.join(save_dir, "index.faiss")) - file_pkl.save(os.path.join(save_dir, "index.pkl")) - # create entry in vectors_collection - vectors_collection.insert_one( - { - "user": user, - "name": job_name, - "language": job_name, - "location": save_dir, - "date": datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S"), - "model": settings.EMBEDDINGS_NAME, - "type": "local", - } - ) - return {"status": "ok"} - - -@app.route("/api/download", methods=["get"]) -def download_file(): - user = secure_filename(request.args.get("user")) - job_name = secure_filename(request.args.get("name")) - filename = secure_filename(request.args.get("file")) - save_dir = os.path.join(app.config["UPLOAD_FOLDER"], user, job_name) - return send_from_directory(save_dir, filename, as_attachment=True) - - -@app.route("/api/delete_old", methods=["get"]) -def delete_old(): - """Delete old indexes.""" - import shutil - - path = request.args.get("path") - dirs = path.split("/") - dirs_clean = [] - for i in range(1, len(dirs)): - dirs_clean.append(secure_filename(dirs[i])) - # check that path strats with indexes or vectors - if dirs[0] not in ["indexes", "vectors"]: - return {"status": "error"} - path_clean = "/".join(dirs) - vectors_collection.delete_one({"location": path}) - try: - shutil.rmtree(path_clean) - except FileNotFoundError: - pass - return {"status": "ok"} - - -@app.route("/api/get_conversations", methods=["get"]) -def get_conversations(): - # provides a list of conversations - conversations = conversations_collection.find().sort("date", -1) - list_conversations = [] - for conversation in conversations: - list_conversations.append({"id": str(conversation["_id"]), "name": conversation["name"]}) - - #list_conversations = [{"id": "default", "name": "default"}, {"id": "jeff", "name": "jeff"}] - - return jsonify(list_conversations) - -@app.route("/api/get_single_conversation", methods=["get"]) -def get_single_conversation(): - # provides data for a conversation - conversation_id = request.args.get("id") - conversation = conversations_collection.find_one({"_id": ObjectId(conversation_id)}) - return jsonify(conversation['queries']) - -@app.route("/api/delete_conversation", methods=["POST"]) -def delete_conversation(): - # deletes a conversation from the database - conversation_id = request.args.get("id") - # write to mongodb - conversations_collection.delete_one( - { - "_id": ObjectId(conversation_id), - } - ) - - return {"status": "ok"} - - # handling CORS @app.after_request def after_request(response): response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") response.headers.add("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS") - response.headers.add("Access-Control-Allow-Credentials", "true") + # response.headers.add("Access-Control-Allow-Credentials", "true") return response diff --git a/application/celery.py b/application/celery.py new file mode 100644 index 00000000..461dc53e --- /dev/null +++ b/application/celery.py @@ -0,0 +1,10 @@ +from celery import Celery +from app import create_app + +def make_celery(app_name=__name__): + app = create_app() + celery = Celery(app_name, broker=app.config['CELERY_BROKER_URL']) + celery.conf.update(app.config) + return celery + +celery = make_celery() diff --git a/application/llm/__init__.py b/application/llm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/application/llm/base.py b/application/llm/base.py new file mode 100644 index 00000000..16c91303 --- /dev/null +++ b/application/llm/base.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod +import json + + +class BaseLLM(ABC): + def __init__(self): + pass + + @abstractmethod + def gen(self, *args, **kwargs): + pass + + @abstractmethod + def gen_stream(self, *args, **kwargs): + pass diff --git a/application/llm/openai.py b/application/llm/openai.py new file mode 100644 index 00000000..981b8394 --- /dev/null +++ b/application/llm/openai.py @@ -0,0 +1,34 @@ +from application.llm.base import BaseLLM + +class OpenAILLM(BaseLLM): + + def __init__(self, api_key): + global openai + import openai + openai.api_key = api_key + self.api_key = api_key # Save the API key to be used later + + def _get_openai(self): + # Import openai when needed + import openai + # Set the API key every time you import openai + openai.api_key = self.api_key + return openai + + def gen(self, *args, **kwargs): + # This is just a stub. In the real implementation, you'd hit the OpenAI API or any other service. + return "Non-streaming response from OpenAI." + + def gen_stream(self, model, engine, messages, stream=True, **kwargs): + # openai = self._get_openai() # Get the openai module with the API key set + response = openai.ChatCompletion.create( + model=model, + engine=engine, + messages=messages, + stream=stream, + **kwargs + ) + + for line in response: + if "content" in line["choices"][0]["delta"]: + yield line["choices"][0]["delta"]["content"] diff --git a/application/vectorstore/__init__.py b/application/vectorstore/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/application/vectorstore/base.py b/application/vectorstore/base.py new file mode 100644 index 00000000..e69de29b diff --git a/frontend/.env.development b/frontend/.env.development index b09c4685..2bb67110 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -1,2 +1,3 @@ # Please put appropriate value -VITE_API_HOST=http://localhost:7091 \ No newline at end of file +VITE_API_HOST=http://localhost:7091 +VITE_API_STREAMING=true \ No newline at end of file