From e85a583f0ae109701096b08af5bcccffe3204ae4 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 26 Sep 2023 10:03:22 +0100 Subject: [PATCH 1/8] 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 000000000..e69de29bb diff --git a/application/api/answer/__init__.py b/application/api/answer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py new file mode 100644 index 000000000..5f04678d3 --- /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 000000000..e69de29bb diff --git a/application/api/internal/routes.py b/application/api/internal/routes.py new file mode 100644 index 000000000..5092f9bef --- /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 000000000..e69de29bb diff --git a/application/api/user/routes.py b/application/api/user/routes.py new file mode 100644 index 000000000..d661af415 --- /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 39ac744f3..c4b2c314b 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.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 @@ 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 @@ 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 000000000..461dc53e9 --- /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 000000000..e69de29bb diff --git a/application/llm/base.py b/application/llm/base.py new file mode 100644 index 000000000..16c91303d --- /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 000000000..981b83941 --- /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 000000000..e69de29bb diff --git a/application/vectorstore/base.py b/application/vectorstore/base.py new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/.env.development b/frontend/.env.development index b09c46852..2bb671102 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 From 025549ebf89bb207af689711cc80590cb4d12722 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 26 Sep 2023 13:00:17 +0100 Subject: [PATCH 2/8] fixes to make it work --- application/api/answer/routes.py | 349 +++++++++++++++++++++++++++---- application/app.py | 293 +------------------------- application/llm/openai.py | 30 ++- 3 files changed, 334 insertions(+), 338 deletions(-) diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index 5f04678d3..3755eb5bc 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -1,20 +1,44 @@ +import asyncio import os from flask import Blueprint, request, jsonify, Response import requests import json import datetime +import logging +import traceback +from celery.result import AsyncResult -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 transformers import GPT2TokenizerFast + +from langchain import FAISS +from langchain import VectorDBQA, Cohere, OpenAI +from langchain.chains import LLMChain, ConversationalRetrievalChain +from langchain.chains.conversational_retrieval.prompts import CONDENSE_QUESTION_PROMPT +from langchain.chains.question_answering import load_qa_chain +from langchain.chat_models import ChatOpenAI, AzureChatOpenAI +from langchain.embeddings import ( + OpenAIEmbeddings, + HuggingFaceHubEmbeddings, + CohereEmbeddings, + HuggingFaceInstructEmbeddings, +) +from langchain.prompts import PromptTemplate +from langchain.prompts.chat import ( + ChatPromptTemplate, + SystemMessagePromptTemplate, + HumanMessagePromptTemplate, + AIMessagePromptTemplate, +) +from langchain.schema import HumanMessage, AIMessage -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 +from application.core.settings import settings +from application.error import bad_request +logger = logging.getLogger(__name__) mongo = MongoClient(settings.MONGO_URI) db = mongo["docsgpt"] @@ -22,28 +46,118 @@ vectors_collection = db["vectors"] answer = Blueprint('answer', __name__) +if settings.LLM_NAME == "gpt4": + gpt_model = 'gpt-4' +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) + tokenizer = AutoTokenizer.from_pretrained(model_id) + model = AutoModelForCausalLM.from_pretrained(model_id) + pipe = pipeline( + "text-generation", model=model, + tokenizer=tokenizer, max_new_tokens=2000, + device_map="auto", eos_token_id=tokenizer.eos_token_id + ) + hf = HuggingFacePipeline(pipeline=pipe) + +# load the prompts +current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +with open(os.path.join(current_dir, "prompts", "combine_prompt.txt"), "r") as f: + template = f.read() + +with open(os.path.join(current_dir, "prompts", "combine_prompt_hist.txt"), "r") as f: + template_hist = f.read() + +with open(os.path.join(current_dir, "prompts", "question_prompt.txt"), "r") as f: + template_quest = f.read() + +with open(os.path.join(current_dir, "prompts", "chat_combine_prompt.txt"), "r") as f: + chat_combine_template = f.read() + +with open(os.path.join(current_dir, "prompts", "chat_reduce_prompt.txt"), "r") as f: + chat_reduce_template = f.read() + +api_key_set = settings.API_KEY is not None +embeddings_key_set = settings.EMBEDDINGS_KEY is not None + + +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() + asyncio.set_event_loop(loop) + result = {} + try: + answer = loop.run_until_complete(async_generate(chain, question, chat_history)) + finally: + loop.close() + result["answer"] = answer + return result + + +def get_vectorstore(data): + if "active_docs" in data: + if data["active_docs"].split("/")[0] == "local": + if data["active_docs"].split("/")[1] == "default": + vectorstore = "" + else: + vectorstore = "indexes/" + data["active_docs"] + else: + vectorstore = "vectors/" + data["active_docs"] + if data["active_docs"] == "default": + vectorstore = "" + else: + vectorstore = "" + vectorstore = os.path.join("application", vectorstore) + return vectorstore + + +def get_docsearch(vectorstore, embeddings_key): + if settings.EMBEDDINGS_NAME == "openai_text-embedding-ada-002": + if is_azure_configured(): + os.environ["OPENAI_API_TYPE"] = "azure" + openai_embeddings = OpenAIEmbeddings(model=settings.AZURE_EMBEDDINGS_DEPLOYMENT_NAME) + else: + openai_embeddings = OpenAIEmbeddings(openai_api_key=embeddings_key) + docsearch = FAISS.load_local(vectorstore, openai_embeddings) + elif settings.EMBEDDINGS_NAME == "huggingface_sentence-transformers/all-mpnet-base-v2": + docsearch = FAISS.load_local(vectorstore, HuggingFaceHubEmbeddings()) + elif settings.EMBEDDINGS_NAME == "huggingface_hkunlp/instructor-large": + docsearch = FAISS.load_local(vectorstore, HuggingFaceInstructEmbeddings()) + elif settings.EMBEDDINGS_NAME == "cohere_medium": + docsearch = FAISS.load_local(vectorstore, CohereEmbeddings(cohere_api_key=embeddings_key)) + return docsearch + + 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 +def complete_stream(question, docsearch, chat_history, api_key, conversation_id): 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) + 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 = 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]) @@ -71,33 +185,20 @@ def complete_stream(question, docsearch, chat_history, api_key, conversation_id) 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) + + response_full = "" completion = llm.gen_stream(model=gpt_model, engine=settings.AZURE_DEPLOYMENT_NAME, - messages=messages_combine) + messages=messages_combine) for line in completion: - data = json.dumps({"answer": str(line)}) - reponse_full += str(line) + response_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}}}, + {"$push": {"queries": {"prompt": question, "response": response_full, "sources": source_log_docs}}}, ) else: @@ -107,19 +208,18 @@ def complete_stream(question, docsearch, chat_history, api_key, conversation_id) "words, respond ONLY with the summary, use the same " "language as the system \n\nUser: " + question + "\n\n" + "AI: " + - reponse_full}, + response_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) + messages=messages_summary, 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}]} + "name": completion, + "queries": [{"prompt": question, "response": response_full, "sources": source_log_docs}]} ).inserted_id # send data.type = "end" to indicate that the stream has ended as json @@ -160,4 +260,165 @@ def stream(): 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 + ) + + +@answer.route("/api/answer", methods=["POST"]) +def api_answer(): + data = request.get_json() + question = data["question"] + history = data["history"] + if "conversation_id" not in data: + conversation_id = None + else: + conversation_id = data["conversation_id"] + print("-" * 5) + 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 + + # use try and except to check for exception + try: + # check if the vectorstore is set + vectorstore = get_vectorstore(data) + # loading the index and the store and the prompt template + # Note if you have used other embeddings than OpenAI, you need to change the embeddings + docsearch = get_docsearch(vectorstore, embeddings_key) + + q_prompt = PromptTemplate( + input_variables=["context", "question"], template=template_quest, template_format="jinja2" + ) + if settings.LLM_NAME == "openai_chat": + if is_azure_configured(): + logger.debug("in Azure") + 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, model_name=gpt_model) # optional parameter: model_name="gpt-4" + messages_combine = [SystemMessagePromptTemplate.from_template(chat_combine_template)] + if history: + tokens_current_history = 0 + # count tokens in history + history.reverse() + for i in 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(HumanMessagePromptTemplate.from_template(i["prompt"])) + messages_combine.append(AIMessagePromptTemplate.from_template(i["response"])) + messages_combine.append(HumanMessagePromptTemplate.from_template("{question}")) + p_chat_combine = ChatPromptTemplate.from_messages(messages_combine) + elif settings.LLM_NAME == "openai": + llm = OpenAI(openai_api_key=api_key, temperature=0) + elif settings.SELF_HOSTED_MODEL: + llm = hf + elif settings.LLM_NAME == "cohere": + llm = Cohere(model="command-xlarge-nightly", cohere_api_key=api_key) + else: + raise ValueError("unknown LLM model") + + if settings.LLM_NAME == "openai_chat": + question_generator = LLMChain(llm=llm, prompt=CONDENSE_QUESTION_PROMPT) + doc_chain = load_qa_chain(llm, chain_type="map_reduce", combine_prompt=p_chat_combine) + chain = ConversationalRetrievalChain( + retriever=docsearch.as_retriever(k=2), + question_generator=question_generator, + combine_docs_chain=doc_chain, + ) + chat_history = [] + # result = chain({"question": question, "chat_history": chat_history}) + # generate async with async generate method + result = run_async_chain(chain, question, chat_history) + elif settings.SELF_HOSTED_MODEL: + question_generator = LLMChain(llm=llm, prompt=CONDENSE_QUESTION_PROMPT) + doc_chain = load_qa_chain(llm, chain_type="map_reduce", combine_prompt=p_chat_combine) + chain = ConversationalRetrievalChain( + retriever=docsearch.as_retriever(k=2), + question_generator=question_generator, + combine_docs_chain=doc_chain, + ) + chat_history = [] + # result = chain({"question": question, "chat_history": chat_history}) + # generate async with async generate method + result = run_async_chain(chain, question, chat_history) + + else: + qa_chain = load_qa_chain( + llm=llm, chain_type="map_reduce", combine_prompt=chat_combine_template, question_prompt=q_prompt + ) + chain = VectorDBQA(combine_documents_chain=qa_chain, vectorstore=docsearch, k=3) + result = chain({"query": question}) + + print(result) + + # some formatting for the frontend + if "result" in result: + result["answer"] = result["result"] + result["answer"] = result["answer"].replace("\\n", "\n") + try: + result["answer"] = result["answer"].split("SOURCES:")[0] + except Exception: + pass + + sources = docsearch.similarity_search(question, k=2) + sources_doc = [] + for doc in sources: + if doc.metadata: + sources_doc.append({'title': doc.metadata['title'], 'text': doc.page_content}) + else: + sources_doc.append({'title': doc.page_content, 'text': doc.page_content}) + result['sources'] = sources_doc + + # generate conversationId + if conversation_id is not None: + conversations_collection.update_one( + {"_id": ObjectId(conversation_id)}, + {"$push": {"queries": {"prompt": question, + "response": result["answer"], "sources": result['sources']}}}, + ) + + else: + # create new conversation + # generate summary + messages_summary = [AIMessage(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\nAI: " + + result["answer"]), + HumanMessage(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.predict_messages(messages_summary) + conversation_id = conversations_collection.insert_one( + {"user": "local", + "date": datetime.datetime.utcnow(), + "name": completion.content, + "queries": [{"prompt": question, "response": result["answer"], "sources": result['sources']}]} + ).inserted_id + + result["conversation_id"] = str(conversation_id) + + # mock result + # result = { + # "answer": "The answer is 42", + # "sources": ["https://en.wikipedia.org/wiki/42_(number)", "https://en.wikipedia.org/wiki/42_(number)"] + # } + return result + except Exception as e: + # print whole traceback + traceback.print_exc() + print(str(e)) + return bad_request(500, str(e)) diff --git a/application/app.py b/application/app.py index c4b2c314b..55c8e5c3b 100644 --- a/application/app.py +++ b/application/app.py @@ -1,69 +1,18 @@ -import asyncio -import datetime -import json -import logging -import os import platform -import traceback + import dotenv -import openai -import requests from celery import Celery -from celery.result import AsyncResult -from flask import Flask, request, send_from_directory, jsonify, Response, redirect -from langchain import FAISS -from langchain import VectorDBQA, Cohere, OpenAI -from langchain.chains import LLMChain, ConversationalRetrievalChain -from langchain.chains.conversational_retrieval.prompts import CONDENSE_QUESTION_PROMPT -from langchain.chains.question_answering import load_qa_chain -from langchain.chat_models import ChatOpenAI, AzureChatOpenAI -from langchain.embeddings import ( - OpenAIEmbeddings, - HuggingFaceHubEmbeddings, - CohereEmbeddings, - HuggingFaceInstructEmbeddings, -) -from langchain.prompts import PromptTemplate -from langchain.prompts.chat import ( - ChatPromptTemplate, - SystemMessagePromptTemplate, - HumanMessagePromptTemplate, - AIMessagePromptTemplate, -) -from langchain.schema import HumanMessage, AIMessage +from flask import Flask, request, redirect + from pymongo import MongoClient -from werkzeug.utils import secure_filename 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" - -logger = logging.getLogger(__name__) -if settings.LLM_NAME == "gpt4": - gpt_model = 'gpt-4' -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) - tokenizer = AutoTokenizer.from_pretrained(model_id) - model = AutoModelForCausalLM.from_pretrained(model_id) - pipe = pipeline( - "text-generation", model=model, - tokenizer=tokenizer, max_new_tokens=2000, - device_map="auto", eos_token_id=tokenizer.eos_token_id - ) - hf = HuggingFacePipeline(pipeline=pipe) # Redirect PosixPath to WindowsPath on Windows @@ -76,25 +25,7 @@ # loading the .env file dotenv.load_dotenv() -# load the prompts -current_dir = os.path.dirname(os.path.abspath(__file__)) -with open(os.path.join(current_dir, "prompts", "combine_prompt.txt"), "r") as f: - template = f.read() - -with open(os.path.join(current_dir, "prompts", "combine_prompt_hist.txt"), "r") as f: - template_hist = f.read() - -with open(os.path.join(current_dir, "prompts", "question_prompt.txt"), "r") as f: - template_quest = f.read() - -with open(os.path.join(current_dir, "prompts", "chat_combine_prompt.txt"), "r") as f: - chat_combine_template = f.read() - -with open(os.path.join(current_dir, "prompts", "chat_reduce_prompt.txt"), "r") as f: - chat_reduce_template = f.read() -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) @@ -105,65 +36,9 @@ app.config["MONGO_URI"] = settings.MONGO_URI celery = Celery() celery.config_from_object("application.celeryconfig") -mongo = MongoClient(app.config["MONGO_URI"]) -db = mongo["docsgpt"] -vectors_collection = db["vectors"] -conversations_collection = db["conversations"] -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() - asyncio.set_event_loop(loop) - result = {} - try: - answer = loop.run_until_complete(async_generate(chain, question, chat_history)) - finally: - loop.close() - result["answer"] = answer - return result - - -def get_vectorstore(data): - if "active_docs" in data: - if data["active_docs"].split("/")[0] == "local": - if data["active_docs"].split("/")[1] == "default": - vectorstore = "" - else: - vectorstore = "indexes/" + data["active_docs"] - else: - vectorstore = "vectors/" + data["active_docs"] - if data["active_docs"] == "default": - vectorstore = "" - else: - vectorstore = "" - vectorstore = os.path.join("application", vectorstore) - return vectorstore - - -def get_docsearch(vectorstore, embeddings_key): - if settings.EMBEDDINGS_NAME == "openai_text-embedding-ada-002": - if is_azure_configured(): - os.environ["OPENAI_API_TYPE"] = "azure" - openai_embeddings = OpenAIEmbeddings(model=settings.AZURE_EMBEDDINGS_DEPLOYMENT_NAME) - else: - openai_embeddings = OpenAIEmbeddings(openai_api_key=embeddings_key) - docsearch = FAISS.load_local(vectorstore, openai_embeddings) - elif settings.EMBEDDINGS_NAME == "huggingface_sentence-transformers/all-mpnet-base-v2": - docsearch = FAISS.load_local(vectorstore, HuggingFaceHubEmbeddings()) - elif settings.EMBEDDINGS_NAME == "huggingface_hkunlp/instructor-large": - docsearch = FAISS.load_local(vectorstore, HuggingFaceInstructEmbeddings()) - elif settings.EMBEDDINGS_NAME == "cohere_medium": - docsearch = FAISS.load_local(vectorstore, CohereEmbeddings(cohere_api_key=embeddings_key)) - return docsearch @celery.task(bind=True) @@ -188,169 +63,7 @@ def home(): -def is_azure_configured(): - return settings.OPENAI_API_BASE and settings.OPENAI_API_VERSION and settings.AZURE_DEPLOYMENT_NAME - - -@app.route("/api/answer", methods=["POST"]) -def api_answer(): - data = request.get_json() - question = data["question"] - history = data["history"] - if "conversation_id" not in data: - conversation_id = None - else: - conversation_id = data["conversation_id"] - print("-" * 5) - 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 - - # use try and except to check for exception - try: - # check if the vectorstore is set - vectorstore = get_vectorstore(data) - # loading the index and the store and the prompt template - # Note if you have used other embeddings than OpenAI, you need to change the embeddings - docsearch = get_docsearch(vectorstore, embeddings_key) - - q_prompt = PromptTemplate( - input_variables=["context", "question"], template=template_quest, template_format="jinja2" - ) - if settings.LLM_NAME == "openai_chat": - if is_azure_configured(): - logger.debug("in Azure") - 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, model_name=gpt_model) # optional parameter: model_name="gpt-4" - messages_combine = [SystemMessagePromptTemplate.from_template(chat_combine_template)] - if history: - tokens_current_history = 0 - # count tokens in history - history.reverse() - for i in 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(HumanMessagePromptTemplate.from_template(i["prompt"])) - messages_combine.append(AIMessagePromptTemplate.from_template(i["response"])) - messages_combine.append(HumanMessagePromptTemplate.from_template("{question}")) - p_chat_combine = ChatPromptTemplate.from_messages(messages_combine) - elif settings.LLM_NAME == "openai": - llm = OpenAI(openai_api_key=api_key, temperature=0) - elif settings.SELF_HOSTED_MODEL: - llm = hf - elif settings.LLM_NAME == "cohere": - llm = Cohere(model="command-xlarge-nightly", cohere_api_key=api_key) - else: - raise ValueError("unknown LLM model") - - if settings.LLM_NAME == "openai_chat": - question_generator = LLMChain(llm=llm, prompt=CONDENSE_QUESTION_PROMPT) - doc_chain = load_qa_chain(llm, chain_type="map_reduce", combine_prompt=p_chat_combine) - chain = ConversationalRetrievalChain( - retriever=docsearch.as_retriever(k=2), - question_generator=question_generator, - combine_docs_chain=doc_chain, - ) - chat_history = [] - # result = chain({"question": question, "chat_history": chat_history}) - # generate async with async generate method - result = run_async_chain(chain, question, chat_history) - elif settings.SELF_HOSTED_MODEL: - question_generator = LLMChain(llm=llm, prompt=CONDENSE_QUESTION_PROMPT) - doc_chain = load_qa_chain(llm, chain_type="map_reduce", combine_prompt=p_chat_combine) - chain = ConversationalRetrievalChain( - retriever=docsearch.as_retriever(k=2), - question_generator=question_generator, - combine_docs_chain=doc_chain, - ) - chat_history = [] - # result = chain({"question": question, "chat_history": chat_history}) - # generate async with async generate method - result = run_async_chain(chain, question, chat_history) - - else: - qa_chain = load_qa_chain( - llm=llm, chain_type="map_reduce", combine_prompt=chat_combine_template, question_prompt=q_prompt - ) - chain = VectorDBQA(combine_documents_chain=qa_chain, vectorstore=docsearch, k=3) - result = chain({"query": question}) - - print(result) - - # some formatting for the frontend - if "result" in result: - result["answer"] = result["result"] - result["answer"] = result["answer"].replace("\\n", "\n") - try: - result["answer"] = result["answer"].split("SOURCES:")[0] - except Exception: - pass - - sources = docsearch.similarity_search(question, k=2) - sources_doc = [] - for doc in sources: - if doc.metadata: - sources_doc.append({'title': doc.metadata['title'], 'text': doc.page_content}) - else: - sources_doc.append({'title': doc.page_content, 'text': doc.page_content}) - result['sources'] = sources_doc - - # generate conversationId - if conversation_id is not None: - conversations_collection.update_one( - {"_id": ObjectId(conversation_id)}, - {"$push": {"queries": {"prompt": question, - "response": result["answer"], "sources": result['sources']}}}, - ) - - else: - # create new conversation - # generate summary - messages_summary = [AIMessage(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\nAI: " + - result["answer"]), - HumanMessage(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.predict_messages(messages_summary) - conversation_id = conversations_collection.insert_one( - {"user": "local", - "date": datetime.datetime.utcnow(), - "name": completion.content, - "queries": [{"prompt": question, "response": result["answer"], "sources": result['sources']}]} - ).inserted_id - - result["conversation_id"] = str(conversation_id) - # mock result - # result = { - # "answer": "The answer is 42", - # "sources": ["https://en.wikipedia.org/wiki/42_(number)", "https://en.wikipedia.org/wiki/42_(number)"] - # } - return result - except Exception as e: - # print whole traceback - traceback.print_exc() - print(str(e)) - return bad_request(500, str(e)) # handling CORS diff --git a/application/llm/openai.py b/application/llm/openai.py index 981b83941..23e5fab0e 100644 --- a/application/llm/openai.py +++ b/application/llm/openai.py @@ -15,12 +15,18 @@ def _get_openai(self): 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(self, model, engine, messages, stream=False, **kwargs): + response = openai.ChatCompletion.create( + model=model, + engine=engine, + messages=messages, + stream=stream, + **kwargs + ) + + return response["choices"][0]["message"]["content"] 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, @@ -32,3 +38,19 @@ def gen_stream(self, model, engine, messages, stream=True, **kwargs): for line in response: if "content" in line["choices"][0]["delta"]: yield line["choices"][0]["delta"]["content"] + + +class AzureOpenAILLM(OpenAILLM): + + def __init__(self, openai_api_key, openai_api_base, openai_api_version, deployment_name): + super().__init__(openai_api_key) + self.api_base = openai_api_base + self.api_version = openai_api_version + self.deployment_name = deployment_name + + def _get_openai(self): + openai = super()._get_openai() + openai.api_base = self.api_base + openai.api_version = self.api_version + openai.api_type = "azure" + return openai From 8fa9657ba6d427980540bc235cfe4f0f4494205e Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 27 Sep 2023 16:25:57 +0100 Subject: [PATCH 3/8] working full --- application/api/answer/routes.py | 226 +++++++++++------------------ application/api/internal/routes.py | 7 +- application/api/user/routes.py | 9 +- application/api/user/tasks.py | 7 + application/app.py | 16 +- application/celery.py | 7 +- application/core/settings.py | 1 + application/vectorstore/base.py | 45 ++++++ application/vectorstore/faiss.py | 15 ++ application/worker.py | 5 + 10 files changed, 178 insertions(+), 160 deletions(-) create mode 100644 application/api/user/tasks.py create mode 100644 application/vectorstore/faiss.py diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index 3755eb5bc..8f64a8f91 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -1,43 +1,24 @@ import asyncio import os -from flask import Blueprint, request, jsonify, Response -import requests +from flask import Blueprint, request, Response import json import datetime import logging import traceback -from celery.result import AsyncResult from pymongo import MongoClient from bson.objectid import ObjectId from transformers import GPT2TokenizerFast -from langchain import FAISS -from langchain import VectorDBQA, Cohere, OpenAI -from langchain.chains import LLMChain, ConversationalRetrievalChain -from langchain.chains.conversational_retrieval.prompts import CONDENSE_QUESTION_PROMPT -from langchain.chains.question_answering import load_qa_chain -from langchain.chat_models import ChatOpenAI, AzureChatOpenAI -from langchain.embeddings import ( - OpenAIEmbeddings, - HuggingFaceHubEmbeddings, - CohereEmbeddings, - HuggingFaceInstructEmbeddings, -) -from langchain.prompts import PromptTemplate -from langchain.prompts.chat import ( - ChatPromptTemplate, - SystemMessagePromptTemplate, - HumanMessagePromptTemplate, - AIMessagePromptTemplate, -) -from langchain.schema import HumanMessage, AIMessage + from application.core.settings import settings -from application.llm.openai import OpenAILLM -from application.core.settings import settings +from application.llm.openai import OpenAILLM, AzureOpenAILLM +from application.vectorstore.faiss import FaissStore from application.error import bad_request + + logger = logging.getLogger(__name__) mongo = MongoClient(settings.MONGO_URI) @@ -125,21 +106,21 @@ def get_vectorstore(data): return vectorstore -def get_docsearch(vectorstore, embeddings_key): - if settings.EMBEDDINGS_NAME == "openai_text-embedding-ada-002": - if is_azure_configured(): - os.environ["OPENAI_API_TYPE"] = "azure" - openai_embeddings = OpenAIEmbeddings(model=settings.AZURE_EMBEDDINGS_DEPLOYMENT_NAME) - else: - openai_embeddings = OpenAIEmbeddings(openai_api_key=embeddings_key) - docsearch = FAISS.load_local(vectorstore, openai_embeddings) - elif settings.EMBEDDINGS_NAME == "huggingface_sentence-transformers/all-mpnet-base-v2": - docsearch = FAISS.load_local(vectorstore, HuggingFaceHubEmbeddings()) - elif settings.EMBEDDINGS_NAME == "huggingface_hkunlp/instructor-large": - docsearch = FAISS.load_local(vectorstore, HuggingFaceInstructEmbeddings()) - elif settings.EMBEDDINGS_NAME == "cohere_medium": - docsearch = FAISS.load_local(vectorstore, CohereEmbeddings(cohere_api_key=embeddings_key)) - return docsearch +# def get_docsearch(vectorstore, embeddings_key): +# if settings.EMBEDDINGS_NAME == "openai_text-embedding-ada-002": +# if is_azure_configured(): +# os.environ["OPENAI_API_TYPE"] = "azure" +# openai_embeddings = OpenAIEmbeddings(model=settings.AZURE_EMBEDDINGS_DEPLOYMENT_NAME) +# else: +# openai_embeddings = OpenAIEmbeddings(openai_api_key=embeddings_key) +# docsearch = FAISS.load_local(vectorstore, openai_embeddings) +# elif settings.EMBEDDINGS_NAME == "huggingface_sentence-transformers/all-mpnet-base-v2": +# docsearch = FAISS.load_local(vectorstore, HuggingFaceHubEmbeddings()) +# elif settings.EMBEDDINGS_NAME == "huggingface_hkunlp/instructor-large": +# docsearch = FAISS.load_local(vectorstore, HuggingFaceInstructEmbeddings()) +# elif settings.EMBEDDINGS_NAME == "cohere_medium": +# docsearch = FAISS.load_local(vectorstore, CohereEmbeddings(cohere_api_key=embeddings_key)) +# return docsearch def is_azure_configured(): @@ -148,7 +129,7 @@ def is_azure_configured(): def complete_stream(question, docsearch, chat_history, api_key, conversation_id): if is_azure_configured(): - llm = AzureChatOpenAI( + llm = AzureOpenAILLM( openai_api_key=api_key, openai_api_base=settings.OPENAI_API_BASE, openai_api_version=settings.OPENAI_API_VERSION, @@ -158,7 +139,7 @@ def complete_stream(question, docsearch, chat_history, api_key, conversation_id) logger.debug("plain OpenAI") llm = OpenAILLM(api_key=api_key) - docs = docsearch.similarity_search(question, k=2) + docs = docsearch.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) @@ -253,9 +234,8 @@ def stream(): vectorstore = get_vectorstore({"active_docs": data["active_docs"]}) else: vectorstore = "" - docsearch = get_docsearch(vectorstore, embeddings_key) + docsearch = FaissStore(vectorstore, embeddings_key) - # question = "Hi" return Response( complete_stream(question, docsearch, chat_history=history, api_key=api_key, @@ -288,97 +268,55 @@ def api_answer(): vectorstore = get_vectorstore(data) # loading the index and the store and the prompt template # Note if you have used other embeddings than OpenAI, you need to change the embeddings - docsearch = get_docsearch(vectorstore, embeddings_key) + docsearch = FaissStore(vectorstore, embeddings_key) - q_prompt = PromptTemplate( - input_variables=["context", "question"], template=template_quest, template_format="jinja2" - ) - if settings.LLM_NAME == "openai_chat": - if is_azure_configured(): - logger.debug("in Azure") - 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, model_name=gpt_model) # optional parameter: model_name="gpt-4" - messages_combine = [SystemMessagePromptTemplate.from_template(chat_combine_template)] - if history: - tokens_current_history = 0 - # count tokens in history - history.reverse() - for i in 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(HumanMessagePromptTemplate.from_template(i["prompt"])) - messages_combine.append(AIMessagePromptTemplate.from_template(i["response"])) - messages_combine.append(HumanMessagePromptTemplate.from_template("{question}")) - p_chat_combine = ChatPromptTemplate.from_messages(messages_combine) - elif settings.LLM_NAME == "openai": - llm = OpenAI(openai_api_key=api_key, temperature=0) - elif settings.SELF_HOSTED_MODEL: - llm = hf - elif settings.LLM_NAME == "cohere": - llm = Cohere(model="command-xlarge-nightly", cohere_api_key=api_key) - else: - raise ValueError("unknown LLM model") - - if settings.LLM_NAME == "openai_chat": - question_generator = LLMChain(llm=llm, prompt=CONDENSE_QUESTION_PROMPT) - doc_chain = load_qa_chain(llm, chain_type="map_reduce", combine_prompt=p_chat_combine) - chain = ConversationalRetrievalChain( - retriever=docsearch.as_retriever(k=2), - question_generator=question_generator, - combine_docs_chain=doc_chain, - ) - chat_history = [] - # result = chain({"question": question, "chat_history": chat_history}) - # generate async with async generate method - result = run_async_chain(chain, question, chat_history) - elif settings.SELF_HOSTED_MODEL: - question_generator = LLMChain(llm=llm, prompt=CONDENSE_QUESTION_PROMPT) - doc_chain = load_qa_chain(llm, chain_type="map_reduce", combine_prompt=p_chat_combine) - chain = ConversationalRetrievalChain( - retriever=docsearch.as_retriever(k=2), - question_generator=question_generator, - combine_docs_chain=doc_chain, + if is_azure_configured(): + llm = AzureOpenAILLM( + 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, ) - chat_history = [] - # result = chain({"question": question, "chat_history": chat_history}) - # generate async with async generate method - result = run_async_chain(chain, question, chat_history) - else: - qa_chain = load_qa_chain( - llm=llm, chain_type="map_reduce", combine_prompt=chat_combine_template, question_prompt=q_prompt - ) - chain = VectorDBQA(combine_documents_chain=qa_chain, vectorstore=docsearch, k=3) - result = chain({"query": question}) - - print(result) - - # some formatting for the frontend - if "result" in result: - result["answer"] = result["result"] - result["answer"] = result["answer"].replace("\\n", "\n") - try: - result["answer"] = result["answer"].split("SOURCES:")[0] - except Exception: - pass - - sources = docsearch.similarity_search(question, k=2) - sources_doc = [] - for doc in sources: + logger.debug("plain OpenAI") + llm = OpenAILLM(api_key=api_key) + + + + docs = docsearch.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: - sources_doc.append({'title': doc.metadata['title'], 'text': doc.page_content}) + source_log_docs.append({"title": doc.metadata['title'].split('/')[-1], "text": doc.page_content}) else: - sources_doc.append({'title': doc.page_content, 'text': doc.page_content}) - result['sources'] = sources_doc + source_log_docs.append({"title": doc.page_content, "text": doc.page_content}) + # join all page_content together with a newline + + + if len(history) > 1: + tokens_current_history = 0 + # count tokens in history + history.reverse() + for i in 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 = llm.gen(model=gpt_model, engine=settings.AZURE_DEPLOYMENT_NAME, + messages=messages_combine) + + + result = {"answer": completion, "sources": source_log_docs} + logger.debug(result) # generate conversationId if conversation_id is not None: @@ -391,22 +329,22 @@ def api_answer(): else: # create new conversation # generate summary - messages_summary = [AIMessage(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\nAI: " + - result["answer"]), - HumanMessage(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.predict_messages(messages_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: " + + result["answer"]}, + {"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 = llm.gen(model=gpt_model, engine=settings.AZURE_DEPLOYMENT_NAME, + messages=messages_summary, max_tokens=30) conversation_id = conversations_collection.insert_one( {"user": "local", - "date": datetime.datetime.utcnow(), - "name": completion.content, - "queries": [{"prompt": question, "response": result["answer"], "sources": result['sources']}]} + "date": datetime.datetime.utcnow(), + "name": completion, + "queries": [{"prompt": question, "response": result["answer"], "sources": source_log_docs}]} ).inserted_id result["conversation_id"] = str(conversation_id) diff --git a/application/api/internal/routes.py b/application/api/internal/routes.py index 5092f9bef..13c44724a 100644 --- a/application/api/internal/routes.py +++ b/application/api/internal/routes.py @@ -12,13 +12,16 @@ conversations_collection = db["conversations"] vectors_collection = db["vectors"] +current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + 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) + save_dir = os.path.join(current_dir, settings.UPLOAD_FOLDER, user, job_name) return send_from_directory(save_dir, filename, as_attachment=True) @@ -46,7 +49,7 @@ def upload_index_files(): return {"status": "no file name"} # saves index files - save_dir = os.path.join("indexes", user, job_name) + save_dir = os.path.join(current_dir, "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")) diff --git a/application/api/user/routes.py b/application/api/user/routes.py index d661af415..f04631d23 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -6,6 +6,9 @@ from bson.objectid import ObjectId from werkzeug.utils import secure_filename import http.client +from celery.result import AsyncResult + +from application.api.user.tasks import ingest from application.core.settings import settings mongo = MongoClient(settings.MONGO_URI) @@ -14,6 +17,8 @@ vectors_collection = db["vectors"] user = Blueprint('user', __name__) +current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + @user.route("/api/delete_conversation", methods=["POST"]) def delete_conversation(): # deletes a conversation from the database @@ -111,13 +116,13 @@ def upload_file(): if file: filename = secure_filename(file.filename) # save dir - save_dir = os.path.join(app.config["UPLOAD_FOLDER"], user, job_name) + save_dir = os.path.join(current_dir, settings.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 = ingest.delay(settings.UPLOAD_FOLDER, [".rst", ".md", ".pdf", ".txt"], job_name, filename, user) # task id task_id = task.id return {"status": "ok", "task_id": task_id} diff --git a/application/api/user/tasks.py b/application/api/user/tasks.py new file mode 100644 index 000000000..a3474939a --- /dev/null +++ b/application/api/user/tasks.py @@ -0,0 +1,7 @@ +from application.worker import ingest_worker +from application.celery import celery + +@celery.task(bind=True) +def ingest(self, directory, formats, name_job, filename, user): + resp = ingest_worker(self, directory, formats, name_job, filename, user) + return resp diff --git a/application/app.py b/application/app.py index 55c8e5c3b..5941ed6f8 100644 --- a/application/app.py +++ b/application/app.py @@ -2,15 +2,14 @@ import dotenv -from celery import Celery +from application.celery import celery from flask import Flask, request, redirect -from pymongo import MongoClient from application.core.settings import settings -from application.worker import ingest_worker from application.api.user.routes import user from application.api.answer.routes import answer +from application.api.internal.routes import internal @@ -30,21 +29,22 @@ app = Flask(__name__) app.register_blueprint(user) app.register_blueprint(answer) +app.register_blueprint(internal) 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 app.config["MONGO_URI"] = settings.MONGO_URI -celery = Celery() +#celery = Celery() celery.config_from_object("application.celeryconfig") -@celery.task(bind=True) -def ingest(self, directory, formats, name_job, filename, user): - resp = ingest_worker(self, directory, formats, name_job, filename, user) - return resp +# @celery.task(bind=True) +# def ingest(self, directory, formats, name_job, filename, user): +# resp = ingest_worker(self, directory, formats, name_job, filename, user) +# return resp @app.route("/") diff --git a/application/celery.py b/application/celery.py index 461dc53e9..3b4819e73 100644 --- a/application/celery.py +++ b/application/celery.py @@ -1,10 +1,9 @@ from celery import Celery -from app import create_app +from application.core.settings import settings def make_celery(app_name=__name__): - app = create_app() - celery = Celery(app_name, broker=app.config['CELERY_BROKER_URL']) - celery.conf.update(app.config) + celery = Celery(app_name, broker=settings.CELERY_BROKER_URL) + celery.conf.update(settings) return celery celery = make_celery() diff --git a/application/core/settings.py b/application/core/settings.py index 08673475e..d127c293b 100644 --- a/application/core/settings.py +++ b/application/core/settings.py @@ -12,6 +12,7 @@ class Settings(BaseSettings): MODEL_PATH: str = "./models/gpt4all-model.bin" TOKENS_MAX_HISTORY: int = 150 SELF_HOSTED_MODEL: bool = False + UPLOAD_FOLDER: str = "inputs" API_URL: str = "http://localhost:7091" # backend url for celery worker diff --git a/application/vectorstore/base.py b/application/vectorstore/base.py index e69de29bb..cf3623e4c 100644 --- a/application/vectorstore/base.py +++ b/application/vectorstore/base.py @@ -0,0 +1,45 @@ +from abc import ABC, abstractmethod +import os +from langchain.embeddings import ( + OpenAIEmbeddings, + HuggingFaceHubEmbeddings, + CohereEmbeddings, + HuggingFaceInstructEmbeddings, +) +from application.core.settings import settings + +class BaseVectorStore(ABC): + def __init__(self): + pass + + @abstractmethod + def search(self, *args, **kwargs): + pass + + def is_azure_configured(self): + return settings.OPENAI_API_BASE and settings.OPENAI_API_VERSION and settings.AZURE_DEPLOYMENT_NAME + + def _get_docsearch(self, embeddings_name, embeddings_key=None): + embeddings_factory = { + "openai_text-embedding-ada-002": OpenAIEmbeddings, + "huggingface_sentence-transformers/all-mpnet-base-v2": HuggingFaceHubEmbeddings, + "huggingface_hkunlp/instructor-large": HuggingFaceInstructEmbeddings, + "cohere_medium": CohereEmbeddings + } + + if embeddings_name not in embeddings_factory: + raise ValueError(f"Invalid embeddings_name: {embeddings_name}") + + if embeddings_name == "openai_text-embedding-ada-002": + if self.is_azure_configured(): + os.environ["OPENAI_API_TYPE"] = "azure" + embedding_instance = embeddings_factory[embeddings_name](model=settings.AZURE_EMBEDDINGS_DEPLOYMENT_NAME) + else: + embedding_instance = embeddings_factory[embeddings_name](openai_api_key=embeddings_key) + elif embeddings_name == "cohere_medium": + embedding_instance = embeddings_factory[embeddings_name](cohere_api_key=embeddings_key) + else: + embedding_instance = embeddings_factory[embeddings_name]() + + return embedding_instance + diff --git a/application/vectorstore/faiss.py b/application/vectorstore/faiss.py new file mode 100644 index 000000000..9a562dce6 --- /dev/null +++ b/application/vectorstore/faiss.py @@ -0,0 +1,15 @@ +from application.vectorstore.base import BaseVectorStore +from langchain import FAISS +from application.core.settings import settings + +class FaissStore(BaseVectorStore): + + def __init__(self, path, embeddings_key): + super().__init__() + self.path = path + self.docsearch = FAISS.load_local( + self.path, self._get_docsearch(settings.EMBEDDINGS_NAME, settings.EMBEDDINGS_KEY) + ) + + def search(self, *args, **kwargs): + return self.docsearch.similarity_search(*args, **kwargs) diff --git a/application/worker.py b/application/worker.py index da955a7ec..91c19c309 100644 --- a/application/worker.py +++ b/application/worker.py @@ -27,6 +27,7 @@ def metadata_from_filename(title): def generate_random_string(length): return ''.join([string.ascii_letters[i % 52] for i in range(length)]) +current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) def ingest_worker(self, directory, formats, name_job, filename, user): # directory = 'inputs' or 'temp' @@ -43,9 +44,13 @@ def ingest_worker(self, directory, formats, name_job, filename, user): min_tokens = 150 max_tokens = 1250 full_path = directory + '/' + user + '/' + name_job + import sys + print(full_path, file=sys.stderr) # check if API_URL env variable is set file_data = {'name': name_job, 'file': filename, 'user': user} response = requests.get(urljoin(settings.API_URL, "/api/download"), params=file_data) + # check if file is in the response + print(response, file=sys.stderr) file = response.content if not os.path.exists(full_path): From e6849b85d1e215e66b1f5051c4a9fe9b1f903aa5 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 27 Sep 2023 17:02:47 +0100 Subject: [PATCH 4/8] Create huggingface.py --- application/llm/huggingface.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 application/llm/huggingface.py diff --git a/application/llm/huggingface.py b/application/llm/huggingface.py new file mode 100644 index 000000000..e9cc47be6 --- /dev/null +++ b/application/llm/huggingface.py @@ -0,0 +1,31 @@ +from application.llm.base import BaseLLM + +class HuggingFaceLLM(BaseLLM): + + def __init__(self, api_key, llm_name='Arc53/DocsGPT-7B'): + global hf + + from langchain.llms import HuggingFacePipeline + from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline + tokenizer = AutoTokenizer.from_pretrained(llm_name) + model = AutoModelForCausalLM.from_pretrained(llm_name) + pipe = pipeline( + "text-generation", model=model, + tokenizer=tokenizer, max_new_tokens=2000, + device_map="auto", eos_token_id=tokenizer.eos_token_id + ) + hf = HuggingFacePipeline(pipeline=pipe) + + def gen(self, model, engine, messages, stream=False, **kwargs): + context = messages[0]['content'] + user_question = messages[-1]['content'] + prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n" + + result = hf(prompt) + + return result.content + + def gen_stream(self, model, engine, messages, stream=True, **kwargs): + + raise NotImplementedError("HuggingFaceLLM Streaming is not implemented yet.") + From b8acb860aa697c15eeef2082902a30edc9ff9361 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 27 Sep 2023 17:54:57 +0100 Subject: [PATCH 5/8] some tests --- .gitignore | 3 ++- application/app.py | 12 ----------- tests/llm/test_openai.py | 34 ++++++++++++++++++++++++++++++ tests/test_app.py | 45 ++++++++++++++++------------------------ tests/test_celery.py | 18 ++++++++++++++++ tests/test_error.py | 41 ++++++++++++++++++++++++++++++++++++ 6 files changed, 113 insertions(+), 40 deletions(-) create mode 100644 tests/llm/test_openai.py create mode 100644 tests/test_celery.py create mode 100644 tests/test_error.py diff --git a/.gitignore b/.gitignore index a1592a40b..a896c299f 100644 --- a/.gitignore +++ b/.gitignore @@ -169,4 +169,5 @@ application/vectors/ **/yarn.lock -node_modules/ \ No newline at end of file +node_modules/ +.vscode/settings.json diff --git a/application/app.py b/application/app.py index 5941ed6f8..41b821b7d 100644 --- a/application/app.py +++ b/application/app.py @@ -34,19 +34,10 @@ app.config["CELERY_BROKER_URL"] = settings.CELERY_BROKER_URL app.config["CELERY_RESULT_BACKEND"] = settings.CELERY_RESULT_BACKEND app.config["MONGO_URI"] = settings.MONGO_URI -#celery = Celery() celery.config_from_object("application.celeryconfig") - - -# @celery.task(bind=True) -# def ingest(self, directory, formats, name_job, filename, user): -# resp = ingest_worker(self, directory, formats, name_job, filename, user) -# return resp - - @app.route("/") def home(): """ @@ -63,9 +54,6 @@ def home(): - - - # handling CORS @app.after_request def after_request(response): diff --git a/tests/llm/test_openai.py b/tests/llm/test_openai.py new file mode 100644 index 000000000..5baca037f --- /dev/null +++ b/tests/llm/test_openai.py @@ -0,0 +1,34 @@ +# FILEPATH: /Users/alextu/Documents/GitHub/DocsGPT/tests/llm/test_openai.py + +import unittest +from unittest.mock import patch, Mock +from application.llm.openai import OpenAILLM, AzureOpenAILLM + +class TestOpenAILLM(unittest.TestCase): + + def setUp(self): + self.api_key = "test_api_key" + self.llm = OpenAILLM(self.api_key) + + def test_init(self): + self.assertEqual(self.llm.api_key, self.api_key) + + @patch('application.llm.openai.openai.ChatCompletion.create') + def test_gen(self, mock_create): + model = "test_model" + engine = "test_engine" + messages = ["test_message"] + response = {"choices": [{"message": {"content": "test_response"}}]} + mock_create.return_value = response + result = self.llm.gen(model, engine, messages) + self.assertEqual(result, "test_response") + + @patch('application.llm.openai.openai.ChatCompletion.create') + def test_gen_stream(self, mock_create): + model = "test_model" + engine = "test_engine" + messages = ["test_message"] + response = [{"choices": [{"delta": {"content": "test_response"}}]}] + mock_create.return_value = response + result = list(self.llm.gen_stream(model, engine, messages)) + self.assertEqual(result, ["test_response"]) diff --git a/tests/test_app.py b/tests/test_app.py index a86234aee..96e88ea11 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,32 +1,23 @@ -from application.app import get_vectorstore, is_azure_configured -import os +from flask import Flask +from application.api.answer.routes import answer +from application.api.internal.routes import internal +from application.api.user.routes import user +from application.core.settings import settings -# Test cases for get_vectorstore function -def test_no_active_docs(): - data = {} - assert get_vectorstore(data) == os.path.join("application", "") -def test_local_default_active_docs(): - data = {"active_docs": "local/default"} - assert get_vectorstore(data) == os.path.join("application", "") +def test_app_config(): + app = Flask(__name__) + app.register_blueprint(user) + app.register_blueprint(answer) + app.register_blueprint(internal) + 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 + app.config["MONGO_URI"] = settings.MONGO_URI - -def test_local_non_default_active_docs(): - data = {"active_docs": "local/something"} - assert get_vectorstore(data) == os.path.join("application", "indexes/local/something") - - -def test_default_active_docs(): - data = {"active_docs": "default"} - assert get_vectorstore(data) == os.path.join("application", "") - - -def test_complex_active_docs(): - data = {"active_docs": "local/other/path"} - assert get_vectorstore(data) == os.path.join("application", "indexes/local/other/path") - - -def test_is_azure_configured(): - assert not is_azure_configured() + assert app.config["UPLOAD_FOLDER"] == "inputs" + assert app.config["CELERY_BROKER_URL"] == settings.CELERY_BROKER_URL + assert app.config["CELERY_RESULT_BACKEND"] == settings.CELERY_RESULT_BACKEND + assert app.config["MONGO_URI"] == settings.MONGO_URI diff --git a/tests/test_celery.py b/tests/test_celery.py new file mode 100644 index 000000000..d4b86b61c --- /dev/null +++ b/tests/test_celery.py @@ -0,0 +1,18 @@ +from celery import Celery +from unittest.mock import patch, MagicMock +from application.core.settings import settings +from application.celery import make_celery + + +@patch('application.celery.Celery') +def test_make_celery(mock_celery): + # Arrange + app_name = 'test_app_name' + + # Act + celery = make_celery(app_name) + + # Assert + mock_celery.assert_called_once_with(app_name, broker=settings.CELERY_BROKER_URL) + celery.conf.update.assert_called_once_with(settings) + assert celery == mock_celery.return_value \ No newline at end of file diff --git a/tests/test_error.py b/tests/test_error.py new file mode 100644 index 000000000..696a0fb34 --- /dev/null +++ b/tests/test_error.py @@ -0,0 +1,41 @@ +# FILEPATH: /Users/alextu/Documents/GitHub/DocsGPT/tests/test_error.py + +import pytest +from flask import Flask +from application.error import bad_request, response_error + + +@pytest.fixture +def app(): + app = Flask(__name__) + return app + + +def test_bad_request_with_message(app): + with app.app_context(): + message = "Invalid input" + response = bad_request(status_code=400, message=message) + assert response.status_code == 400 + assert response.json == {'error': 'Bad Request', 'message': message} + + +def test_bad_request_without_message(app): + with app.app_context(): + response = bad_request(status_code=400) + assert response.status_code == 400 + assert response.json == {'error': 'Bad Request'} + + +def test_response_error_with_message(app): + with app.app_context(): + message = "Something went wrong" + response = response_error(code_status=500, message=message) + assert response.status_code == 500 + assert response.json == {'error': 'Internal Server Error', 'message': message} + + +def test_response_error_without_message(app): + with app.app_context(): + response = response_error(code_status=500) + assert response.status_code == 500 + assert response.json == {'error': 'Internal Server Error'} \ No newline at end of file From 852de8bdfc99dc79c10cdf8835d989018c861e4f Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 27 Sep 2023 18:01:40 +0100 Subject: [PATCH 6/8] ruff linting --- application/api/answer/routes.py | 25 ++++++++++++++----------- application/api/internal/routes.py | 3 +-- application/llm/base.py | 1 - application/vectorstore/base.py | 12 +++++++++--- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index 8f64a8f91..ae9ef71f7 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -329,17 +329,20 @@ def api_answer(): 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: " + - result["answer"]}, - {"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 = llm.gen(model=gpt_model, engine=settings.AZURE_DEPLOYMENT_NAME, - messages=messages_summary, max_tokens=30) + 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\n" + "User: " + question + "\n\n" + "AI: " + result["answer"]}, + {"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 = llm.gen( + model=gpt_model, + engine=settings.AZURE_DEPLOYMENT_NAME, + messages=messages_summary, + max_tokens=30 + ) conversation_id = conversations_collection.insert_one( {"user": "local", "date": datetime.datetime.utcnow(), diff --git a/application/api/internal/routes.py b/application/api/internal/routes.py index 13c44724a..ca6da1747 100644 --- a/application/api/internal/routes.py +++ b/application/api/internal/routes.py @@ -1,7 +1,6 @@ import os import datetime -from flask import Blueprint, request, jsonify, send_from_directory -import requests +from flask import Blueprint, request, send_from_directory from pymongo import MongoClient from werkzeug.utils import secure_filename diff --git a/application/llm/base.py b/application/llm/base.py index 16c91303d..e08a3b090 100644 --- a/application/llm/base.py +++ b/application/llm/base.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -import json class BaseLLM(ABC): diff --git a/application/vectorstore/base.py b/application/vectorstore/base.py index cf3623e4c..ad481744f 100644 --- a/application/vectorstore/base.py +++ b/application/vectorstore/base.py @@ -33,11 +33,17 @@ def _get_docsearch(self, embeddings_name, embeddings_key=None): if embeddings_name == "openai_text-embedding-ada-002": if self.is_azure_configured(): os.environ["OPENAI_API_TYPE"] = "azure" - embedding_instance = embeddings_factory[embeddings_name](model=settings.AZURE_EMBEDDINGS_DEPLOYMENT_NAME) + embedding_instance = embeddings_factory[embeddings_name]( + model=settings.AZURE_EMBEDDINGS_DEPLOYMENT_NAME + ) else: - embedding_instance = embeddings_factory[embeddings_name](openai_api_key=embeddings_key) + embedding_instance = embeddings_factory[embeddings_name]( + openai_api_key=embeddings_key + ) elif embeddings_name == "cohere_medium": - embedding_instance = embeddings_factory[embeddings_name](cohere_api_key=embeddings_key) + embedding_instance = embeddings_factory[embeddings_name]( + cohere_api_key=embeddings_key + ) else: embedding_instance = embeddings_factory[embeddings_name]() From 83ae3e8371f8439e477060e397ce0ec0b41fd3ee Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 27 Sep 2023 18:04:07 +0100 Subject: [PATCH 7/8] more ruff fixes --- tests/test_app.py | 2 +- tests/test_celery.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 96e88ea11..c32c40353 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -12,7 +12,7 @@ def test_app_config(): app.register_blueprint(user) app.register_blueprint(answer) app.register_blueprint(internal) - app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER = "inputs" + app.config["UPLOAD_FOLDER"] = "inputs" app.config["CELERY_BROKER_URL"] = settings.CELERY_BROKER_URL app.config["CELERY_RESULT_BACKEND"] = settings.CELERY_RESULT_BACKEND app.config["MONGO_URI"] = settings.MONGO_URI diff --git a/tests/test_celery.py b/tests/test_celery.py index d4b86b61c..bfb6a3585 100644 --- a/tests/test_celery.py +++ b/tests/test_celery.py @@ -1,5 +1,4 @@ -from celery import Celery -from unittest.mock import patch, MagicMock +from unittest.mock import patch from application.core.settings import settings from application.celery import make_celery From 039062d071b1a8e93f1f33bf6b67104d5ca0425d Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 27 Sep 2023 18:10:26 +0100 Subject: [PATCH 8/8] ruff fix --- tests/llm/test_openai.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/llm/test_openai.py b/tests/llm/test_openai.py index 5baca037f..8fcc2f274 100644 --- a/tests/llm/test_openai.py +++ b/tests/llm/test_openai.py @@ -1,8 +1,8 @@ # FILEPATH: /Users/alextu/Documents/GitHub/DocsGPT/tests/llm/test_openai.py import unittest -from unittest.mock import patch, Mock -from application.llm.openai import OpenAILLM, AzureOpenAILLM +from unittest.mock import patch +from application.llm.openai import OpenAILLM class TestOpenAILLM(unittest.TestCase):