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):