From 32975cdf73eabc955fd026fde14f9c1900950dae Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Wed, 17 May 2023 18:40:31 +0800 Subject: [PATCH 01/66] =?UTF-8?q?update=EF=BC=9Atext=20splitter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../source_embedding/chinese_text_splitter.py | 60 +++++++++++++++++++ pilot/source_embedding/pdf_loader.py | 53 ++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 pilot/source_embedding/chinese_text_splitter.py create mode 100644 pilot/source_embedding/pdf_loader.py diff --git a/pilot/source_embedding/chinese_text_splitter.py b/pilot/source_embedding/chinese_text_splitter.py new file mode 100644 index 000000000..091276af6 --- /dev/null +++ b/pilot/source_embedding/chinese_text_splitter.py @@ -0,0 +1,60 @@ +from langchain.text_splitter import CharacterTextSplitter +import re +from typing import List +# from configs.model_config import SENTENCE_SIZE + + +class ChineseTextSplitter(CharacterTextSplitter): + def __init__(self, pdf: bool = False, sentence_size: int = None, **kwargs): + super().__init__(**kwargs) + self.pdf = pdf + self.sentence_size = sentence_size + + def split_text1(self, text: str) -> List[str]: + if self.pdf: + text = re.sub(r"\n{3,}", "\n", text) + text = re.sub('\s', ' ', text) + text = text.replace("\n\n", "") + sent_sep_pattern = re.compile('([﹒﹔﹖﹗.。!?]["’”」』]{0,2}|(?=["‘“「『]{1,2}|$))') # del :; + sent_list = [] + for ele in sent_sep_pattern.split(text): + if sent_sep_pattern.match(ele) and sent_list: + sent_list[-1] += ele + elif ele: + sent_list.append(ele) + return sent_list + + def split_text(self, text: str) -> List[str]: ##此处需要进一步优化逻辑 + if self.pdf: + text = re.sub(r"\n{3,}", r"\n", text) + text = re.sub('\s', " ", text) + text = re.sub("\n\n", "", text) + + text = re.sub(r'([;;.!?。!?\?])([^”’])', r"\1\n\2", text) # 单字符断句符 + text = re.sub(r'(\.{6})([^"’”」』])', r"\1\n\2", text) # 英文省略号 + text = re.sub(r'(\…{2})([^"’”」』])', r"\1\n\2", text) # 中文省略号 + text = re.sub(r'([;;!?。!?\?]["’”」』]{0,2})([^;;!?,。!?\?])', r'\1\n\2', text) + # 如果双引号前有终止符,那么双引号才是句子的终点,把分句符\n放到双引号后,注意前面的几句都小心保留了双引号 + text = text.rstrip() # 段尾如果有多余的\n就去掉它 + # 很多规则中会考虑分号;,但是这里我把它忽略不计,破折号、英文双引号等同样忽略,需要的再做些简单调整即可。 + ls = [i for i in text.split("\n") if i] + for ele in ls: + if len(ele) > self.sentence_size: + ele1 = re.sub(r'([,,.]["’”」』]{0,2})([^,,.])', r'\1\n\2', ele) + ele1_ls = ele1.split("\n") + for ele_ele1 in ele1_ls: + if len(ele_ele1) > self.sentence_size: + ele_ele2 = re.sub(r'([\n]{1,}| {2,}["’”」』]{0,2})([^\s])', r'\1\n\2', ele_ele1) + ele2_ls = ele_ele2.split("\n") + for ele_ele2 in ele2_ls: + if len(ele_ele2) > self.sentence_size: + ele_ele3 = re.sub('( ["’”」』]{0,2})([^ ])', r'\1\n\2', ele_ele2) + ele2_id = ele2_ls.index(ele_ele2) + ele2_ls = ele2_ls[:ele2_id] + [i for i in ele_ele3.split("\n") if i] + ele2_ls[ + ele2_id + 1:] + ele_id = ele1_ls.index(ele_ele1) + ele1_ls = ele1_ls[:ele_id] + [i for i in ele2_ls if i] + ele1_ls[ele_id + 1:] + + id = ls.index(ele) + ls = ls[:id] + [i for i in ele1_ls if i] + ls[id + 1:] + return ls diff --git a/pilot/source_embedding/pdf_loader.py b/pilot/source_embedding/pdf_loader.py new file mode 100644 index 000000000..aa7cf4da5 --- /dev/null +++ b/pilot/source_embedding/pdf_loader.py @@ -0,0 +1,53 @@ +"""Loader that loads image files.""" +from typing import List + +from langchain.document_loaders.unstructured import UnstructuredFileLoader +from paddleocr import PaddleOCR +import os +import fitz + + +class UnstructuredPaddlePDFLoader(UnstructuredFileLoader): + """Loader that uses unstructured to load image files, such as PNGs and JPGs.""" + + def _get_elements(self) -> List: + def pdf_ocr_txt(filepath, dir_path="tmp_files"): + full_dir_path = os.path.join(os.path.dirname(filepath), dir_path) + if not os.path.exists(full_dir_path): + os.makedirs(full_dir_path) + filename = os.path.split(filepath)[-1] + ocr = PaddleOCR(lang="ch", use_gpu=False, show_log=False) + doc = fitz.open(filepath) + txt_file_path = os.path.join(full_dir_path, "%s.txt" % (filename)) + img_name = os.path.join(full_dir_path, '.tmp.png') + with open(txt_file_path, 'w', encoding='utf-8') as fout: + + for i in range(doc.page_count): + page = doc[i] + text = page.get_text("") + fout.write(text) + fout.write("\n") + + img_list = page.get_images() + for img in img_list: + pix = fitz.Pixmap(doc, img[0]) + + pix.save(img_name) + + result = ocr.ocr(img_name) + ocr_result = [i[1][0] for line in result for i in line] + fout.write("\n".join(ocr_result)) + os.remove(img_name) + return txt_file_path + + txt_file_path = pdf_ocr_txt(self.file_path) + from unstructured.partition.text import partition_text + return partition_text(filename=txt_file_path, **self.unstructured_kwargs) + + +if __name__ == "__main__": + filepath = os.path.join(os.path.dirname(os.path.dirname(__file__)), "content", "samples", "test.pdf") + loader = UnstructuredPaddlePDFLoader(filepath, mode="elements") + docs = loader.load() + for doc in docs: + print(doc) From 2bdfcdec939f1bc41e3ed2bad814c7b8817d3431 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Wed, 17 May 2023 22:12:22 +0800 Subject: [PATCH 02/66] update: replace embedding model --- pilot/configs/model_config.py | 3 ++- pilot/server/webserver.py | 20 ++++++++++++++++---- pilot/source_embedding/pdf_embedding.py | 4 +--- requirements.txt | 3 ++- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pilot/configs/model_config.py b/pilot/configs/model_config.py index 12c7e33da..819152845 100644 --- a/pilot/configs/model_config.py +++ b/pilot/configs/model_config.py @@ -20,6 +20,7 @@ DEVICE = "cuda" if torch.cuda.is_available() else "cpu" LLM_MODEL_CONFIG = { "flan-t5-base": os.path.join(MODEL_PATH, "flan-t5-base"), "vicuna-13b": os.path.join(MODEL_PATH, "vicuna-13b"), + "text2vec": os.path.join(MODEL_PATH, "text2vec"), "sentence-transforms": os.path.join(MODEL_PATH, "all-MiniLM-L6-v2") } @@ -28,7 +29,7 @@ VECTOR_SEARCH_TOP_K = 3 LLM_MODEL = "vicuna-13b" LIMIT_MODEL_CONCURRENCY = 5 MAX_POSITION_EMBEDDINGS = 4096 -VICUNA_MODEL_SERVER = "http://121.41.167.183:8000" +VICUNA_MODEL_SERVER = "http://121.41.227.141:8000" # Load model config ISLOAD_8BIT = True diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index 139caab4d..e139ff09b 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -242,10 +242,10 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re if mode == conversation_types["custome"] and not db_selector: persist_dir = os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, vector_store_name["vs_name"] + ".vectordb") print("向量数据库持久化地址: ", persist_dir) - knowledge_embedding_client = KnowledgeEmbedding(file_path="", model_name=LLM_MODEL_CONFIG["sentence-transforms"], vector_store_config={"vector_store_name": vector_store_name["vs_name"], + knowledge_embedding_client = KnowledgeEmbedding(file_path="", model_name=LLM_MODEL_CONFIG["text2vec"], vector_store_config={"vector_store_name": vector_store_name["vs_name"], "vector_store_path": KNOWLEDGE_UPLOAD_ROOT_PATH}) query = state.messages[-2][1] - docs = knowledge_embedding_client.similar_search(query, 1) + docs = knowledge_embedding_client.similar_search(query, 10) context = [d.page_content for d in docs] prompt_template = PromptTemplate( template=conv_qa_prompt_template, @@ -254,6 +254,18 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re result = prompt_template.format(context="\n".join(context), question=query) state.messages[-2][1] = result prompt = state.get_prompt() + if len(prompt) > 4000: + logger.info("prompt length greater than 4000, rebuild") + docs = knowledge_embedding_client.similar_search(query, 5) + context = [d.page_content for d in docs] + prompt_template = PromptTemplate( + template=conv_qa_prompt_template, + input_variables=["context", "question"] + ) + result = prompt_template.format(context="\n".join(context), question=query) + state.messages[-2][1] = result + prompt = state.get_prompt() + print(len(prompt)) state.messages[-2][1] = query skip_echo_len = len(prompt.replace("", " ")) + 1 @@ -420,7 +432,7 @@ def build_single_model_ui(): max_output_tokens = gr.Slider( minimum=0, maximum=1024, - value=1024, + value=512, step=64, interactive=True, label="最大输出Token数", @@ -570,7 +582,7 @@ def knowledge_embedding_store(vs_id, files): shutil.move(file.name, os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, vs_id, filename)) knowledge_embedding_client = KnowledgeEmbedding( file_path=os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, vs_id, filename), - model_name=LLM_MODEL_CONFIG["sentence-transforms"], + model_name=LLM_MODEL_CONFIG["text2vec"], vector_store_config={ "vector_store_name": vector_store_name["vs_name"], "vector_store_path": KNOWLEDGE_UPLOAD_ROOT_PATH}) diff --git a/pilot/source_embedding/pdf_embedding.py b/pilot/source_embedding/pdf_embedding.py index 617190fe5..e162aefd8 100644 --- a/pilot/source_embedding/pdf_embedding.py +++ b/pilot/source_embedding/pdf_embedding.py @@ -17,8 +17,6 @@ class PDFEmbedding(SourceEmbedding): self.file_path = file_path self.model_name = model_name self.vector_store_config = vector_store_config - # SourceEmbedding(file_path =file_path, ); - SourceEmbedding(file_path, model_name, vector_store_config) @register def read(self): @@ -30,7 +28,7 @@ class PDFEmbedding(SourceEmbedding): def data_process(self, documents: List[Document]): i = 0 for d in documents: - documents[i].page_content = d.page_content.replace(" ", "").replace("\n", "") + documents[i].page_content = d.page_content.replace("\n", "") i += 1 return documents diff --git a/requirements.txt b/requirements.txt index 5654dba6f..3bca421f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -72,4 +72,5 @@ chromadb markdown2 colorama playsound -distro \ No newline at end of file +distro +pypdf \ No newline at end of file From def9fd833059eb84720b99b38a4f9566c7feb05a Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Wed, 17 May 2023 22:24:25 +0800 Subject: [PATCH 03/66] fix:update model name --- pilot/configs/model_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pilot/configs/model_config.py b/pilot/configs/model_config.py index 819152845..128e87513 100644 --- a/pilot/configs/model_config.py +++ b/pilot/configs/model_config.py @@ -20,7 +20,7 @@ DEVICE = "cuda" if torch.cuda.is_available() else "cpu" LLM_MODEL_CONFIG = { "flan-t5-base": os.path.join(MODEL_PATH, "flan-t5-base"), "vicuna-13b": os.path.join(MODEL_PATH, "vicuna-13b"), - "text2vec": os.path.join(MODEL_PATH, "text2vec"), + "text2vec": os.path.join(MODEL_PATH, "text2vec-large-chinese"), "sentence-transforms": os.path.join(MODEL_PATH, "all-MiniLM-L6-v2") } From 406c5fa4832630ee0a9b69e220ab05ae03ba99aa Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Wed, 17 May 2023 22:26:49 +0800 Subject: [PATCH 04/66] update:config update --- pilot/configs/model_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pilot/configs/model_config.py b/pilot/configs/model_config.py index 128e87513..4781cd08a 100644 --- a/pilot/configs/model_config.py +++ b/pilot/configs/model_config.py @@ -44,4 +44,4 @@ DB_SETTINGS = { } VS_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "vs_store") -KNOWLEDGE_UPLOAD_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "knowledge") \ No newline at end of file +KNOWLEDGE_UPLOAD_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") \ No newline at end of file From 3084123cefd5039801bab94cd503bd45e98967c4 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Wed, 17 May 2023 22:50:28 +0800 Subject: [PATCH 05/66] update:prompt length --- pilot/server/webserver.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index e139ff09b..ad9f66b61 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -256,8 +256,7 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re prompt = state.get_prompt() if len(prompt) > 4000: logger.info("prompt length greater than 4000, rebuild") - docs = knowledge_embedding_client.similar_search(query, 5) - context = [d.page_content for d in docs] + context = context[:2000] prompt_template = PromptTemplate( template=conv_qa_prompt_template, input_variables=["context", "question"] From c0115061c26af771fd21bdc993c8032920b03638 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Wed, 17 May 2023 23:03:51 +0800 Subject: [PATCH 06/66] update:pdf split chunk --- pilot/server/webserver.py | 5 ++++- pilot/source_embedding/pdf_embedding.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index ad9f66b61..a76cf3400 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -254,6 +254,8 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re result = prompt_template.format(context="\n".join(context), question=query) state.messages[-2][1] = result prompt = state.get_prompt() + print("prompt length:" + len(prompt)) + if len(prompt) > 4000: logger.info("prompt length greater than 4000, rebuild") context = context[:2000] @@ -264,7 +266,8 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re result = prompt_template.format(context="\n".join(context), question=query) state.messages[-2][1] = result prompt = state.get_prompt() - print(len(prompt)) + print("new prompt length:" + len(prompt)) + state.messages[-2][1] = query skip_echo_len = len(prompt.replace("", " ")) + 1 diff --git a/pilot/source_embedding/pdf_embedding.py b/pilot/source_embedding/pdf_embedding.py index e162aefd8..557637c5a 100644 --- a/pilot/source_embedding/pdf_embedding.py +++ b/pilot/source_embedding/pdf_embedding.py @@ -6,6 +6,7 @@ from langchain.document_loaders import PyPDFLoader from langchain.schema import Document from pilot.source_embedding import SourceEmbedding, register +from pilot.source_embedding.chinese_text_splitter import ChineseTextSplitter class PDFEmbedding(SourceEmbedding): @@ -22,7 +23,8 @@ class PDFEmbedding(SourceEmbedding): def read(self): """Load from pdf path.""" loader = PyPDFLoader(self.file_path) - return loader.load() + textsplitter = ChineseTextSplitter(pdf=True, sentence_size=100) + return loader.load_and_split(textsplitter) @register def data_process(self, documents: List[Document]): From e8a7fd4546d4dfb91845de4721bc26cdd00a2785 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Wed, 17 May 2023 23:12:00 +0800 Subject: [PATCH 07/66] fix:log --- pilot/server/webserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index a76cf3400..e3d7a6dbb 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -266,7 +266,7 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re result = prompt_template.format(context="\n".join(context), question=query) state.messages[-2][1] = result prompt = state.get_prompt() - print("new prompt length:" + len(prompt)) + print("new prompt length:" + str(len(prompt))) state.messages[-2][1] = query skip_echo_len = len(prompt.replace("", " ")) + 1 From b92ecf219964855cf3c1eaa621d014f8805b4a34 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Thu, 18 May 2023 15:39:28 +0800 Subject: [PATCH 08/66] fix:prompt --- pilot/server/webserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index e3d7a6dbb..c2a780244 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -254,7 +254,7 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re result = prompt_template.format(context="\n".join(context), question=query) state.messages[-2][1] = result prompt = state.get_prompt() - print("prompt length:" + len(prompt)) + print("prompt length:" + str(len(prompt))) if len(prompt) > 4000: logger.info("prompt length greater than 4000, rebuild") From 9ddd0893813f2c3ae2320c8bf41fe663b6c9063f Mon Sep 17 00:00:00 2001 From: csunny Date: Thu, 18 May 2023 17:28:06 +0800 Subject: [PATCH 09/66] fix run path --- pilot/__init__.py | 7 ++++++- pilot/model/adapter.py | 2 +- pilot/server/llmserver.py | 15 ++++++++++----- pilot/server/webserver.py | 7 +++++++ 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/pilot/__init__.py b/pilot/__init__.py index a1531040e..b6865c44c 100644 --- a/pilot/__init__.py +++ b/pilot/__init__.py @@ -1,7 +1,12 @@ from pilot.source_embedding import (SourceEmbedding, register) +import os +import sys + +ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(ROOT_PATH) __all__ = [ "SourceEmbedding", "register" -] \ No newline at end of file +] diff --git a/pilot/model/adapter.py b/pilot/model/adapter.py index fa5803d3a..9afd2c01f 100644 --- a/pilot/model/adapter.py +++ b/pilot/model/adapter.py @@ -24,7 +24,7 @@ class BaseLLMAdaper: return model, tokenizer -llm_model_adapters = List[BaseLLMAdaper] = [] +llm_model_adapters: List[BaseLLMAdaper] = [] # Register llm models to adapters, by this we can use multi models. def register_llm_model_adapters(cls): diff --git a/pilot/server/llmserver.py b/pilot/server/llmserver.py index 2b29949a3..e341cc457 100644 --- a/pilot/server/llmserver.py +++ b/pilot/server/llmserver.py @@ -1,14 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import os import uvicorn import asyncio import json +import sys from typing import Optional, List from fastapi import FastAPI, Request, BackgroundTasks from fastapi.responses import StreamingResponse -from pilot.model.inference import generate_stream from pydantic import BaseModel + +global_counter = 0 +model_semaphore = None + +ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(ROOT_PATH) + +from pilot.model.inference import generate_stream from pilot.model.inference import generate_output, get_embeddings from pilot.model.loader import ModelLoader @@ -19,10 +28,6 @@ from pilot.configs.config import Config CFG = Config() model_path = LLM_MODEL_CONFIG[CFG.LLM_MODEL] - -global_counter = 0 -model_semaphore = None - ml = ModelLoader(model_path=model_path) model, tokenizer = ml.loader(num_gpus=1, load_8bit=ISLOAD_8BIT, debug=ISDEBUG) #model, tokenizer = load_model(model_path=model_path, device=DEVICE, num_gpus=1, load_8bit=True, debug=False) diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index 0f19bc354..3ed989b07 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -13,6 +13,11 @@ import requests from urllib.parse import urljoin from langchain import PromptTemplate +import os +import sys + +ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(ROOT_PATH) from pilot.configs.model_config import KNOWLEDGE_UPLOAD_ROOT_PATH, LLM_MODEL_CONFIG from pilot.server.vectordb_qa import KnownLedgeBaseQA @@ -30,6 +35,8 @@ from pilot.prompts.generator import PromptGenerator from pilot.commands.exception_not_commands import NotCommands + + from pilot.conversation import ( default_conversation, conv_templates, From 0d370a1a64d868ab34f98599f55de29c04c89464 Mon Sep 17 00:00:00 2001 From: csunny Date: Thu, 18 May 2023 17:32:53 +0800 Subject: [PATCH 10/66] cfg: update --- pilot/__init__.py | 6 ------ pilot/configs/model_config.py | 7 ------- pilot/server/webserver.py | 4 ++-- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/pilot/__init__.py b/pilot/__init__.py index b6865c44c..b1d1cd3d2 100644 --- a/pilot/__init__.py +++ b/pilot/__init__.py @@ -1,11 +1,5 @@ from pilot.source_embedding import (SourceEmbedding, register) -import os -import sys - -ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -sys.path.append(ROOT_PATH) - __all__ = [ "SourceEmbedding", "register" diff --git a/pilot/configs/model_config.py b/pilot/configs/model_config.py index af6c138b5..ad81a6e79 100644 --- a/pilot/configs/model_config.py +++ b/pilot/configs/model_config.py @@ -28,12 +28,5 @@ ISDEBUG = False VECTOR_SEARCH_TOP_K = 3 -# LLM_MODEL = "vicuna-13b" -# LIMIT_MODEL_CONCURRENCY = 5 -# MAX_POSITION_EMBEDDINGS = 4096 -# VICUNA_MODEL_SERVER = "http://121.41.167.183:8000" - - - VS_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "vs_store") KNOWLEDGE_UPLOAD_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "knowledge") \ No newline at end of file diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index 3ed989b07..233655381 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -6,6 +6,7 @@ import os import shutil import uuid import json +import sys import time import gradio as gr import datetime @@ -13,8 +14,7 @@ import requests from urllib.parse import urljoin from langchain import PromptTemplate -import os -import sys + ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(ROOT_PATH) From eb24aef9f118d0b3010635dd3fc8a88da4bbf193 Mon Sep 17 00:00:00 2001 From: csunny Date: Thu, 18 May 2023 17:36:59 +0800 Subject: [PATCH 11/66] fix: python path --- README.md | 5 +---- README.zh.md | 4 ---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/README.md b/README.md index 311fb1f54..c74a120ec 100644 --- a/README.md +++ b/README.md @@ -158,10 +158,7 @@ Alternatively, you can use the following command: cd DB-GPT conda env create -f environment.yml ``` -It is recommended to set the Python package path to avoid runtime errors due to package not found. -``` -echo "/root/workspace/DB-GPT" > /root/miniconda3/env/dbgpt_env/lib/python3.10/site-packages/dbgpt.pth -``` + Notice: You need replace the path to your owner. ### 3. Run diff --git a/README.zh.md b/README.zh.md index 976db50e0..c786cfe35 100644 --- a/README.zh.md +++ b/README.zh.md @@ -157,10 +157,6 @@ pip install -r requirements.txt cd DB-GPT conda env create -f environment.yml ``` -另外需要设置一下python包路径, 避免出现运行时找不到包 -``` -echo "/root/workspace/DB-GPT" > /root/miniconda3/env/dbgpt_env/lib/python3.10/site-packages/dbgpt.pth -``` ### 3. 运行大模型 From 51dd31aa0eda7208de14872b246e7cab65b3b33c Mon Sep 17 00:00:00 2001 From: csunny Date: Thu, 18 May 2023 17:45:36 +0800 Subject: [PATCH 12/66] docs: readme update --- README.md | 7 ----- README.zh.md | 5 ---- environment.yml | 68 ------------------------------------------------- 3 files changed, 80 deletions(-) delete mode 100644 environment.yml diff --git a/README.md b/README.md index c74a120ec..54a8d0de7 100644 --- a/README.md +++ b/README.md @@ -153,13 +153,6 @@ conda create -n dbgpt_env python=3.10 conda activate dbgpt_env pip install -r requirements.txt ``` -Alternatively, you can use the following command: -``` -cd DB-GPT -conda env create -f environment.yml -``` - -Notice: You need replace the path to your owner. ### 3. Run You can refer to this document to obtain the Vicuna weights: [Vicuna](https://github.com/lm-sys/FastChat/blob/main/README.md#model-weights) . diff --git a/README.zh.md b/README.zh.md index c786cfe35..7063dddb3 100644 --- a/README.zh.md +++ b/README.zh.md @@ -151,11 +151,6 @@ conda create -n dbgpt_env python=3.10 conda activate dbgpt_env pip install -r requirements.txt -``` -或者也可以使用命令: -``` -cd DB-GPT -conda env create -f environment.yml ``` ### 3. 运行大模型 diff --git a/environment.yml b/environment.yml deleted file mode 100644 index 99959ec5f..000000000 --- a/environment.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: db_pgt -channels: - - pytorch - - defaults - - anaconda -dependencies: - - python=3.10 - - cudatoolkit - - pip - - pytorch-mutex=1.0=cuda - - pip: - - pytorch - - accelerate==0.16.0 - - aiohttp==3.8.4 - - aiosignal==1.3.1 - - async-timeout==4.0.2 - - attrs==22.2.0 - - bitsandbytes==0.37.0 - - cchardet==2.1.7 - - chardet==5.1.0 - - contourpy==1.0.7 - - cycler==0.11.0 - - filelock==3.9.0 - - fonttools==4.38.0 - - frozenlist==1.3.3 - - huggingface-hub==0.13.4 - - importlib-resources==5.12.0 - - kiwisolver==1.4.4 - - matplotlib==3.7.0 - - multidict==6.0.4 - - packaging==23.0 - - psutil==5.9.4 - - pycocotools==2.0.6 - - pyparsing==3.0.9 - - python-dateutil==2.8.2 - - pyyaml==6.0 - - regex==2022.10.31 - - tokenizers==0.13.2 - - tqdm==4.64.1 - - transformers==4.28.0 - - timm==0.6.13 - - spacy==3.5.1 - - webdataset==0.2.48 - - scikit-learn==1.2.2 - - scipy==1.10.1 - - yarl==1.8.2 - - zipp==3.14.0 - - omegaconf==2.3.0 - - opencv-python==4.7.0.72 - - iopath==0.1.10 - - tenacity==8.2.2 - - peft - - pycocoevalcap - - sentence-transformers - - umap-learn - - notebook - - gradio==3.23 - - gradio-client==0.0.8 - - wandb - - llama-index==0.5.27 - - pymysql - - unstructured==0.6.3 - - pytesseract==0.3.10 - - markdown2 - - chromadb - - colorama - - playsound - - distro \ No newline at end of file From e50c04ead283aec0c8d242acd035f0b68a4ce7f7 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Thu, 18 May 2023 19:48:28 +0800 Subject: [PATCH 13/66] feature:chn splitter --- ...t_splitter.py => chn_document_splitter.py} | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) rename pilot/source_embedding/{chinese_text_splitter.py => chn_document_splitter.py} (76%) diff --git a/pilot/source_embedding/chinese_text_splitter.py b/pilot/source_embedding/chn_document_splitter.py similarity index 76% rename from pilot/source_embedding/chinese_text_splitter.py rename to pilot/source_embedding/chn_document_splitter.py index 091276af6..090a6af56 100644 --- a/pilot/source_embedding/chinese_text_splitter.py +++ b/pilot/source_embedding/chn_document_splitter.py @@ -1,30 +1,29 @@ -from langchain.text_splitter import CharacterTextSplitter import re from typing import List -# from configs.model_config import SENTENCE_SIZE +from langchain.text_splitter import CharacterTextSplitter -class ChineseTextSplitter(CharacterTextSplitter): +class CHNDocumentSplitter(CharacterTextSplitter): def __init__(self, pdf: bool = False, sentence_size: int = None, **kwargs): super().__init__(**kwargs) self.pdf = pdf self.sentence_size = sentence_size - def split_text1(self, text: str) -> List[str]: - if self.pdf: - text = re.sub(r"\n{3,}", "\n", text) - text = re.sub('\s', ' ', text) - text = text.replace("\n\n", "") - sent_sep_pattern = re.compile('([﹒﹔﹖﹗.。!?]["’”」』]{0,2}|(?=["‘“「『]{1,2}|$))') # del :; - sent_list = [] - for ele in sent_sep_pattern.split(text): - if sent_sep_pattern.match(ele) and sent_list: - sent_list[-1] += ele - elif ele: - sent_list.append(ele) - return sent_list + # def split_text_version2(self, text: str) -> List[str]: + # if self.pdf: + # text = re.sub(r"\n{3,}", "\n", text) + # text = re.sub('\s', ' ', text) + # text = text.replace("\n\n", "") + # sent_sep_pattern = re.compile('([﹒﹔﹖﹗.。!?]["’”」』]{0,2}|(?=["‘“「『]{1,2}|$))') # del :; + # sent_list = [] + # for ele in sent_sep_pattern.split(text): + # if sent_sep_pattern.match(ele) and sent_list: + # sent_list[-1] += ele + # elif ele: + # sent_list.append(ele) + # return sent_list - def split_text(self, text: str) -> List[str]: ##此处需要进一步优化逻辑 + def split_text(self, text: str) -> List[str]: if self.pdf: text = re.sub(r"\n{3,}", r"\n", text) text = re.sub('\s', " ", text) From e59c3834eb65c10e32ce2746859b1e190a467c5e Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Thu, 18 May 2023 19:52:59 +0800 Subject: [PATCH 14/66] update:merge --- .env.template | 19 +- .gitignore | 1 + README.en.md | 223 ----------------------- README.md | 264 +++++++++++++--------------- environment.yml | 68 ------- examples/embdserver.py | 9 +- pilot/__init__.py | 3 +- pilot/configs/config.py | 42 +++-- pilot/configs/model_config.py | 2 +- pilot/conversation.py | 16 +- pilot/model/loader.py | 32 ++-- pilot/model/vicuna_llm.py | 7 +- pilot/plugins.py | 2 +- pilot/prompts/auto_mode_prompt.py | 8 +- pilot/pturning/lora/finetune.py | 7 +- pilot/server/llmserver.py | 22 ++- pilot/server/webserver.py | 32 +++- pilot/vector_store/extract_tovec.py | 4 +- pilot/vector_store/file_loader.py | 14 +- requirements.txt | 3 + 20 files changed, 271 insertions(+), 507 deletions(-) delete mode 100644 README.en.md delete mode 100644 environment.yml diff --git a/.env.template b/.env.template index 5bf746eaa..d809a362b 100644 --- a/.env.template +++ b/.env.template @@ -17,6 +17,10 @@ #*******************************************************************# #** LLM MODELS **# #*******************************************************************# +LLM_MODEL=vicuna-13b +MODEL_SERVER=http://your_model_server_url +LIMIT_MODEL_CONCURRENCY=5 +MAX_POSITION_EMBEDDINGS=4096 ## SMART_LLM_MODEL - Smart language model (Default: vicuna-13b) ## FAST_LLM_MODEL - Fast language model (Default: chatglm-6b) @@ -36,10 +40,10 @@ #*******************************************************************# #** DATABASE SETTINGS **# #*******************************************************************# -DB_SETTINGS_MYSQL_USER=root -DB_SETTINGS_MYSQL_PASSWORD=password -DB_SETTINGS_MYSQL_HOST=localhost -DB_SETTINGS_MYSQL_PORT=3306 +LOCAL_DB_USER=root +LOCAL_DB_PASSWORD=aa12345678 +LOCAL_DB_HOST=127.0.0.1 +LOCAL_DB_PORT=3306 ### MILVUS @@ -55,6 +59,13 @@ DB_SETTINGS_MYSQL_PORT=3306 # MILVUS_SECURE= # MILVUS_COLLECTION=dbgpt +#*******************************************************************# +#** COMMANDS **# +#*******************************************************************# +EXECUTE_LOCAL_COMMANDS=False + + + #*******************************************************************# #** ALLOWLISTED PLUGINS **# #*******************************************************************# diff --git a/.gitignore b/.gitignore index c4c4a344e..cb21ee557 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__/ # C extensions *.so +.env .idea .vscode .idea diff --git a/README.en.md b/README.en.md deleted file mode 100644 index 5a8b1601b..000000000 --- a/README.en.md +++ /dev/null @@ -1,223 +0,0 @@ -# DB-GPT ![GitHub Repo stars](https://img.shields.io/github/stars/csunny/db-gpt?style=social) - ---- - -[中文版](README.md) - -[![Star History Chart](https://api.star-history.com/svg?repos=csunny/DB-GPT)](https://star-history.com/#csunny/DB-GPT) - -## What is DB-GPT? - -As large models are released and iterated upon, they are becoming increasingly intelligent. However, in the process of using large models, we face significant challenges in data security and privacy. We need to ensure that our sensitive data and environments remain completely controlled and avoid any data privacy leaks or security risks. Based on this, we have launched the DB-GPT project to build a complete private large model solution for all database-based scenarios. This solution supports local deployment, allowing it to be applied not only in independent private environments but also to be independently deployed and isolated according to business modules, ensuring that the ability of large models is absolutely private, secure, and controllable. - -DB-GPT is an experimental open-source project that uses localized GPT large models to interact with your data and environment. With this solution, you can be assured that there is no risk of data leakage, and your data is 100% private and secure. - -## Features - -Currently, we have released multiple key features, which are listed below to demonstrate our current capabilities: - -- SQL language capabilities - - SQL generation - - SQL diagnosis -- Private domain Q&A and data processing - - Database knowledge Q&A - - Data processing -- Plugins - - Support custom plugin execution tasks and natively support the Auto-GPT plugin, such as: - - Automatic execution of SQL and retrieval of query results - - Automatic crawling and learning of knowledge -- Unified vector storage/indexing of knowledge base - - Support for unstructured data such as PDF, Markdown, CSV, and WebURL - - -## Demo - -Run on an RTX 4090 GPU. [YouTube](https://www.youtube.com/watch?v=1PWI6F89LPo) - -### Run - -

- -

- -### SQL Generation - -1. Generate Create Table SQL - -

- -

- -2. Generating executable SQL:To generate executable SQL, first select the corresponding database and then the model can generate SQL based on the corresponding database schema information. The successful result of running it would be demonstrated as follows: -

- -

- -### Q&A - -

- -

- -1. Based on the default built-in knowledge base, question and answer. - -

- -

- -2. Add your own knowledge base. - -

- -

- -3. Learning from crawling data from the Internet - - - TODO - - -## Introduction -DB-GPT creates a vast model operating system using [FastChat](https://github.com/lm-sys/FastChat) and offers a large language model powered by [Vicuna](https://huggingface.co/Tribbiani/vicuna-7b). In addition, we provide private domain knowledge base question-answering capability through LangChain. Furthermore, we also provide support for additional plugins, and our design natively supports the Auto-GPT plugin. - -Is the architecture of the entire DB-GPT shown in the following figure: - -

- -

- -The core capabilities mainly consist of the following parts: -1. Knowledge base capability: Supports private domain knowledge base question-answering capability. -2. Large-scale model management capability: Provides a large model operating environment based on FastChat. -3. Unified data vector storage and indexing: Provides a uniform way to store and index various data types. -4. Connection module: Used to connect different modules and data sources to achieve data flow and interaction. -5. Agent and plugins: Provides Agent and plugin mechanisms, allowing users to customize and enhance the system's behavior. -6. Prompt generation and optimization: Automatically generates high-quality prompts and optimizes them to improve system response efficiency. -7. Multi-platform product interface: Supports various client products, such as web, mobile applications, and desktop applications. - -Below is a brief introduction to each module: - -### Knowledge base capability - -As the knowledge base is currently the most significant user demand scenario, we natively support the construction and processing of knowledge bases. At the same time, we also provide multiple knowledge base management strategies in this project, such as: -1. Default built-in knowledge base -2. Custom addition of knowledge bases -3. Various usage scenarios such as constructing knowledge bases through plugin capabilities and web crawling. Users only need to organize the knowledge documents, and they can use our existing capabilities to build the knowledge base required for the large model. - -### LLMs Management - -In the underlying large model integration, we have designed an open interface that supports integration with various large models. At the same time, we have a very strict control and evaluation mechanism for the effectiveness of the integrated models. In terms of accuracy, the integrated models need to align with the capability of ChatGPT at a level of 85% or higher. We use higher standards to select models, hoping to save users the cumbersome testing and evaluation process in the process of use. - -### Vector storage and indexing - -In order to facilitate the management of knowledge after vectorization, we have built-in multiple vector storage engines, from memory-based Chroma to distributed Milvus. Users can choose different storage engines according to their own scenario needs. The storage of knowledge vectors is the cornerstone of AI capability enhancement. As the intermediate language for interaction between humans and large language models, vectors play a very important role in this project. - -### Connections - -In order to interact more conveniently with users' private environments, the project has designed a connection module, which can support connection to databases, Excel, knowledge bases, and other environments to achieve information and data exchange. - -### Agent and Plugin - -The ability of Agent and Plugin is the core of whether large models can be automated. In this project, we natively support the plugin mode, and large models can automatically achieve their goals. At the same time, in order to give full play to the advantages of the community, the plugins used in this project natively support the Auto-GPT plugin ecology, that is, Auto-GPT plugins can directly run in our project. - -### Prompt Automatic Generation and Optimization - -Prompt is a very important part of the interaction between the large model and the user, and to a certain extent, it determines the quality and accuracy of the answer generated by the large model. In this project, we will automatically optimize the corresponding prompt according to user input and usage scenarios, making it easier and more efficient for users to use large language models. - -### Multi-Platform Product Interface - -TODO: In terms of terminal display, we will provide a multi-platform product interface, including PC, mobile phone, command line, Slack and other platforms. - -## Deployment - -### 1. Hardware Requirements -As our project has the ability to achieve ChatGPT performance of over 85%, there are certain hardware requirements. However, overall, the project can be deployed and used on consumer-grade graphics cards. The specific hardware requirements for deployment are as follows: - -| GPU | VRAM Size | Performance | -| --------- | --------- | ------------------------------------------- | -| RTX 4090 | 24 GB | Smooth conversation inference | -| RTX 3090 | 24 GB | Smooth conversation inference, better than V100 | -| V100 | 16 GB | Conversation inference possible, noticeable stutter | - -### 2. Install - -This project relies on a local MySQL database service, which you need to install locally. We recommend using Docker for installation. - -```bash -$ docker run --name=mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=aa12345678 -dit mysql:latest -``` -We use [Chroma embedding database](https://github.com/chroma-core/chroma) as the default for our vector database, so there is no need for special installation. If you choose to connect to other databases, you can follow our tutorial for installation and configuration. -For the entire installation process of DB-GPT, we use the miniconda3 virtual environment. Create a virtual environment and install the Python dependencies. -``` -python>=3.10 -conda create -n dbgpt_env python=3.10 -conda activate dbgpt_env -pip install -r requirements.txt -``` -Alternatively, you can use the following command: -``` -cd DB-GPT -conda env create -f environment.yml -``` -It is recommended to set the Python package path to avoid runtime errors due to package not found. -``` -echo "/root/workspace/DB-GPT" > /root/miniconda3/env/dbgpt_env/lib/python3.10/site-packages/dbgpt.pth -``` -Notice: You need replace the path to your owner. - -### 3. Run -You can refer to this document to obtain the Vicuna weights: [Vicuna](https://github.com/lm-sys/FastChat/blob/main/README.md#model-weights) . - -If you have difficulty with this step, you can also directly use the model from [this link](https://huggingface.co/Tribbiani/vicuna-7b) as a replacement. - -1. Run server -```bash -$ python pilot/server/llmserver.py -``` - -Run gradio webui - -```bash -$ python pilot/server/webserver.py -``` -Notice: the webserver need to connect llmserver, so you need change the pilot/configs/model_config.py file. change the VICUNA_MODEL_SERVER = "http://127.0.0.1:8000" to your address. It's very important. - -## Usage Instructions -We provide a user interface for Gradio, which allows you to use DB-GPT through our user interface. Additionally, we have prepared several reference articles (written in Chinese) that introduce the code and principles related to our project. -- [LLM Practical In Action Series (1) — Combined Langchain-Vicuna Application Practical](https://medium.com/@cfqcsunny/llm-practical-in-action-series-1-combined-langchain-vicuna-application-practical-701cd0413c9f) - -## Acknowledgement - -The achievements of this project are thanks to the technical community, especially the following projects: -- [FastChat](https://github.com/lm-sys/FastChat) for providing chat services -- [vicuna-13b](https://lmsys.org/blog/2023-03-30-vicuna/) as the base model -- [langchain](https://langchain.readthedocs.io/) tool chain -- [Auto-GPT](https://github.com/Significant-Gravitas/Auto-GPT) universal plugin template -- [Hugging Face](https://huggingface.co/) for big model management -- [Chroma](https://github.com/chroma-core/chroma) for vector storage -- [Milvus](https://milvus.io/) for distributed vector storage -- [ChatGLM](https://github.com/THUDM/ChatGLM-6B) as the base model -- [llama_index](https://github.com/jerryjliu/llama_index) for enhancing database-related knowledge using [in-context learning](https://arxiv.org/abs/2301.00234) based on existing knowledge bases. - - - -## Contributors - -|[
csunny](https://github.com/csunny)
|[
xudafeng](https://github.com/xudafeng)
|[
明天](https://github.com/yhjun1026)
| [
Aries-ckt](https://github.com/Aries-ckt)
|[
thebigbone](https://github.com/thebigbone)
| -| :---: | :---: | :---: | :---: |:---: | - - -This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Sun May 14 2023 23:02:43 GMT+0800`. - - - -## Licence - -The MIT License (MIT) - -## Contact Information -We are working on building a community, if you have any ideas about building the community, feel free to contact me us. - -name | email| ----------|--------------------- - yushun06| my_prophet@hotmail.com - csunny | cfqcsunny@gmail.com \ No newline at end of file diff --git a/README.md b/README.md index 3ef04884b..54a8d0de7 100644 --- a/README.md +++ b/README.md @@ -1,205 +1,192 @@ # DB-GPT ![GitHub Repo stars](https://img.shields.io/github/stars/csunny/db-gpt?style=social) -[English Edition](README.en.md) +--- + +[简体中文](README.zh.md) [![Star History Chart](https://api.star-history.com/svg?repos=csunny/DB-GPT)](https://star-history.com/#csunny/DB-GPT) -## DB-GPT 是什么? -随着大模型的发布迭代,大模型变得越来越智能,在使用大模型的过程当中,遇到极大的数据安全与隐私挑战。在利用大模型能力的过程中我们的私密数据跟环境需要掌握自己的手里,完全可控,避免任何的数据隐私泄露以及安全风险。基于此,我们发起了DB-GPT项目,为所有以数据库为基础的场景,构建一套完整的私有大模型解决方案。 此方案因为支持本地部署,所以不仅仅可以应用于独立私有环境,而且还可以根据业务模块独立部署隔离,让大模型的能力绝对私有、安全、可控。 +## What is DB-GPT? -DB-GPT 是一个开源的以数据库为基础的GPT实验项目,使用本地化的GPT大模型与您的数据和环境进行交互,无数据泄露风险,100% 私密,100% 安全。 +As large models are released and iterated upon, they are becoming increasingly intelligent. However, in the process of using large models, we face significant challenges in data security and privacy. We need to ensure that our sensitive data and environments remain completely controlled and avoid any data privacy leaks or security risks. Based on this, we have launched the DB-GPT project to build a complete private large model solution for all database-based scenarios. This solution supports local deployment, allowing it to be applied not only in independent private environments but also to be independently deployed and isolated according to business modules, ensuring that the ability of large models is absolutely private, secure, and controllable. + +DB-GPT is an experimental open-source project that uses localized GPT large models to interact with your data and environment. With this solution, you can be assured that there is no risk of data leakage, and your data is 100% private and secure. + +## Features + +Currently, we have released multiple key features, which are listed below to demonstrate our current capabilities: + +- SQL language capabilities + - SQL generation + - SQL diagnosis +- Private domain Q&A and data processing + - Database knowledge Q&A + - Data processing +- Plugins + - Support custom plugin execution tasks and natively support the Auto-GPT plugin, such as: + - Automatic execution of SQL and retrieval of query results + - Automatic crawling and learning of knowledge +- Unified vector storage/indexing of knowledge base + - Support for unstructured data such as PDF, Markdown, CSV, and WebURL -## 特性一览 +## Demo -目前我们已经发布了多种关键的特性,这里一一列举展示一下当前发布的能力。 -- SQL 语言能力 - - SQL生成 - - SQL诊断 -- 私域问答与数据处理 - - 数据库知识问答 - - 数据处理 -- 插件模型 - - 支持自定义插件执行任务,原生支持Auto-GPT插件。如: - - SQL自动执行,获取查询结果 - - 自动爬取学习知识 -- 知识库统一向量存储/索引 - - 非结构化数据支持包括PDF、MarkDown、CSV、WebURL +Run on an RTX 4090 GPU. [YouTube](https://www.youtube.com/watch?v=1PWI6F89LPo) -## 效果演示 - -示例通过 RTX 4090 GPU 演示,[YouTube 地址](https://www.youtube.com/watch?v=1PWI6F89LPo) -### 运行环境演示 +### Run

- +

+### SQL Generation + +1. Generate Create Table SQL +

- +

-### SQL 生成 +2. Generating executable SQL:To generate executable SQL, first select the corresponding database and then the model can generate SQL based on the corresponding database schema information. The successful result of running it would be demonstrated as follows: +

+ +

-1. 生成建表语句 +### Q&A

- +

-2. 生成可运行SQL -首先选择对应的数据库, 然后模型即可根据对应的数据库 Schema 信息生成 SQL, 运行成功的效果如下面的演示: +1. Based on the default built-in knowledge base, question and answer.

- +

-3. 自动分析执行SQL输出运行结果 +2. Add your own knowledge base.

- +

-### 数据库问答 +3. Learning from crawling data from the Internet -

- -

+ - TODO -1. 基于默认内置知识库问答 +## Introduction +DB-GPT creates a vast model operating system using [FastChat](https://github.com/lm-sys/FastChat) and offers a large language model powered by [Vicuna](https://huggingface.co/Tribbiani/vicuna-7b). In addition, we provide private domain knowledge base question-answering capability through LangChain. Furthermore, we also provide support for additional plugins, and our design natively supports the Auto-GPT plugin. -

- -

- -2. 自己新增知识库 - -

- -

- -3. 从网络自己爬取数据学习 -- TODO - -## 架构方案 -DB-GPT基于 [FastChat](https://github.com/lm-sys/FastChat) 构建大模型运行环境,并提供 vicuna 作为基础的大语言模型。此外,我们通过LangChain提供私域知识库问答能力。同时我们支持插件模式, 在设计上原生支持Auto-GPT插件。 - -整个DB-GPT的架构,如下图所示 +Is the architecture of the entire DB-GPT shown in the following figure:

-核心能力主要有以下几个部分。 -1. 知识库能力:支持私域知识库问答能力 -2. 大模型管理能力:基于FastChat提供一个大模型的运营环境。 -3. 统一的数据向量化存储与索引:提供一种统一的方式来存储和索引各种数据类型。 -4. 连接模块:用于连接不同的模块和数据源,实现数据的流转和交互。 -5. Agent与插件:提供Agent和插件机制,使得用户可以自定义并增强系统的行为。 -6. Prompt自动生成与优化:自动化生成高质量的Prompt,并进行优化,提高系统的响应效率。 -7. 多端产品界面:支持多种不同的客户端产品,例如Web、移动应用和桌面应用等。 +The core capabilities mainly consist of the following parts: +1. Knowledge base capability: Supports private domain knowledge base question-answering capability. +2. Large-scale model management capability: Provides a large model operating environment based on FastChat. +3. Unified data vector storage and indexing: Provides a uniform way to store and index various data types. +4. Connection module: Used to connect different modules and data sources to achieve data flow and interaction. +5. Agent and plugins: Provides Agent and plugin mechanisms, allowing users to customize and enhance the system's behavior. +6. Prompt generation and optimization: Automatically generates high-quality prompts and optimizes them to improve system response efficiency. +7. Multi-platform product interface: Supports various client products, such as web, mobile applications, and desktop applications. -下面对每个模块也做一些简要的介绍: +Below is a brief introduction to each module: -### 知识库能力 -知识库作为当前用户需求最大的场景,我们原生支持知识库的构建与处理。同时在本项目当中,也提供了多种知识库的管理策略。 如: -1. 默认内置知识库 -2. 自定义新增知识库 -3. 通过插件能力自抓取构建知识库等多种使用场景。 - -用户只需要整理好知识文档,即可用我们现有的能力构建大模型所需要的知识库能力。 +### Knowledge base capability -### 大模型管理能力 -在底层大模型接入中,设计了开放的接口,支持对接多种大模型。同时对于接入模型的效果,我们有非常严格的把控与评审机制。对大模型能力上与ChatGPT对比,在准确率上需要满足85%以上的能力对齐。我们用更高的标准筛选模型,是期望在用户使用过程中,可以省去前面繁琐的测试评估环节。 +As the knowledge base is currently the most significant user demand scenario, we natively support the construction and processing of knowledge bases. At the same time, we also provide multiple knowledge base management strategies in this project, such as: +1. Default built-in knowledge base +2. Custom addition of knowledge bases +3. Various usage scenarios such as constructing knowledge bases through plugin capabilities and web crawling. Users only need to organize the knowledge documents, and they can use our existing capabilities to build the knowledge base required for the large model. -### 统一的数据向量化存储与索引 -为了方便对知识向量化之后的管理,我们内置了多种向量存储引擎,从基于内存的Chroma到分布式的Milvus, 可以根据自己的场景需求,选择不同的存储引擎,整个知识向量存储是AI能力增强的基石,向量作为人与大语言模型交互的中间语言,在本项目中的作用非常重要。 +### LLMs Management -### 连接模块 -为了能够更方便的与用户的私有环境进行交互,项目设计了连接模块,连接模块可以支持连接到数据库、Excel、知识库等等多种环境当中,实现信息与数据交互。 +In the underlying large model integration, we have designed an open interface that supports integration with various large models. At the same time, we have a very strict control and evaluation mechanism for the effectiveness of the integrated models. In terms of accuracy, the integrated models need to align with the capability of ChatGPT at a level of 85% or higher. We use higher standards to select models, hoping to save users the cumbersome testing and evaluation process in the process of use. -### Agent与插件 -Agent与插件能力是大模型能否自动化的核心,在本的项目中,原生支持插件模式,大模型可以自动化完成目标。 同时为了充分发挥社区的优势,本项目中所用的插件原生支持Auto-GPT插件生态,即Auto-GPT的插件可以直接在我们的项目中运行。 +### Vector storage and indexing -### Prompt自动生成与优化 -Prompt是与大模型交互过程中非常重要的部分,一定程度上Prompt决定了大模型生成答案的质量与准确性,在本的项目中,我们会根据用户输入与使用场景,自动优化对应的Prompt,让用户使用大语言模型变得更简单、更高效。 +In order to facilitate the management of knowledge after vectorization, we have built-in multiple vector storage engines, from memory-based Chroma to distributed Milvus. Users can choose different storage engines according to their own scenario needs. The storage of knowledge vectors is the cornerstone of AI capability enhancement. As the intermediate language for interaction between humans and large language models, vectors play a very important role in this project. -### 多端产品界面 -TODO: 在终端展示上,我们将提供多端产品界面。包括PC、手机、命令行、Slack等多种模式。 +### Connections +In order to interact more conveniently with users' private environments, the project has designed a connection module, which can support connection to databases, Excel, knowledge bases, and other environments to achieve information and data exchange. -## 安装教程 -### 1.硬件说明 -因为我们的项目在效果上具备ChatGPT 85%以上的能力,因此对硬件有一定的要求。 但总体来说,我们在消费级的显卡上即可完成项目的部署使用,具体部署的硬件说明如下: -| GPU型号 | 显存大小 | 性能 | -| ------- | -------- | ------------------------------------------ | -| RTX4090 | 24G | 可以流畅的进行对话推理,无卡顿 | -| RTX3090 | 24G | 可以流畅进行对话推理,有卡顿感,但好于V100 | -| V100 | 16G | 可以进行对话推理,有明显卡顿 | -### 2.DB-GPT安装 +### Agent and Plugin -本项目依赖一个本地的 MySQL 数据库服务,你需要本地安装,推荐直接使用 Docker 安装。 +The ability of Agent and Plugin is the core of whether large models can be automated. In this project, we natively support the plugin mode, and large models can automatically achieve their goals. At the same time, in order to give full play to the advantages of the community, the plugins used in this project natively support the Auto-GPT plugin ecology, that is, Auto-GPT plugins can directly run in our project. + +### Prompt Automatic Generation and Optimization + +Prompt is a very important part of the interaction between the large model and the user, and to a certain extent, it determines the quality and accuracy of the answer generated by the large model. In this project, we will automatically optimize the corresponding prompt according to user input and usage scenarios, making it easier and more efficient for users to use large language models. + +### Multi-Platform Product Interface + +TODO: In terms of terminal display, we will provide a multi-platform product interface, including PC, mobile phone, command line, Slack and other platforms. + +## Deployment + +### 1. Hardware Requirements +As our project has the ability to achieve ChatGPT performance of over 85%, there are certain hardware requirements. However, overall, the project can be deployed and used on consumer-grade graphics cards. The specific hardware requirements for deployment are as follows: + +| GPU | VRAM Size | Performance | +| --------- | --------- | ------------------------------------------- | +| RTX 4090 | 24 GB | Smooth conversation inference | +| RTX 3090 | 24 GB | Smooth conversation inference, better than V100 | +| V100 | 16 GB | Conversation inference possible, noticeable stutter | + +### 2. Install + +This project relies on a local MySQL database service, which you need to install locally. We recommend using Docker for installation. + +```bash +$ docker run --name=mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=aa12345678 -dit mysql:latest ``` -docker run --name=mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=aa12345678 -dit mysql:latest -``` -向量数据库我们默认使用的是Chroma内存数据库,所以无需特殊安装,如果有需要连接其他的同学,可以按照我们的教程进行安装配置。整个DB-GPT的安装过程,我们使用的是miniconda3的虚拟环境。创建虚拟环境,并安装python依赖包 - +We use [Chroma embedding database](https://github.com/chroma-core/chroma) as the default for our vector database, so there is no need for special installation. If you choose to connect to other databases, you can follow our tutorial for installation and configuration. +For the entire installation process of DB-GPT, we use the miniconda3 virtual environment. Create a virtual environment and install the Python dependencies. ``` python>=3.10 conda create -n dbgpt_env python=3.10 conda activate dbgpt_env pip install -r requirements.txt - -``` -或者也可以使用命令: -``` -cd DB-GPT -conda env create -f environment.yml -``` -另外需要设置一下python包路径, 避免出现运行时找不到包 -``` -echo "/root/workspace/DB-GPT" > /root/miniconda3/env/dbgpt_env/lib/python3.10/site-packages/dbgpt.pth ``` -### 3. 运行大模型 +### 3. Run +You can refer to this document to obtain the Vicuna weights: [Vicuna](https://github.com/lm-sys/FastChat/blob/main/README.md#model-weights) . -关于基础模型, 可以根据[Vicuna](https://github.com/lm-sys/FastChat/blob/main/README.md#model-weights)合成教程进行合成。 -如果此步有困难的同学,也可以直接使用[此链接](https://huggingface.co/Tribbiani/vicuna-7b)上的模型进行替代。 +If you have difficulty with this step, you can also directly use the model from [this link](https://huggingface.co/Tribbiani/vicuna-7b) as a replacement. - 运行模型服务 -``` -cd pilot/server -python llmserver.py +1. Run server +```bash +$ python pilot/server/llmserver.py ``` -运行 gradio webui +Run gradio webui ```bash -$ python webserver.py +$ python pilot/server/webserver.py ``` -注意: 在启动Webserver之前, 需要修改pilot/configs/model_config.py 文件中的VICUNA_MODEL_SERVER = "http://127.0.0.1:8000", 将地址设置为你的服务器地址。 +Notice: the webserver need to connect llmserver, so you need change the .env file. change the MODEL_SERVER = "http://127.0.0.1:8000" to your address. It's very important. -## 使用说明 +## Usage Instructions +We provide a user interface for Gradio, which allows you to use DB-GPT through our user interface. Additionally, we have prepared several reference articles (written in Chinese) that introduce the code and principles related to our project. +- [LLM Practical In Action Series (1) — Combined Langchain-Vicuna Application Practical](https://medium.com/@cfqcsunny/llm-practical-in-action-series-1-combined-langchain-vicuna-application-practical-701cd0413c9f) -我们提供了Gradio的用户界面,可以通过我们的用户界面使用DB-GPT, 同时关于我们项目相关的一些代码跟原理介绍,我们也准备了以下几篇参考文章。 -1. [大模型实战系列(1) —— 强强联合Langchain-Vicuna应用实战](https://zhuanlan.zhihu.com/p/628750042) -2. [大模型实战系列(2) —— DB-GPT 阿里云部署指南](https://zhuanlan.zhihu.com/p/629467580) -3. [大模型实战系列(3) —— DB-GPT插件模型原理与使用](https://zhuanlan.zhihu.com/p/629623125) +## Acknowledgement -## 感谢 - -项目取得的成果,需要感谢技术社区,尤其以下项目。 - -- [FastChat](https://github.com/lm-sys/FastChat) 提供 chat 服务 -- [vicuna-13b](https://huggingface.co/Tribbiani/vicuna-13b) 作为基础模型 -- [langchain](https://github.com/hwchase17/langchain) 工具链 -- [Auto-GPT](https://github.com/Significant-Gravitas/Auto-GPT) 通用的插件模版 -- [Hugging Face](https://huggingface.co/) 大模型管理 -- [Chroma](https://github.com/chroma-core/chroma) 向量存储 -- [Milvus](https://milvus.io/) 分布式向量存储 -- [ChatGLM](https://github.com/THUDM/ChatGLM-6B) 基础模型 -- [llama-index](https://github.com/jerryjliu/llama_index) 基于现有知识库进行[In-Context Learning](https://arxiv.org/abs/2301.00234)来对其进行数据库相关知识的增强。 +The achievements of this project are thanks to the technical community, especially the following projects: +- [FastChat](https://github.com/lm-sys/FastChat) for providing chat services +- [vicuna-13b](https://lmsys.org/blog/2023-03-30-vicuna/) as the base model +- [langchain](https://langchain.readthedocs.io/) tool chain +- [Auto-GPT](https://github.com/Significant-Gravitas/Auto-GPT) universal plugin template +- [Hugging Face](https://huggingface.co/) for big model management +- [Chroma](https://github.com/chroma-core/chroma) for vector storage +- [Milvus](https://milvus.io/) for distributed vector storage +- [ChatGLM](https://github.com/THUDM/ChatGLM-6B) as the base model +- [llama_index](https://github.com/jerryjliu/llama_index) for enhancing database-related knowledge using [in-context learning](https://arxiv.org/abs/2301.00234) based on existing knowledge bases. @@ -213,12 +200,9 @@ This project follows the git-contributor [spec](https://github.com/xudafeng/git- -这是一个用于数据库的复杂且创新的工具, 我们的项目也在紧急的开发当中, 会陆续发布一些新的feature。如在使用当中有任何具体问题, 优先在项目下提issue, 如有需要, 请联系如下微信,我会尽力提供帮助,同时也非常欢迎大家参与到项目建设中。 - -

- -

- ## Licence The MIT License (MIT) + +## Contact Information +We are working on building a community, if you have any ideas about building the community, feel free to contact us. [Discord](https://discord.com/invite/twmZk3vv) diff --git a/environment.yml b/environment.yml deleted file mode 100644 index 99959ec5f..000000000 --- a/environment.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: db_pgt -channels: - - pytorch - - defaults - - anaconda -dependencies: - - python=3.10 - - cudatoolkit - - pip - - pytorch-mutex=1.0=cuda - - pip: - - pytorch - - accelerate==0.16.0 - - aiohttp==3.8.4 - - aiosignal==1.3.1 - - async-timeout==4.0.2 - - attrs==22.2.0 - - bitsandbytes==0.37.0 - - cchardet==2.1.7 - - chardet==5.1.0 - - contourpy==1.0.7 - - cycler==0.11.0 - - filelock==3.9.0 - - fonttools==4.38.0 - - frozenlist==1.3.3 - - huggingface-hub==0.13.4 - - importlib-resources==5.12.0 - - kiwisolver==1.4.4 - - matplotlib==3.7.0 - - multidict==6.0.4 - - packaging==23.0 - - psutil==5.9.4 - - pycocotools==2.0.6 - - pyparsing==3.0.9 - - python-dateutil==2.8.2 - - pyyaml==6.0 - - regex==2022.10.31 - - tokenizers==0.13.2 - - tqdm==4.64.1 - - transformers==4.28.0 - - timm==0.6.13 - - spacy==3.5.1 - - webdataset==0.2.48 - - scikit-learn==1.2.2 - - scipy==1.10.1 - - yarl==1.8.2 - - zipp==3.14.0 - - omegaconf==2.3.0 - - opencv-python==4.7.0.72 - - iopath==0.1.10 - - tenacity==8.2.2 - - peft - - pycocoevalcap - - sentence-transformers - - umap-learn - - notebook - - gradio==3.23 - - gradio-client==0.0.8 - - wandb - - llama-index==0.5.27 - - pymysql - - unstructured==0.6.3 - - pytesseract==0.3.10 - - markdown2 - - chromadb - - colorama - - playsound - - distro \ No newline at end of file diff --git a/examples/embdserver.py b/examples/embdserver.py index 6599a18ad..79140ba66 100644 --- a/examples/embdserver.py +++ b/examples/embdserver.py @@ -7,12 +7,15 @@ import time import uuid from urllib.parse import urljoin import gradio as gr -from pilot.configs.model_config import * +from pilot.configs.config import Config from pilot.conversation import conv_qa_prompt_template, conv_templates from langchain.prompts import PromptTemplate + vicuna_stream_path = "generate_stream" +CFG = Config() + def generate(query): template_name = "conv_one_shot" @@ -41,7 +44,7 @@ def generate(query): } response = requests.post( - url=urljoin(VICUNA_MODEL_SERVER, vicuna_stream_path), data=json.dumps(params) + url=urljoin(CFG.MODEL_SERVER, vicuna_stream_path), data=json.dumps(params) ) skip_echo_len = len(params["prompt"]) + 1 - params["prompt"].count("") * 3 @@ -54,7 +57,7 @@ def generate(query): yield(output) if __name__ == "__main__": - print(LLM_MODEL) + print(CFG.LLM_MODEL) with gr.Blocks() as demo: gr.Markdown("数据库SQL生成助手") with gr.Tab("SQL生成"): diff --git a/pilot/__init__.py b/pilot/__init__.py index a1531040e..b1d1cd3d2 100644 --- a/pilot/__init__.py +++ b/pilot/__init__.py @@ -1,7 +1,6 @@ from pilot.source_embedding import (SourceEmbedding, register) - __all__ = [ "SourceEmbedding", "register" -] \ No newline at end of file +] diff --git a/pilot/configs/config.py b/pilot/configs/config.py index 5749a752d..9023bc061 100644 --- a/pilot/configs/config.py +++ b/pilot/configs/config.py @@ -2,24 +2,23 @@ # -*- coding: utf-8 -*- import os +import nltk from typing import List from auto_gpt_plugin_template import AutoGPTPluginTemplate from pilot.singleton import Singleton + class Config(metaclass=Singleton): """Configuration class to store the state of bools for different scripts access""" def __init__(self) -> None: """Initialize the Config class""" - # TODO change model_config there - self.debug_mode = False self.skip_reprompt = False - self.temperature = float(os.getenv("TEMPERATURE", 0.7)) - # TODO change model_config there + self.execute_local_commands = ( os.getenv("EXECUTE_LOCAL_COMMANDS", "False") == "True" ) @@ -46,17 +45,12 @@ class Config(metaclass=Singleton): self.milvus_collection = os.getenv("MILVUS_COLLECTION", "dbgpt") self.milvus_secure = os.getenv("MILVUS_SECURE") == "True" + self.authorise_key = os.getenv("AUTHORISE_COMMAND_KEY", "y") self.exit_key = os.getenv("EXIT_KEY", "n") - self.image_provider = bool(os.getenv("IMAGE_PROVIDER", True)) + self.image_provider = os.getenv("IMAGE_PROVIDER", True) self.image_size = int(os.getenv("IMAGE_SIZE", 256)) - self.plugins_dir = os.getenv("PLUGINS_DIR", "../../plugins") - self.plugins: List[AutoGPTPluginTemplate] = [] - self.plugins_openai = [] - - self.command_registry = [] - self.huggingface_api_token = os.getenv("HUGGINGFACE_API_TOKEN") self.image_provider = os.getenv("IMAGE_PROVIDER") self.image_size = int(os.getenv("IMAGE_SIZE", 256)) @@ -68,6 +62,10 @@ class Config(metaclass=Singleton): ) self.speak_mode = False + + ### Related configuration of built-in commands + self.command_registry = [] + disabled_command_categories = os.getenv("DISABLED_COMMAND_CATEGORIES") if disabled_command_categories: self.disabled_command_categories = disabled_command_categories.split(",") @@ -78,6 +76,12 @@ class Config(metaclass=Singleton): os.getenv("EXECUTE_LOCAL_COMMANDS", "False") == "True" ) + + ### The associated configuration parameters of the plug-in control the loading and use of the plug-in + self.plugins_dir = os.getenv("PLUGINS_DIR", "../../plugins") + self.plugins: List[AutoGPTPluginTemplate] = [] + self.plugins_openai = [] + plugins_allowlist = os.getenv("ALLOWLISTED_PLUGINS") if plugins_allowlist: self.plugins_allowlist = plugins_allowlist.split(",") @@ -89,7 +93,21 @@ class Config(metaclass=Singleton): self.plugins_denylist = plugins_denylist.split(",") else: self.plugins_denylist = [] - + + + ### Local database connection configuration + self.LOCAL_DB_HOST = os.getenv("LOCAL_DB_HOST", "127.0.0.1") + self.LOCAL_DB_PORT = int(os.getenv("LOCAL_DB_PORT", 3306)) + self.LOCAL_DB_USER = os.getenv("LOCAL_DB_USER", "root") + self.LOCAL_DB_PASSWORD = os.getenv("LOCAL_DB_PASSWORD", "aa123456") + + ### LLM Model Service Configuration + self.LLM_MODEL = os.getenv("LLM_MODEL", "vicuna-13b") + self.LIMIT_MODEL_CONCURRENCY = int(os.getenv("LIMIT_MODEL_CONCURRENCY", 5)) + self.MAX_POSITION_EMBEDDINGS = int(os.getenv("MAX_POSITION_EMBEDDINGS", 4096)) + self.MODEL_SERVER = os.getenv("MODEL_SERVER", "http://121.41.167.183:8000") + self.ISLOAD_8BIT = os.getenv("ISLOAD_8BIT", "True") == "True" + def set_debug_mode(self, value: bool) -> None: """Set the debug mode value""" self.debug_mode = value diff --git a/pilot/configs/model_config.py b/pilot/configs/model_config.py index 4781cd08a..faa93227f 100644 --- a/pilot/configs/model_config.py +++ b/pilot/configs/model_config.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python3 # -*- coding:utf-8 -*- import torch diff --git a/pilot/conversation.py b/pilot/conversation.py index 073f25f24..7054fb453 100644 --- a/pilot/conversation.py +++ b/pilot/conversation.py @@ -4,8 +4,16 @@ import dataclasses from enum import auto, Enum from typing import List, Any -from pilot.configs.model_config import DB_SETTINGS +from pilot.configs.config import Config +CFG = Config() + +DB_SETTINGS = { + "user": CFG.LOCAL_DB_USER, + "password": CFG.LOCAL_DB_PASSWORD, + "host": CFG.LOCAL_DB_HOST, + "port": CFG.LOCAL_DB_PORT +} class SeparatorStyle(Enum): SINGLE = auto() @@ -91,7 +99,7 @@ class Conversation: def gen_sqlgen_conversation(dbname): from pilot.connections.mysql import MySQLOperator mo = MySQLOperator( - **DB_SETTINGS + **(DB_SETTINGS) ) message = "" @@ -99,7 +107,7 @@ def gen_sqlgen_conversation(dbname): schemas = mo.get_schema(dbname) for s in schemas: message += s["schema_info"] + ";" - return f"数据库{dbname}的Schema信息如下: {message}\n" + return f"Database {dbname} Schema information as follows: {message}\n" conv_one_shot = Conversation( @@ -162,7 +170,7 @@ auto_dbgpt_one_shot = Conversation( Schema: - 数据库gpt-user的Schema信息如下: users(city,create_time,email,last_login_time,phone,user_name); + Database gpt-user Schema information as follows: users(city,create_time,email,last_login_time,phone,user_name); Commands: diff --git a/pilot/model/loader.py b/pilot/model/loader.py index 3c0b9a6a7..66d9c733e 100644 --- a/pilot/model/loader.py +++ b/pilot/model/loader.py @@ -2,21 +2,19 @@ # -*- coding: utf-8 -*- import torch +import warnings from pilot.singleton import Singleton -from transformers import ( - AutoTokenizer, - AutoModelForCausalLM, - AutoModel -) - from pilot.model.compression import compress_module +from pilot.model.adapter import get_llm_model_adapter + class ModelLoader(metaclass=Singleton): """Model loader is a class for model load Args: model_path - + + TODO: multi model support. """ kwargs = {} @@ -31,9 +29,11 @@ class ModelLoader(metaclass=Singleton): "device_map": "auto", } + # TODO multi gpu support def loader(self, num_gpus, load_8bit=False, debug=False): if self.device == "cpu": kwargs = {} + elif self.device == "cuda": kwargs = {"torch_dtype": torch.float16} if num_gpus == "auto": @@ -46,18 +46,20 @@ class ModelLoader(metaclass=Singleton): "max_memory": {i: "13GiB" for i in range(num_gpus)}, }) else: + # Todo Support mps for practise raise ValueError(f"Invalid device: {self.device}") - if "chatglm" in self.model_path: - tokenizer = AutoTokenizer.from_pretrained(self.model_path, trust_remote_code=True) - model = AutoModel.from_pretrained(self.model_path, trust_remote_code=True).half().cuda() - else: - tokenizer = AutoTokenizer.from_pretrained(self.model_path, use_fast=False) - model = AutoModelForCausalLM.from_pretrained(self.model_path, - low_cpu_mem_usage=True, **kwargs) + + llm_adapter = get_llm_model_adapter(self.model_path) + model, tokenizer = llm_adapter.loader(self.model_path, kwargs) if load_8bit: - compress_module(model, self.device) + if num_gpus != 1: + warnings.warn( + "8-bit quantization is not supported for multi-gpu inference" + ) + else: + compress_module(model, self.device) if (self.device == "cuda" and num_gpus == 1): model.to(self.device) diff --git a/pilot/model/vicuna_llm.py b/pilot/model/vicuna_llm.py index 2337a3bbf..63788a619 100644 --- a/pilot/model/vicuna_llm.py +++ b/pilot/model/vicuna_llm.py @@ -8,8 +8,9 @@ from langchain.embeddings.base import Embeddings from pydantic import BaseModel from typing import Any, Mapping, Optional, List from langchain.llms.base import LLM -from pilot.configs.model_config import * +from pilot.configs.config import Config +CFG = Config() class VicunaLLM(LLM): vicuna_generate_path = "generate_stream" @@ -22,7 +23,7 @@ class VicunaLLM(LLM): "stop": stop } response = requests.post( - url=urljoin(VICUNA_MODEL_SERVER, self.vicuna_generate_path), + url=urljoin(CFG.MODEL_SERVER, self.vicuna_generate_path), data=json.dumps(params), ) @@ -51,7 +52,7 @@ class VicunaEmbeddingLLM(BaseModel, Embeddings): print("Sending prompt ", p) response = requests.post( - url=urljoin(VICUNA_MODEL_SERVER, self.vicuna_embedding_path), + url=urljoin(CFG.MODEL_SERVER, self.vicuna_embedding_path), json={ "prompt": p } diff --git a/pilot/plugins.py b/pilot/plugins.py index 196a68b22..28f33a5a4 100644 --- a/pilot/plugins.py +++ b/pilot/plugins.py @@ -17,7 +17,7 @@ from pilot.logs import logger def inspect_zip_for_modules(zip_path: str, debug: bool = False) -> list[str]: """ - 加载zip文件的插件,完全兼容Auto_gpt_plugin + Loader zip plugin file. Native support Auto_gpt_plugin Args: zip_path (str): Path to the zipfile. diff --git a/pilot/prompts/auto_mode_prompt.py b/pilot/prompts/auto_mode_prompt.py index ec5918582..86f707783 100644 --- a/pilot/prompts/auto_mode_prompt.py +++ b/pilot/prompts/auto_mode_prompt.py @@ -42,7 +42,7 @@ class AutoModePrompt: prompt_generator: Optional[PromptGenerator] = None )-> str: """ - 基于用户输入的后续对话信息构建完整的prompt信息 + Build complete prompt information based on subsequent dialogue information entered by the user Args: self: prompt_generator: @@ -69,7 +69,7 @@ class AutoModePrompt: if not self.ai_goals : self.ai_goals = user_input for i, goal in enumerate(self.ai_goals): - full_prompt += f"{i+1}.根据提供的Schema信息, {goal}\n" + full_prompt += f"{i+1}.According to the provided Schema information, {goal}\n" # if last_auto_return == None: # full_prompt += f"{cfg.last_plugin_return}\n\n" # else: @@ -88,7 +88,7 @@ class AutoModePrompt: prompt_generator: Optional[PromptGenerator] = None ) -> str: """ - 基于用户输入的初始对话信息构建完整的prompt信息 + Build complete prompt information based on the initial dialogue information entered by the user Args: self: prompt_generator: @@ -128,7 +128,7 @@ class AutoModePrompt: if not self.ai_goals : self.ai_goals = fisrt_message for i, goal in enumerate(self.ai_goals): - full_prompt += f"{i+1}.根据提供的Schema信息,{goal}\n" + full_prompt += f"{i+1}.According to the provided Schema information,{goal}\n" if db_schemes: full_prompt += f"\nSchema:\n\n" full_prompt += f"{db_schemes}" diff --git a/pilot/pturning/lora/finetune.py b/pilot/pturning/lora/finetune.py index 6cd9935ed..91ec07d0a 100644 --- a/pilot/pturning/lora/finetune.py +++ b/pilot/pturning/lora/finetune.py @@ -17,14 +17,17 @@ from peft import ( import torch from datasets import load_dataset import pandas as pd +from pilot.configs.config import Config -from pilot.configs.model_config import DATA_DIR, LLM_MODEL, LLM_MODEL_CONFIG +from pilot.configs.model_config import DATA_DIR, LLM_MODEL_CONFIG device = "cuda" if torch.cuda.is_available() else "cpu" CUTOFF_LEN = 50 df = pd.read_csv(os.path.join(DATA_DIR, "BTC_Tweets_Updated.csv")) +CFG = Config() + def sentiment_score_to_name(score: float): if score > 0: return "Positive" @@ -49,7 +52,7 @@ with open(os.path.join(DATA_DIR, "alpaca-bitcoin-sentiment-dataset.json"), "w") data = load_dataset("json", data_files=os.path.join(DATA_DIR, "alpaca-bitcoin-sentiment-dataset.json")) print(data["train"]) -BASE_MODEL = LLM_MODEL_CONFIG[LLM_MODEL] +BASE_MODEL = LLM_MODEL_CONFIG[CFG.LLM_MODEL] model = LlamaForCausalLM.from_pretrained( BASE_MODEL, torch_dtype=torch.float16, diff --git a/pilot/server/llmserver.py b/pilot/server/llmserver.py index 2860c3b77..e341cc457 100644 --- a/pilot/server/llmserver.py +++ b/pilot/server/llmserver.py @@ -1,24 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import os import uvicorn import asyncio import json +import sys from typing import Optional, List from fastapi import FastAPI, Request, BackgroundTasks from fastapi.responses import StreamingResponse -from pilot.model.inference import generate_stream from pydantic import BaseModel + +global_counter = 0 +model_semaphore = None + +ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(ROOT_PATH) + +from pilot.model.inference import generate_stream from pilot.model.inference import generate_output, get_embeddings from pilot.model.loader import ModelLoader from pilot.configs.model_config import * - -model_path = LLM_MODEL_CONFIG[LLM_MODEL] +from pilot.configs.config import Config -global_counter = 0 -model_semaphore = None +CFG = Config() +model_path = LLM_MODEL_CONFIG[CFG.LLM_MODEL] ml = ModelLoader(model_path=model_path) model, tokenizer = ml.loader(num_gpus=1, load_8bit=ISLOAD_8BIT, debug=ISDEBUG) @@ -60,7 +68,7 @@ def generate_stream_gate(params): tokenizer, params, DEVICE, - MAX_POSITION_EMBEDDINGS, + CFG.MAX_POSITION_EMBEDDINGS, ): print("output: ", output) ret = { @@ -84,7 +92,7 @@ async def api_generate_stream(request: Request): print(model, tokenizer, params, DEVICE) if model_semaphore is None: - model_semaphore = asyncio.Semaphore(LIMIT_MODEL_CONCURRENCY) + model_semaphore = asyncio.Semaphore(CFG.LIMIT_MODEL_CONCURRENCY) await model_semaphore.acquire() generator = generate_stream_gate(params) diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index c2a780244..289ce7f32 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -6,6 +6,7 @@ import os import shutil import uuid import json +import sys import time import gradio as gr import datetime @@ -14,13 +15,17 @@ from urllib.parse import urljoin from langchain import PromptTemplate -from pilot.configs.model_config import DB_SETTINGS, KNOWLEDGE_UPLOAD_ROOT_PATH, LLM_MODEL_CONFIG + +ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(ROOT_PATH) + +from pilot.configs.model_config import KNOWLEDGE_UPLOAD_ROOT_PATH, LLM_MODEL_CONFIG from pilot.server.vectordb_qa import KnownLedgeBaseQA from pilot.connections.mysql import MySQLOperator from pilot.source_embedding.knowledge_embedding import KnowledgeEmbedding from pilot.vector_store.extract_tovec import get_vector_storelist, load_knownledge_from_doc, knownledge_tovec_st -from pilot.configs.model_config import LOGDIR, VICUNA_MODEL_SERVER, LLM_MODEL, DATASETS_DIR +from pilot.configs.model_config import LOGDIR, DATASETS_DIR from pilot.plugins import scan_plugins from pilot.configs.config import Config @@ -30,6 +35,8 @@ from pilot.prompts.generator import PromptGenerator from pilot.commands.exception_not_commands import NotCommands + + from pilot.conversation import ( default_conversation, conv_templates, @@ -67,7 +74,15 @@ priority = { "vicuna-13b": "aaa" } +# 加载插件 +CFG= Config() +DB_SETTINGS = { + "user": CFG.LOCAL_DB_USER, + "password": CFG.LOCAL_DB_PASSWORD, + "host": CFG.LOCAL_DB_HOST, + "port": CFG.LOCAL_DB_PORT +} def get_simlar(q): docsearch = knownledge_tovec_st(os.path.join(DATASETS_DIR, "plan.md")) docs = docsearch.similarity_search_with_score(q, k=1) @@ -178,7 +193,7 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re print("是否是AUTO-GPT模式.", autogpt) start_tstamp = time.time() - model_name = LLM_MODEL + model_name = CFG.LLM_MODEL dbname = db_selector # TODO 这里的请求需要拼接现有知识库, 使得其根据现有知识库作答, 所以prompt需要继续优化 @@ -282,7 +297,7 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re logger.info(f"Requert: \n{payload}") if sql_mode == conversation_sql_mode["auto_execute_ai_response"]: - response = requests.post(urljoin(VICUNA_MODEL_SERVER, "generate"), + response = requests.post(urljoin(CFG.MODEL_SERVER, "generate"), headers=headers, json=payload, timeout=120) print(response.json()) @@ -330,7 +345,7 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re try: # Stream output - response = requests.post(urljoin(VICUNA_MODEL_SERVER, "generate_stream"), + response = requests.post(urljoin(CFG.MODEL_SERVER, "generate_stream"), headers=headers, json=payload, stream=True, timeout=20) for chunk in response.iter_lines(decode_unicode=False, delimiter=b"\0"): if chunk: @@ -606,12 +621,11 @@ if __name__ == "__main__": args = parser.parse_args() logger.info(f"args: {args}") - - # dbs = get_database_list() - - # 加载插件 + # 配置初始化 cfg = Config() + dbs = get_database_list() + cfg.set_plugins(scan_plugins(cfg, cfg.debug_mode)) # 加载插件可执行命令 diff --git a/pilot/vector_store/extract_tovec.py b/pilot/vector_store/extract_tovec.py index 8badf6fed..c6b83d467 100644 --- a/pilot/vector_store/extract_tovec.py +++ b/pilot/vector_store/extract_tovec.py @@ -40,8 +40,8 @@ def knownledge_tovec_st(filename): def load_knownledge_from_doc(): - """从数据集当中加载知识 - # TODO 如果向量存储已经存在, 则无需初始化 + """Loader Knownledge from current datasets + # TODO if the vector store is exists, just use it. """ if not os.path.exists(DATASETS_DIR): diff --git a/pilot/vector_store/file_loader.py b/pilot/vector_store/file_loader.py index 296232f21..8f668f60e 100644 --- a/pilot/vector_store/file_loader.py +++ b/pilot/vector_store/file_loader.py @@ -40,15 +40,15 @@ class KnownLedge2Vector: def init_vector_store(self): persist_dir = os.path.join(VECTORE_PATH, ".vectordb") - print("向量数据库持久化地址: ", persist_dir) + print("Vector store Persist address is: ", persist_dir) if os.path.exists(persist_dir): - # 从本地持久化文件中Load - print("从本地向量加载数据...") + # Loader from local file. + print("Loader data from local persist vector file...") vector_store = Chroma(persist_directory=persist_dir, embedding_function=self.embeddings) # vector_store.add_documents(documents=documents) else: documents = self.load_knownlege() - # 重新初始化 + # reinit vector_store = Chroma.from_documents(documents=documents, embedding=self.embeddings, persist_directory=persist_dir) @@ -61,17 +61,17 @@ class KnownLedge2Vector: for file in files: filename = os.path.join(root, file) docs = self._load_file(filename) - # 更新metadata数据 + # update metadata. new_docs = [] for doc in docs: doc.metadata = {"source": doc.metadata["source"].replace(DATASETS_DIR, "")} - print("文档2向量初始化中, 请稍等...", doc.metadata) + print("Documents to vector running, please wait...", doc.metadata) new_docs.append(doc) docments += new_docs return docments def _load_file(self, filename): - # 加载文件 + # Loader file if filename.lower().endswith(".pdf"): loader = UnstructuredFileLoader(filename) text_splitor = CharacterTextSplitter() diff --git a/requirements.txt b/requirements.txt index 3bca421f6..eac927c3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -57,6 +57,9 @@ pymdown-extensions mkdocs requests gTTS==2.3.1 +langchain +nltk +python-dotenv==1.0.0 # Testing dependencies pytest From 2656a8030ecf24c20ef582a302bb865a791a801a Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Thu, 18 May 2023 20:03:24 +0800 Subject: [PATCH 15/66] feature:knowledge embedding update --- .gitignore | 3 +- pilot/conversation.py | 7 ++ pilot/server/webserver.py | 13 +-- pilot/source_embedding/knowledge_embedding.py | 82 ++++++++++++++++++- pilot/source_embedding/markdown_embedding.py | 27 +++++- pilot/source_embedding/pdf_embedding.py | 4 +- pilot/source_embedding/source_embedding.py | 12 +++ pilot/source_embedding/url_embedding.py | 6 +- pilot/vector_store/file_loader.py | 6 +- 9 files changed, 143 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index cb21ee557..5043f7db0 100644 --- a/.gitignore +++ b/.gitignore @@ -136,4 +136,5 @@ dmypy.json .DS_Store logs nltk_data -.vectordb \ No newline at end of file +.vectordb +pilot/data/ \ No newline at end of file diff --git a/pilot/conversation.py b/pilot/conversation.py index 7054fb453..7f526fb89 100644 --- a/pilot/conversation.py +++ b/pilot/conversation.py @@ -247,6 +247,13 @@ conv_qa_prompt_template = """ 基于以下已知的信息, 专业、简要的回 {question} """ +# conv_qa_prompt_template = """ Please provide the known information so that I can professionally and briefly answer the user's question. If the answer cannot be obtained from the provided content, +# please say: "The information provided in the knowledge base is insufficient to answer this question." Fabrication is prohibited.。 +# known information: +# {context} +# question: +# {question} +# """ default_conversation = conv_one_shot conversation_sql_mode ={ diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index 289ce7f32..e6ba19160 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -19,7 +19,7 @@ from langchain import PromptTemplate ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(ROOT_PATH) -from pilot.configs.model_config import KNOWLEDGE_UPLOAD_ROOT_PATH, LLM_MODEL_CONFIG +from pilot.configs.model_config import DB_SETTINGS, KNOWLEDGE_UPLOAD_ROOT_PATH, LLM_MODEL_CONFIG, TOP_RETURN_SIZE from pilot.server.vectordb_qa import KnownLedgeBaseQA from pilot.connections.mysql import MySQLOperator from pilot.source_embedding.knowledge_embedding import KnowledgeEmbedding @@ -256,11 +256,13 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re if mode == conversation_types["custome"] and not db_selector: persist_dir = os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, vector_store_name["vs_name"] + ".vectordb") - print("向量数据库持久化地址: ", persist_dir) - knowledge_embedding_client = KnowledgeEmbedding(file_path="", model_name=LLM_MODEL_CONFIG["text2vec"], vector_store_config={"vector_store_name": vector_store_name["vs_name"], + print("vector store path: ", persist_dir) + knowledge_embedding_client = KnowledgeEmbedding(file_path="", model_name=LLM_MODEL_CONFIG["text2vec"], + local_persist=False, + vector_store_config={"vector_store_name": vector_store_name["vs_name"], "vector_store_path": KNOWLEDGE_UPLOAD_ROOT_PATH}) query = state.messages[-2][1] - docs = knowledge_embedding_client.similar_search(query, 10) + docs = knowledge_embedding_client.similar_search(query, TOP_RETURN_SIZE) context = [d.page_content for d in docs] prompt_template = PromptTemplate( template=conv_qa_prompt_template, @@ -600,6 +602,7 @@ def knowledge_embedding_store(vs_id, files): knowledge_embedding_client = KnowledgeEmbedding( file_path=os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, vs_id, filename), model_name=LLM_MODEL_CONFIG["text2vec"], + local_persist=False, vector_store_config={ "vector_store_name": vector_store_name["vs_name"], "vector_store_path": KNOWLEDGE_UPLOAD_ROOT_PATH}) @@ -624,7 +627,7 @@ if __name__ == "__main__": # 配置初始化 cfg = Config() - dbs = get_database_list() + # dbs = get_database_list() cfg.set_plugins(scan_plugins(cfg, cfg.debug_mode)) diff --git a/pilot/source_embedding/knowledge_embedding.py b/pilot/source_embedding/knowledge_embedding.py index a9e4d4e4e..594723b6e 100644 --- a/pilot/source_embedding/knowledge_embedding.py +++ b/pilot/source_embedding/knowledge_embedding.py @@ -1,20 +1,35 @@ +import os + +from bs4 import BeautifulSoup +from langchain.document_loaders import PyPDFLoader, TextLoader, markdown +from langchain.embeddings import HuggingFaceEmbeddings +from langchain.vectorstores import Chroma +from pilot.configs.model_config import DATASETS_DIR +from pilot.source_embedding.chn_document_splitter import CHNDocumentSplitter from pilot.source_embedding.csv_embedding import CSVEmbedding from pilot.source_embedding.markdown_embedding import MarkdownEmbedding from pilot.source_embedding.pdf_embedding import PDFEmbedding +import markdown class KnowledgeEmbedding: - def __init__(self, file_path, model_name, vector_store_config): + def __init__(self, file_path, model_name, vector_store_config, local_persist=True): """Initialize with Loader url, model_name, vector_store_config""" self.file_path = file_path self.model_name = model_name self.vector_store_config = vector_store_config self.vector_store_type = "default" - self.knowledge_embedding_client = self.init_knowledge_embedding() + self.embeddings = HuggingFaceEmbeddings(model_name=self.model_name) + self.local_persist = local_persist + if not self.local_persist: + self.knowledge_embedding_client = self.init_knowledge_embedding() def knowledge_embedding(self): self.knowledge_embedding_client.source_embedding() + def knowledge_embedding_batch(self): + self.knowledge_embedding_client.batch_embedding() + def init_knowledge_embedding(self): if self.file_path.endswith(".pdf"): embedding = PDFEmbedding(file_path=self.file_path, model_name=self.model_name, @@ -31,4 +46,65 @@ class KnowledgeEmbedding: return embedding def similar_search(self, text, topk): - return self.knowledge_embedding_client.similar_search(text, topk) \ No newline at end of file + return self.knowledge_embedding_client.similar_search(text, topk) + + def knowledge_persist_initialization(self, append_mode): + vector_name = self.vector_store_config["vector_store_name"] + persist_dir = os.path.join(self.vector_store_config["vector_store_path"], vector_name + ".vectordb") + print("vector db path: ", persist_dir) + if os.path.exists(persist_dir): + if append_mode: + print("append knowledge return vector store") + new_documents = self._load_knownlege(self.file_path) + vector_store = Chroma.from_documents(documents=new_documents, + embedding=self.embeddings, + persist_directory=persist_dir) + else: + print("directly return vector store") + vector_store = Chroma(persist_directory=persist_dir, embedding_function=self.embeddings) + else: + print(vector_name + "is new vector store, knowledge begin load...") + documents = self._load_knownlege(self.file_path) + vector_store = Chroma.from_documents(documents=documents, + embedding=self.embeddings, + persist_directory=persist_dir) + vector_store.persist() + return vector_store + + def _load_knownlege(self, path): + docments = [] + for root, _, files in os.walk(path, topdown=False): + for file in files: + filename = os.path.join(root, file) + docs = self._load_file(filename) + new_docs = [] + for doc in docs: + doc.metadata = {"source": doc.metadata["source"].replace(DATASETS_DIR, "")} + print("doc is embedding...", doc.metadata) + new_docs.append(doc) + docments += new_docs + return docments + + def _load_file(self, filename): + if filename.lower().endswith(".md"): + loader = TextLoader(filename) + text_splitter = CHNDocumentSplitter(pdf=True, sentence_size=100) + docs = loader.load_and_split(text_splitter) + i = 0 + for d in docs: + content = markdown.markdown(d.page_content) + soup = BeautifulSoup(content, 'html.parser') + for tag in soup(['!doctype', 'meta', 'i.fa']): + tag.extract() + docs[i].page_content = soup.get_text() + docs[i].page_content = docs[i].page_content.replace("\n", " ") + i += 1 + elif filename.lower().endswith(".pdf"): + loader = PyPDFLoader(filename) + textsplitter = CHNDocumentSplitter(pdf=True, sentence_size=100) + docs = loader.load_and_split(textsplitter) + else: + loader = TextLoader(filename) + text_splitor = CHNDocumentSplitter(sentence_size=100) + docs = loader.load_and_split(text_splitor) + return docs \ No newline at end of file diff --git a/pilot/source_embedding/markdown_embedding.py b/pilot/source_embedding/markdown_embedding.py index 622011006..fee9504b6 100644 --- a/pilot/source_embedding/markdown_embedding.py +++ b/pilot/source_embedding/markdown_embedding.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import os from typing import List from bs4 import BeautifulSoup @@ -8,6 +9,7 @@ from langchain.schema import Document import markdown from pilot.source_embedding import SourceEmbedding, register +from pilot.source_embedding.chn_document_splitter import CHNDocumentSplitter class MarkdownEmbedding(SourceEmbedding): @@ -24,7 +26,28 @@ class MarkdownEmbedding(SourceEmbedding): def read(self): """Load from markdown path.""" loader = TextLoader(self.file_path) - return loader.load() + text_splitter = CHNDocumentSplitter(pdf=True, sentence_size=100) + return loader.load_and_split(text_splitter) + + @register + def read_batch(self): + """Load from markdown path.""" + docments = [] + for root, _, files in os.walk(self.file_path, topdown=False): + for file in files: + filename = os.path.join(root, file) + loader = TextLoader(filename) + # text_splitor = CHNDocumentSplitter(chunk_size=1000, chunk_overlap=20, length_function=len) + # docs = loader.load_and_split() + docs = loader.load() + # 更新metadata数据 + new_docs = [] + for doc in docs: + doc.metadata = {"source": doc.metadata["source"].replace(self.file_path, "")} + print("doc is embedding ... ", doc.metadata) + new_docs.append(doc) + docments += new_docs + return docments @register def data_process(self, documents: List[Document]): @@ -35,7 +58,7 @@ class MarkdownEmbedding(SourceEmbedding): for tag in soup(['!doctype', 'meta', 'i.fa']): tag.extract() documents[i].page_content = soup.get_text() - documents[i].page_content = documents[i].page_content.replace(" ", "").replace("\n", " ") + documents[i].page_content = documents[i].page_content.replace("\n", " ") i += 1 return documents diff --git a/pilot/source_embedding/pdf_embedding.py b/pilot/source_embedding/pdf_embedding.py index 557637c5a..bd0ae3aba 100644 --- a/pilot/source_embedding/pdf_embedding.py +++ b/pilot/source_embedding/pdf_embedding.py @@ -6,7 +6,7 @@ from langchain.document_loaders import PyPDFLoader from langchain.schema import Document from pilot.source_embedding import SourceEmbedding, register -from pilot.source_embedding.chinese_text_splitter import ChineseTextSplitter +from pilot.source_embedding.chn_document_splitter import CHNDocumentSplitter class PDFEmbedding(SourceEmbedding): @@ -23,7 +23,7 @@ class PDFEmbedding(SourceEmbedding): def read(self): """Load from pdf path.""" loader = PyPDFLoader(self.file_path) - textsplitter = ChineseTextSplitter(pdf=True, sentence_size=100) + textsplitter = CHNDocumentSplitter(pdf=True, sentence_size=100) return loader.load_and_split(textsplitter) @register diff --git a/pilot/source_embedding/source_embedding.py b/pilot/source_embedding/source_embedding.py index 656d24eaf..66bc97b6d 100644 --- a/pilot/source_embedding/source_embedding.py +++ b/pilot/source_embedding/source_embedding.py @@ -76,3 +76,15 @@ class SourceEmbedding(ABC): self.text_to_vector(text) if 'index_to_store' in registered_methods: self.index_to_store(text) + + def batch_embedding(self): + if 'read_batch' in registered_methods: + text = self.read_batch() + if 'data_process' in registered_methods: + text = self.data_process(text) + if 'text_split' in registered_methods: + self.text_split(text) + if 'text_to_vector' in registered_methods: + self.text_to_vector(text) + if 'index_to_store' in registered_methods: + self.index_to_store(text) diff --git a/pilot/source_embedding/url_embedding.py b/pilot/source_embedding/url_embedding.py index 5fa29e0d2..68fbdd5e4 100644 --- a/pilot/source_embedding/url_embedding.py +++ b/pilot/source_embedding/url_embedding.py @@ -1,4 +1,7 @@ from typing import List + +from langchain.text_splitter import CharacterTextSplitter + from pilot.source_embedding import SourceEmbedding, register from bs4 import BeautifulSoup @@ -20,7 +23,8 @@ class URLEmbedding(SourceEmbedding): def read(self): """Load from url path.""" loader = WebBaseLoader(web_path=self.file_path) - return loader.load() + text_splitor = CharacterTextSplitter(chunk_size=1000, chunk_overlap=20, length_function=len) + return loader.load_and_split(text_splitor) @register def data_process(self, documents: List[Document]): diff --git a/pilot/vector_store/file_loader.py b/pilot/vector_store/file_loader.py index 8f668f60e..8703e2e4c 100644 --- a/pilot/vector_store/file_loader.py +++ b/pilot/vector_store/file_loader.py @@ -48,12 +48,12 @@ class KnownLedge2Vector: # vector_store.add_documents(documents=documents) else: documents = self.load_knownlege() - # reinit + # reinit vector_store = Chroma.from_documents(documents=documents, embedding=self.embeddings, persist_directory=persist_dir) vector_store.persist() - return vector_store + return vector_store def load_knownlege(self): docments = [] @@ -61,7 +61,7 @@ class KnownLedge2Vector: for file in files: filename = os.path.join(root, file) docs = self._load_file(filename) - # update metadata. + # update metadata. new_docs = [] for doc in docs: doc.metadata = {"source": doc.metadata["source"].replace(DATASETS_DIR, "")} From c82907c963fb93202611fa783196165d8e32d521 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Thu, 18 May 2023 20:23:06 +0800 Subject: [PATCH 16/66] feature:knowledge embedding update --- pilot/server/webserver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index e6ba19160..466ba30b8 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -19,7 +19,7 @@ from langchain import PromptTemplate ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(ROOT_PATH) -from pilot.configs.model_config import DB_SETTINGS, KNOWLEDGE_UPLOAD_ROOT_PATH, LLM_MODEL_CONFIG, TOP_RETURN_SIZE +from pilot.configs.model_config import DB_SETTINGS, KNOWLEDGE_UPLOAD_ROOT_PATH, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K from pilot.server.vectordb_qa import KnownLedgeBaseQA from pilot.connections.mysql import MySQLOperator from pilot.source_embedding.knowledge_embedding import KnowledgeEmbedding @@ -262,7 +262,7 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re vector_store_config={"vector_store_name": vector_store_name["vs_name"], "vector_store_path": KNOWLEDGE_UPLOAD_ROOT_PATH}) query = state.messages[-2][1] - docs = knowledge_embedding_client.similar_search(query, TOP_RETURN_SIZE) + docs = knowledge_embedding_client.similar_search(query, VECTOR_SEARCH_TOP_K) context = [d.page_content for d in docs] prompt_template = PromptTemplate( template=conv_qa_prompt_template, From 3625d76c619f20e6b11d9b61ab608f1a42a5f606 Mon Sep 17 00:00:00 2001 From: csunny Date: Thu, 18 May 2023 20:27:10 +0800 Subject: [PATCH 17/66] fix: config --- pilot/server/webserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index bdfa60f34..c0ebd968a 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -19,7 +19,7 @@ from langchain import PromptTemplate ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(ROOT_PATH) -from pilot.configs.model_config import DB_SETTINGS, KNOWLEDGE_UPLOAD_ROOT_PATH, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K +from pilot.configs.model_config import KNOWLEDGE_UPLOAD_ROOT_PATH, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K from pilot.server.vectordb_qa import KnownLedgeBaseQA from pilot.connections.mysql import MySQLOperator from pilot.source_embedding.knowledge_embedding import KnowledgeEmbedding From caf4705f3baaa964b9c406d49dcdd057b695730d Mon Sep 17 00:00:00 2001 From: csunny Date: Thu, 18 May 2023 20:29:30 +0800 Subject: [PATCH 18/66] fix: update --- pilot/vector_store/file_loader.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pilot/vector_store/file_loader.py b/pilot/vector_store/file_loader.py index b94b45ba2..279d5343c 100644 --- a/pilot/vector_store/file_loader.py +++ b/pilot/vector_store/file_loader.py @@ -48,11 +48,7 @@ class KnownLedge2Vector: # vector_store.add_documents(documents=documents) else: documents = self.load_knownlege() -<<<<<<< HEAD # reinit -======= - # reinit ->>>>>>> 31797ecdb53eff76cceb52454888c91c97572851 vector_store = Chroma.from_documents(documents=documents, embedding=self.embeddings, persist_directory=persist_dir) @@ -65,11 +61,7 @@ class KnownLedge2Vector: for file in files: filename = os.path.join(root, file) docs = self._load_file(filename) -<<<<<<< HEAD - # update metadata. -======= # update metadata. ->>>>>>> 31797ecdb53eff76cceb52454888c91c97572851 new_docs = [] for doc in docs: doc.metadata = {"source": doc.metadata["source"].replace(DATASETS_DIR, "")} From 09a26cb1497c018a99d426ac9a02a65f03cd48d3 Mon Sep 17 00:00:00 2001 From: csunny Date: Thu, 18 May 2023 21:19:28 +0800 Subject: [PATCH 19/66] Repo: requirements --- requirements.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index eac927c3d..410d3129c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,6 +60,13 @@ gTTS==2.3.1 langchain nltk python-dotenv==1.0.0 +vcrpy +chromadb +markdown2 +colorama +playsound +distro +pypdf # Testing dependencies pytest @@ -69,11 +76,4 @@ pytest-benchmark pytest-cov pytest-integration pytest-mock -vcrpy -pytest-recording -chromadb -markdown2 -colorama -playsound -distro -pypdf \ No newline at end of file +pytest-recording \ No newline at end of file From ae0a046b9857e3b309592e4723bf3487755c429d Mon Sep 17 00:00:00 2001 From: csunny Date: Thu, 18 May 2023 21:29:03 +0800 Subject: [PATCH 20/66] update --- pilot/server/webserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index c0ebd968a..2c1ccace0 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -428,7 +428,7 @@ def build_single_model_ui(): notice_markdown = """ # DB-GPT - [DB-GPT](https://github.com/csunny/DB-GPT) 是一个实验性的开源应用程序,它基于[FastChat](https://github.com/lm-sys/FastChat),并使用vicuna-13b作为基础模型。此外,此程序结合了langchain和llama-index基于现有知识库进行In-Context Learning来对其进行数据库相关知识的增强。它可以进行SQL生成、SQL诊断、数据库知识问答等一系列的工作。 总的来说,它是一个用于数据库的复杂且创新的AI工具。如果您对如何在工作中使用或实施DB-GPT有任何具体问题,请联系我, 我会尽力提供帮助, 同时也欢迎大家参与到项目建设中, 做一些有趣的事情。 + [DB-GPT](https://github.com/csunny/DB-GPT) DB-GPT is an experimental open-source project that uses localized GPT large models to interact with your data and environment. With this solution, you can be assured that there is no risk of data leakage, and your data is 100% private and secure. """ learn_more_markdown = """ ### Licence From 7c68a4f44e4507b17546362a5fcb9c7316bff171 Mon Sep 17 00:00:00 2001 From: csunny Date: Thu, 18 May 2023 21:32:51 +0800 Subject: [PATCH 21/66] docs: update --- pilot/server/webserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index 2c1ccace0..377fb1b91 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -428,7 +428,7 @@ def build_single_model_ui(): notice_markdown = """ # DB-GPT - [DB-GPT](https://github.com/csunny/DB-GPT) DB-GPT is an experimental open-source project that uses localized GPT large models to interact with your data and environment. With this solution, you can be assured that there is no risk of data leakage, and your data is 100% private and secure. + [DB-GPT](https://github.com/csunny/DB-GPT) 是一个开源的以数据库为基础的GPT实验项目,使用本地化的GPT大模型与您的数据和环境进行交互,无数据泄露风险,100% 私密,100% 安全。 """ learn_more_markdown = """ ### Licence From 21076a98fbab1ed68eecf85d1c8a5b9c3f57c257 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Thu, 18 May 2023 22:33:21 +0800 Subject: [PATCH 22/66] update:knowledge only one conversation --- pilot/server/webserver.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index 466ba30b8..07d94b773 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -238,7 +238,19 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re ### 后续对话 query = state.messages[-2][1] # 第一轮对话需要加入提示Prompt - if sql_mode == conversation_sql_mode["auto_execute_ai_response"]: + if mode == conversation_types["custome"]: + template_name = "conv_one_shot" + new_state = conv_templates[template_name].copy() + # prompt 中添加上下文提示, 根据已有知识对话, 上下文提示是否也应该放在第一轮, 还是每一轮都添加上下文? + # 如果用户侧的问题跨度很大, 应该每一轮都加提示。 + if db_selector: + new_state.append_message(new_state.roles[0], gen_sqlgen_conversation(dbname) + query) + new_state.append_message(new_state.roles[1], None) + else: + new_state.append_message(new_state.roles[0], query) + new_state.append_message(new_state.roles[1], None) + state = new_state + elif sql_mode == conversation_sql_mode["auto_execute_ai_response"]: ## 获取最后一次插件的返回 follow_up_prompt = auto_prompt.construct_follow_up_prompt([query]) state.messages[0][0] = "" From 03f7ed32e50131812afc6eaa1d69da80c45ee149 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Thu, 18 May 2023 22:36:19 +0800 Subject: [PATCH 23/66] update:knowledge load script --- tools/knowlege_init.py | 45 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tools/knowlege_init.py diff --git a/tools/knowlege_init.py b/tools/knowlege_init.py new file mode 100644 index 000000000..bc827953d --- /dev/null +++ b/tools/knowlege_init.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import argparse + +from pilot.configs.model_config import DATASETS_DIR, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K, \ + KNOWLEDGE_UPLOAD_ROOT_PATH +from pilot.source_embedding.knowledge_embedding import KnowledgeEmbedding + + +class LocalKnowledgeInit: + embeddings: object = None + model_name = LLM_MODEL_CONFIG["text2vec"] + top_k: int = VECTOR_SEARCH_TOP_K + + def __init__(self) -> None: + pass + + def knowledge_persist(self, file_path, vector_name, append_mode): + """ knowledge persist """ + kv = KnowledgeEmbedding( + file_path=file_path, + model_name=LLM_MODEL_CONFIG["text2vec"], + vector_store_config= {"vector_store_name":vector_name, "vector_store_path": KNOWLEDGE_UPLOAD_ROOT_PATH}) + vector_store = kv.knowledge_persist_initialization(append_mode) + return vector_store + + def query(self, q): + """Query similar doc from Vector """ + vector_store = self.init_vector_store() + docs = vector_store.similarity_search_with_score(q, k=self.top_k) + for doc in docs: + dc, s = doc + yield s, dc + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--vector_name", type=str, default="default") + parser.add_argument("--append", type=bool, default=False) + args = parser.parse_args() + vector_name = args.vector_name + append_mode = args.append + kv = LocalKnowledgeInit() + vector_store = kv.knowledge_persist(file_path=DATASETS_DIR, vector_name=vector_name, append_mode=append_mode) + docs = vector_store.similarity_search("小明",1) + print("your knowledge embedding success...") \ No newline at end of file From 9a1e54ebd4137c2857adf1818d37b5f9bae6fecd Mon Sep 17 00:00:00 2001 From: csunny Date: Thu, 18 May 2023 23:31:15 +0800 Subject: [PATCH 24/66] top_k update --- pilot/configs/model_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pilot/configs/model_config.py b/pilot/configs/model_config.py index 2a4f5eac4..5ab2e4fbe 100644 --- a/pilot/configs/model_config.py +++ b/pilot/configs/model_config.py @@ -29,6 +29,6 @@ ISLOAD_8BIT = True ISDEBUG = False -VECTOR_SEARCH_TOP_K = 3 +VECTOR_SEARCH_TOP_K = 10 VS_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "vs_store") KNOWLEDGE_UPLOAD_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") \ No newline at end of file From 09b7f99abae333f2b3d1e49f4aa35d7a24803052 Mon Sep 17 00:00:00 2001 From: xudafeng Date: Fri, 19 May 2023 00:25:09 +0800 Subject: [PATCH 25/66] docs: update readme --- README.md | 2 +- README.zh.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 54a8d0de7..57ca42fbf 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ The achievements of this project are thanks to the technical community, especial | :---: | :---: | :---: | :---: |:---: | -This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Sun May 14 2023 23:02:43 GMT+0800`. +This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Fri May 19 2023 00:24:18 GMT+0800`. diff --git a/README.zh.md b/README.zh.md index 7063dddb3..ed7e3f03c 100644 --- a/README.zh.md +++ b/README.zh.md @@ -194,13 +194,13 @@ $ python webserver.py -## Contributors +## 贡献者 |[
csunny](https://github.com/csunny)
|[
xudafeng](https://github.com/xudafeng)
|[
明天](https://github.com/yhjun1026)
| [
Aries-ckt](https://github.com/Aries-ckt)
|[
thebigbone](https://github.com/thebigbone)
| | :---: | :---: | :---: | :---: |:---: | -This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Sun May 14 2023 23:02:43 GMT+0800`. +[git-contributor 说明](https://github.com/xudafeng/git-contributor),自动生成时间:`Fri May 19 2023 00:24:18 GMT+0800`。 From 78158c6886a8290a40c84f9e3437c0897cff3bd9 Mon Sep 17 00:00:00 2001 From: "magic.chen" Date: Fri, 19 May 2023 09:23:00 +0800 Subject: [PATCH 26/66] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++ .../ISSUE_TEMPLATE/documentation-related.md | 10 +++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++ 3 files changed, 68 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/documentation-related.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..a2998b85e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]: " +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/documentation-related.md b/.github/ISSUE_TEMPLATE/documentation-related.md new file mode 100644 index 000000000..5506434f7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation-related.md @@ -0,0 +1,10 @@ +--- +name: Documentation Related +about: Describe this issue template's purpose here. +title: "[Doc]: " +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..0ccd363af --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature]:" +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 336ba1e042debb1cf7e26cbaba22768f2ef7b1c2 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Fri, 19 May 2023 21:17:39 +0800 Subject: [PATCH 27/66] update:knowledge load script --- pilot/configs/model_config.py | 9 +++-- pilot/server/webserver.py | 1 + .../source_embedding/chn_document_splitter.py | 24 ++---------- pilot/source_embedding/knowledge_embedding.py | 18 ++++++--- pilot/source_embedding/markdown_embedding.py | 3 +- pilot/source_embedding/pdf_embedding.py | 7 ++-- pilot/source_embedding/search_milvus.py | 2 +- pilot/vector_store/milvus_store.py | 37 ++++++++++--------- tools/knowlege_init.py | 1 - 9 files changed, 49 insertions(+), 53 deletions(-) diff --git a/pilot/configs/model_config.py b/pilot/configs/model_config.py index faa93227f..da68ab332 100644 --- a/pilot/configs/model_config.py +++ b/pilot/configs/model_config.py @@ -21,15 +21,17 @@ LLM_MODEL_CONFIG = { "flan-t5-base": os.path.join(MODEL_PATH, "flan-t5-base"), "vicuna-13b": os.path.join(MODEL_PATH, "vicuna-13b"), "text2vec": os.path.join(MODEL_PATH, "text2vec-large-chinese"), + "text2vec-base": os.path.join(MODEL_PATH, "text2vec-base-chinese"), "sentence-transforms": os.path.join(MODEL_PATH, "all-MiniLM-L6-v2") } -VECTOR_SEARCH_TOP_K = 3 +VECTOR_SEARCH_TOP_K = 20 LLM_MODEL = "vicuna-13b" LIMIT_MODEL_CONCURRENCY = 5 MAX_POSITION_EMBEDDINGS = 4096 -VICUNA_MODEL_SERVER = "http://121.41.227.141:8000" +# VICUNA_MODEL_SERVER = "http://121.41.227.141:8000" +VICUNA_MODEL_SERVER = "http://120.79.27.110:8000" # Load model config ISLOAD_8BIT = True @@ -44,4 +46,5 @@ DB_SETTINGS = { } VS_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "vs_store") -KNOWLEDGE_UPLOAD_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") \ No newline at end of file +KNOWLEDGE_UPLOAD_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") +KNOWLEDGE_CHUNK_SPLIT_SIZE = 100 diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index 07d94b773..25940a437 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -499,6 +499,7 @@ def build_single_model_ui(): files = gr.File(label="添加文件", file_types=[".txt", ".md", ".docx", ".pdf"], file_count="multiple", + allow_flagged_uploads=True, show_label=False ) diff --git a/pilot/source_embedding/chn_document_splitter.py b/pilot/source_embedding/chn_document_splitter.py index 090a6af56..10a77aeca 100644 --- a/pilot/source_embedding/chn_document_splitter.py +++ b/pilot/source_embedding/chn_document_splitter.py @@ -9,33 +9,17 @@ class CHNDocumentSplitter(CharacterTextSplitter): self.pdf = pdf self.sentence_size = sentence_size - # def split_text_version2(self, text: str) -> List[str]: - # if self.pdf: - # text = re.sub(r"\n{3,}", "\n", text) - # text = re.sub('\s', ' ', text) - # text = text.replace("\n\n", "") - # sent_sep_pattern = re.compile('([﹒﹔﹖﹗.。!?]["’”」』]{0,2}|(?=["‘“「『]{1,2}|$))') # del :; - # sent_list = [] - # for ele in sent_sep_pattern.split(text): - # if sent_sep_pattern.match(ele) and sent_list: - # sent_list[-1] += ele - # elif ele: - # sent_list.append(ele) - # return sent_list - def split_text(self, text: str) -> List[str]: if self.pdf: text = re.sub(r"\n{3,}", r"\n", text) text = re.sub('\s', " ", text) text = re.sub("\n\n", "", text) - text = re.sub(r'([;;.!?。!?\?])([^”’])', r"\1\n\2", text) # 单字符断句符 - text = re.sub(r'(\.{6})([^"’”」』])', r"\1\n\2", text) # 英文省略号 - text = re.sub(r'(\…{2})([^"’”」』])', r"\1\n\2", text) # 中文省略号 + text = re.sub(r'([;;.!?。!?\?])([^”’])', r"\1\n\2", text) + text = re.sub(r'(\.{6})([^"’”」』])', r"\1\n\2", text) + text = re.sub(r'(\…{2})([^"’”」』])', r"\1\n\2", text) text = re.sub(r'([;;!?。!?\?]["’”」』]{0,2})([^;;!?,。!?\?])', r'\1\n\2', text) - # 如果双引号前有终止符,那么双引号才是句子的终点,把分句符\n放到双引号后,注意前面的几句都小心保留了双引号 - text = text.rstrip() # 段尾如果有多余的\n就去掉它 - # 很多规则中会考虑分号;,但是这里我把它忽略不计,破折号、英文双引号等同样忽略,需要的再做些简单调整即可。 + text = text.rstrip() ls = [i for i in text.split("\n") if i] for ele in ls: if len(ele) > self.sentence_size: diff --git a/pilot/source_embedding/knowledge_embedding.py b/pilot/source_embedding/knowledge_embedding.py index 594723b6e..08d962908 100644 --- a/pilot/source_embedding/knowledge_embedding.py +++ b/pilot/source_embedding/knowledge_embedding.py @@ -4,13 +4,15 @@ from bs4 import BeautifulSoup from langchain.document_loaders import PyPDFLoader, TextLoader, markdown from langchain.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import Chroma -from pilot.configs.model_config import DATASETS_DIR +from pilot.configs.model_config import DATASETS_DIR, KNOWLEDGE_CHUNK_SPLIT_SIZE from pilot.source_embedding.chn_document_splitter import CHNDocumentSplitter from pilot.source_embedding.csv_embedding import CSVEmbedding from pilot.source_embedding.markdown_embedding import MarkdownEmbedding from pilot.source_embedding.pdf_embedding import PDFEmbedding import markdown +from pilot.source_embedding.pdf_loader import UnstructuredPaddlePDFLoader + class KnowledgeEmbedding: def __init__(self, file_path, model_name, vector_store_config, local_persist=True): @@ -63,7 +65,7 @@ class KnowledgeEmbedding: print("directly return vector store") vector_store = Chroma(persist_directory=persist_dir, embedding_function=self.embeddings) else: - print(vector_name + "is new vector store, knowledge begin load...") + print(vector_name + " is new vector store, knowledge begin load...") documents = self._load_knownlege(self.file_path) vector_store = Chroma.from_documents(documents=documents, embedding=self.embeddings, @@ -88,7 +90,7 @@ class KnowledgeEmbedding: def _load_file(self, filename): if filename.lower().endswith(".md"): loader = TextLoader(filename) - text_splitter = CHNDocumentSplitter(pdf=True, sentence_size=100) + text_splitter = CHNDocumentSplitter(pdf=True, sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE) docs = loader.load_and_split(text_splitter) i = 0 for d in docs: @@ -100,11 +102,15 @@ class KnowledgeEmbedding: docs[i].page_content = docs[i].page_content.replace("\n", " ") i += 1 elif filename.lower().endswith(".pdf"): - loader = PyPDFLoader(filename) - textsplitter = CHNDocumentSplitter(pdf=True, sentence_size=100) + loader = UnstructuredPaddlePDFLoader(filename) + textsplitter = CHNDocumentSplitter(pdf=True, sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE) docs = loader.load_and_split(textsplitter) + i = 0 + for d in docs: + docs[i].page_content = d.page_content.replace("\n", " ").replace("�", "") + i += 1 else: loader = TextLoader(filename) - text_splitor = CHNDocumentSplitter(sentence_size=100) + text_splitor = CHNDocumentSplitter(sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE) docs = loader.load_and_split(text_splitor) return docs \ No newline at end of file diff --git a/pilot/source_embedding/markdown_embedding.py b/pilot/source_embedding/markdown_embedding.py index fee9504b6..834226f75 100644 --- a/pilot/source_embedding/markdown_embedding.py +++ b/pilot/source_embedding/markdown_embedding.py @@ -7,6 +7,7 @@ from bs4 import BeautifulSoup from langchain.document_loaders import TextLoader from langchain.schema import Document import markdown +from pilot.configs.model_config import KNOWLEDGE_CHUNK_SPLIT_SIZE from pilot.source_embedding import SourceEmbedding, register from pilot.source_embedding.chn_document_splitter import CHNDocumentSplitter @@ -26,7 +27,7 @@ class MarkdownEmbedding(SourceEmbedding): def read(self): """Load from markdown path.""" loader = TextLoader(self.file_path) - text_splitter = CHNDocumentSplitter(pdf=True, sentence_size=100) + text_splitter = CHNDocumentSplitter(pdf=True, sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE) return loader.load_and_split(text_splitter) @register diff --git a/pilot/source_embedding/pdf_embedding.py b/pilot/source_embedding/pdf_embedding.py index bd0ae3aba..a8749695b 100644 --- a/pilot/source_embedding/pdf_embedding.py +++ b/pilot/source_embedding/pdf_embedding.py @@ -2,11 +2,12 @@ # -*- coding: utf-8 -*- from typing import List -from langchain.document_loaders import PyPDFLoader from langchain.schema import Document +from pilot.configs.model_config import KNOWLEDGE_CHUNK_SPLIT_SIZE from pilot.source_embedding import SourceEmbedding, register from pilot.source_embedding.chn_document_splitter import CHNDocumentSplitter +from pilot.source_embedding.pdf_loader import UnstructuredPaddlePDFLoader class PDFEmbedding(SourceEmbedding): @@ -22,8 +23,8 @@ class PDFEmbedding(SourceEmbedding): @register def read(self): """Load from pdf path.""" - loader = PyPDFLoader(self.file_path) - textsplitter = CHNDocumentSplitter(pdf=True, sentence_size=100) + loader = UnstructuredPaddlePDFLoader(self.file_path) + textsplitter = CHNDocumentSplitter(pdf=True, sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE) return loader.load_and_split(textsplitter) @register diff --git a/pilot/source_embedding/search_milvus.py b/pilot/source_embedding/search_milvus.py index 18f93d1d3..ec0aa6813 100644 --- a/pilot/source_embedding/search_milvus.py +++ b/pilot/source_embedding/search_milvus.py @@ -50,7 +50,7 @@ # # # text_embeddings = Text2Vectors() # mivuls = MilvusStore(cfg={"url": "127.0.0.1", "port": "19530", "alias": "default", "table_name": "test_k"}) -# +# # mivuls.insert(["textc","tezt2"]) # print("success") # ct diff --git a/pilot/vector_store/milvus_store.py b/pilot/vector_store/milvus_store.py index 1f07c969e..eda0b4e38 100644 --- a/pilot/vector_store/milvus_store.py +++ b/pilot/vector_store/milvus_store.py @@ -1,6 +1,7 @@ - +from langchain.embeddings import HuggingFaceEmbeddings from pymilvus import DataType, FieldSchema, CollectionSchema, connections, Collection +from pilot.configs.model_config import LLM_MODEL_CONFIG from pilot.vector_store.vector_store_base import VectorStoreBase @@ -9,7 +10,7 @@ class MilvusStore(VectorStoreBase): """Construct a milvus memory storage connection. Args: - cfg (Config): Auto-GPT global config. + cfg (Config): MilvusStore global config. """ # self.configure(cfg) @@ -71,21 +72,21 @@ class MilvusStore(VectorStoreBase): self.index_params, index_name="vector", ) + info = self.collection.describe() self.collection.load() - # def add(self, data) -> str: - # """Add an embedding of data into milvus. - # - # Args: - # data (str): The raw text to construct embedding index. - # - # Returns: - # str: log. - # """ - # embedding = get_ada_embedding(data) - # result = self.collection.insert([[embedding], [data]]) - # _text = ( - # "Inserting data into memory at primary key: " - # f"{result.primary_keys[0]}:\n data: {data}" - # ) - # return _text \ No newline at end of file + def insert(self, text) -> str: + """Add an embedding of data into milvus. + Args: + text (str): The raw text to construct embedding index. + Returns: + str: log. + """ + # embedding = get_ada_embedding(data) + embeddings = HuggingFaceEmbeddings(model_name=LLM_MODEL_CONFIG["sentence-transforms"]) + result = self.collection.insert([embeddings.embed_documents(text), text]) + _text = ( + "Inserting data into memory at primary key: " + f"{result.primary_keys[0]}:\n data: {text}" + ) + return _text \ No newline at end of file diff --git a/tools/knowlege_init.py b/tools/knowlege_init.py index bc827953d..e9ecad49a 100644 --- a/tools/knowlege_init.py +++ b/tools/knowlege_init.py @@ -41,5 +41,4 @@ if __name__ == "__main__": append_mode = args.append kv = LocalKnowledgeInit() vector_store = kv.knowledge_persist(file_path=DATASETS_DIR, vector_name=vector_name, append_mode=append_mode) - docs = vector_store.similarity_search("小明",1) print("your knowledge embedding success...") \ No newline at end of file From e871df20f5ddaf7f7116245fdb18dad7056c2c3b Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Fri, 19 May 2023 21:35:35 +0800 Subject: [PATCH 28/66] update:knowledge load script --- README.md | 19 +++++ README.zh.md | 233 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 README.zh.md diff --git a/README.md b/README.md index 54a8d0de7..bfc906c2c 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,25 @@ As the knowledge base is currently the most significant user demand scenario, we 2. Custom addition of knowledge bases 3. Various usage scenarios such as constructing knowledge bases through plugin capabilities and web crawling. Users only need to organize the knowledge documents, and they can use our existing capabilities to build the knowledge base required for the large model. +Create your own knowledge base: + +1.Place personal knowledge files or folders in the pilot/datasets directory. + +2.Run the knowledge repository script in the tools directory. + +``` +python tools/knowledge_init.py + +--vector_name : your vector store name default_value:default +--append: append mode, True:append, False: not append default_value:False + +``` + +3.Add the knowledge base in the interface by entering the name of your knowledge base (if not specified, enter "default") so you can use it for Q&A based on your knowledge base. + +Note that the default vector model used is text2vec-large-chinese (which is a large model, so if your personal computer configuration is not enough, it is recommended to use text2vec-base-chinese). Therefore, ensure that you download the model and place it in the models directory. + + ### LLMs Management In the underlying large model integration, we have designed an open interface that supports integration with various large models. At the same time, we have a very strict control and evaluation mechanism for the effectiveness of the integrated models. In terms of accuracy, the integrated models need to align with the capability of ChatGPT at a level of 85% or higher. We use higher standards to select models, hoping to save users the cumbersome testing and evaluation process in the process of use. diff --git a/README.zh.md b/README.zh.md new file mode 100644 index 000000000..3675f3496 --- /dev/null +++ b/README.zh.md @@ -0,0 +1,233 @@ +# DB-GPT ![GitHub Repo stars](https://img.shields.io/github/stars/csunny/db-gpt?style=social) + +[English](README.zh.md) + +[![Star History Chart](https://api.star-history.com/svg?repos=csunny/DB-GPT)](https://star-history.com/#csunny/DB-GPT) + +## DB-GPT 是什么? +随着大模型的发布迭代,大模型变得越来越智能,在使用大模型的过程当中,遇到极大的数据安全与隐私挑战。在利用大模型能力的过程中我们的私密数据跟环境需要掌握自己的手里,完全可控,避免任何的数据隐私泄露以及安全风险。基于此,我们发起了DB-GPT项目,为所有以数据库为基础的场景,构建一套完整的私有大模型解决方案。 此方案因为支持本地部署,所以不仅仅可以应用于独立私有环境,而且还可以根据业务模块独立部署隔离,让大模型的能力绝对私有、安全、可控。 + +DB-GPT 是一个开源的以数据库为基础的GPT实验项目,使用本地化的GPT大模型与您的数据和环境进行交互,无数据泄露风险,100% 私密,100% 安全。 + + +## 特性一览 + +目前我们已经发布了多种关键的特性,这里一一列举展示一下当前发布的能力。 +- SQL 语言能力 + - SQL生成 + - SQL诊断 +- 私域问答与数据处理 + - 数据库知识问答 + - 数据处理 +- 插件模型 + - 支持自定义插件执行任务,原生支持Auto-GPT插件。如: + - SQL自动执行,获取查询结果 + - 自动爬取学习知识 +- 知识库统一向量存储/索引 + - 非结构化数据支持包括PDF、MarkDown、CSV、WebURL + +## 效果演示 + +示例通过 RTX 4090 GPU 演示,[YouTube 地址](https://www.youtube.com/watch?v=1PWI6F89LPo) +### 运行环境演示 + +

+ +

+ +

+ +

+ +### SQL 生成 + +1. 生成建表语句 + +

+ +

+ +2. 生成可运行SQL +首先选择对应的数据库, 然后模型即可根据对应的数据库 Schema 信息生成 SQL, 运行成功的效果如下面的演示: + +

+ +

+ +3. 自动分析执行SQL输出运行结果 + +

+ +

+ +### 数据库问答 + +

+ +

+ + +1. 基于默认内置知识库问答 + +

+ +

+ +2. 自己新增知识库 + +

+ +

+ +3. 从网络自己爬取数据学习 +- TODO + +## 架构方案 +DB-GPT基于 [FastChat](https://github.com/lm-sys/FastChat) 构建大模型运行环境,并提供 vicuna 作为基础的大语言模型。此外,我们通过LangChain提供私域知识库问答能力。同时我们支持插件模式, 在设计上原生支持Auto-GPT插件。 + +整个DB-GPT的架构,如下图所示 + +

+ +

+ +核心能力主要有以下几个部分。 +1. 知识库能力:支持私域知识库问答能力 +2. 大模型管理能力:基于FastChat提供一个大模型的运营环境。 +3. 统一的数据向量化存储与索引:提供一种统一的方式来存储和索引各种数据类型。 +4. 连接模块:用于连接不同的模块和数据源,实现数据的流转和交互。 +5. Agent与插件:提供Agent和插件机制,使得用户可以自定义并增强系统的行为。 +6. Prompt自动生成与优化:自动化生成高质量的Prompt,并进行优化,提高系统的响应效率。 +7. 多端产品界面:支持多种不同的客户端产品,例如Web、移动应用和桌面应用等。 + +下面对每个模块也做一些简要的介绍: + +### 知识库能力 +知识库作为当前用户需求最大的场景,我们原生支持知识库的构建与处理。同时在本项目当中,也提供了多种知识库的管理策略。 如: +1. 默认内置知识库 +2. 自定义新增知识库 +3. 通过插件能力自抓取构建知识库等多种使用场景。 + +用户只需要整理好知识文档,即可用我们现有的能力构建大模型所需要的知识库能力。 + +打造属于你的知识库: + +1、将个人知识文件或者文件夹放入pilot/datasets目录中 + +2、在tools目录执行知识入库脚本 + +``` +python tools/knowledge_init.py + +--vector_name : your vector store name default_value:default +--append: append mode, True:append, False: not append default_value:False + +``` +3、在界面上新增知识库输入你的知识库名(如果没指定输入default),就可以根据你的知识库进行问答 + +注意,这里默认向量模型是text2vec-large-chinese(模型比较大,如果个人电脑配置不够建议采用text2vec-base-chinese),因此确保需要将模型download下来放到models目录中 + + +### 大模型管理能力 +在底层大模型接入中,设计了开放的接口,支持对接多种大模型。同时对于接入模型的效果,我们有非常严格的把控与评审机制。对大模型能力上与ChatGPT对比,在准确率上需要满足85%以上的能力对齐。我们用更高的标准筛选模型,是期望在用户使用过程中,可以省去前面繁琐的测试评估环节。 + +### 统一的数据向量化存储与索引 +为了方便对知识向量化之后的管理,我们内置了多种向量存储引擎,从基于内存的Chroma到分布式的Milvus, 可以根据自己的场景需求,选择不同的存储引擎,整个知识向量存储是AI能力增强的基石,向量作为人与大语言模型交互的中间语言,在本项目中的作用非常重要。 + +### 连接模块 +为了能够更方便的与用户的私有环境进行交互,项目设计了连接模块,连接模块可以支持连接到数据库、Excel、知识库等等多种环境当中,实现信息与数据交互。 + +### Agent与插件 +Agent与插件能力是大模型能否自动化的核心,在本的项目中,原生支持插件模式,大模型可以自动化完成目标。 同时为了充分发挥社区的优势,本项目中所用的插件原生支持Auto-GPT插件生态,即Auto-GPT的插件可以直接在我们的项目中运行。 + +### Prompt自动生成与优化 +Prompt是与大模型交互过程中非常重要的部分,一定程度上Prompt决定了大模型生成答案的质量与准确性,在本的项目中,我们会根据用户输入与使用场景,自动优化对应的Prompt,让用户使用大语言模型变得更简单、更高效。 + +### 多端产品界面 +TODO: 在终端展示上,我们将提供多端产品界面。包括PC、手机、命令行、Slack等多种模式。 + + +## 安装教程 +### 1.硬件说明 +因为我们的项目在效果上具备ChatGPT 85%以上的能力,因此对硬件有一定的要求。 但总体来说,我们在消费级的显卡上即可完成项目的部署使用,具体部署的硬件说明如下: +| GPU型号 | 显存大小 | 性能 | +| ------- | -------- | ------------------------------------------ | +| RTX4090 | 24G | 可以流畅的进行对话推理,无卡顿 | +| RTX3090 | 24G | 可以流畅进行对话推理,有卡顿感,但好于V100 | +| V100 | 16G | 可以进行对话推理,有明显卡顿 | +### 2.DB-GPT安装 + +本项目依赖一个本地的 MySQL 数据库服务,你需要本地安装,推荐直接使用 Docker 安装。 +``` +docker run --name=mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=aa12345678 -dit mysql:latest +``` +向量数据库我们默认使用的是Chroma内存数据库,所以无需特殊安装,如果有需要连接其他的同学,可以按照我们的教程进行安装配置。整个DB-GPT的安装过程,我们使用的是miniconda3的虚拟环境。创建虚拟环境,并安装python依赖包 + +``` +python>=3.10 +conda create -n dbgpt_env python=3.10 +conda activate dbgpt_env +pip install -r requirements.txt + +``` + +### 3. 运行大模型 + +关于基础模型, 可以根据[Vicuna](https://github.com/lm-sys/FastChat/blob/main/README.md#model-weights)合成教程进行合成。 +如果此步有困难的同学,也可以直接使用[此链接](https://huggingface.co/Tribbiani/vicuna-7b)上的模型进行替代。 + + 运行模型服务 +``` +cd pilot/server +python llmserver.py +``` + +运行 gradio webui + +```bash +$ python webserver.py +``` +注意: 在启动Webserver之前, 需要修改.env 文件中的MODEL_SERVER = "http://127.0.0.1:8000", 将地址设置为你的服务器地址。 + +## 使用说明 + +我们提供了Gradio的用户界面,可以通过我们的用户界面使用DB-GPT, 同时关于我们项目相关的一些代码跟原理介绍,我们也准备了以下几篇参考文章。 +1. [大模型实战系列(1) —— 强强联合Langchain-Vicuna应用实战](https://zhuanlan.zhihu.com/p/628750042) +2. [大模型实战系列(2) —— DB-GPT 阿里云部署指南](https://zhuanlan.zhihu.com/p/629467580) +3. [大模型实战系列(3) —— DB-GPT插件模型原理与使用](https://zhuanlan.zhihu.com/p/629623125) + +## 感谢 + +项目取得的成果,需要感谢技术社区,尤其以下项目。 + +- [FastChat](https://github.com/lm-sys/FastChat) 提供 chat 服务 +- [vicuna-13b](https://huggingface.co/Tribbiani/vicuna-13b) 作为基础模型 +- [langchain](https://github.com/hwchase17/langchain) 工具链 +- [Auto-GPT](https://github.com/Significant-Gravitas/Auto-GPT) 通用的插件模版 +- [Hugging Face](https://huggingface.co/) 大模型管理 +- [Chroma](https://github.com/chroma-core/chroma) 向量存储 +- [Milvus](https://milvus.io/) 分布式向量存储 +- [ChatGLM](https://github.com/THUDM/ChatGLM-6B) 基础模型 +- [llama-index](https://github.com/jerryjliu/llama_index) 基于现有知识库进行[In-Context Learning](https://arxiv.org/abs/2301.00234)来对其进行数据库相关知识的增强。 + + + +## Contributors + +|[
csunny](https://github.com/csunny)
|[
xudafeng](https://github.com/xudafeng)
|[
明天](https://github.com/yhjun1026)
| [
Aries-ckt](https://github.com/Aries-ckt)
|[
thebigbone](https://github.com/thebigbone)
| +| :---: | :---: | :---: | :---: |:---: | + + +This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Sun May 14 2023 23:02:43 GMT+0800`. + + + +这是一个用于数据库的复杂且创新的工具, 我们的项目也在紧急的开发当中, 会陆续发布一些新的feature。如在使用当中有任何具体问题, 优先在项目下提issue, 如有需要, 请联系如下微信,我会尽力提供帮助,同时也非常欢迎大家参与到项目建设中。 + +

+ +

+ +## Licence + +The MIT License (MIT) From e2f18fc187811e93abe5c4874e0781ad3b192639 Mon Sep 17 00:00:00 2001 From: csunny Date: Fri, 19 May 2023 21:44:54 +0800 Subject: [PATCH 29/66] add multi adapter --- pilot/configs/model_config.py | 4 +++- pilot/model/adapter.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pilot/configs/model_config.py b/pilot/configs/model_config.py index 5ab2e4fbe..9699061b7 100644 --- a/pilot/configs/model_config.py +++ b/pilot/configs/model_config.py @@ -21,7 +21,9 @@ LLM_MODEL_CONFIG = { "flan-t5-base": os.path.join(MODEL_PATH, "flan-t5-base"), "vicuna-13b": os.path.join(MODEL_PATH, "vicuna-13b"), "text2vec": os.path.join(MODEL_PATH, "text2vec-large-chinese"), - "sentence-transforms": os.path.join(MODEL_PATH, "all-MiniLM-L6-v2") + "sentence-transforms": os.path.join(MODEL_PATH, "all-MiniLM-L6-v2"), + "codegen2-7b": os.path.join(MODEL_PATH, ""), + "codet5p-2b": os.path.join(MODEL_PATH, "codet5p-2b"), } # Load model config diff --git a/pilot/model/adapter.py b/pilot/model/adapter.py index 9afd2c01f..83d8a3717 100644 --- a/pilot/model/adapter.py +++ b/pilot/model/adapter.py @@ -68,6 +68,19 @@ class ChatGLMAdapater(BaseLLMAdaper): model_path, trust_remote_code=True, **from_pretrained_kwargs ).half().cuda() return model, tokenizer + +class ZiYaLLaMaAdapter(BaseLLMAdaper): + # TODO + pass + +class CodeGenAdapter(BaseLLMAdaper): + pass + +class StarCoderAdapter(BaseLLMAdaper): + pass + +class T5CodeAdapter(BaseLLMAdaper): + pass class KoalaLLMAdapter(BaseLLMAdaper): """Koala LLM Adapter which Based LLaMA """ From 98d50b1b98c93f8cc5d22f69da2cd0bf99466a03 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Fri, 19 May 2023 22:04:20 +0800 Subject: [PATCH 30/66] update:reademe knowledge init --- README.md | 36 ++++++++++++++++++------------------ README.zh.md | 32 ++++++++++++++++---------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index bfc906c2c..cd7aaff9b 100644 --- a/README.md +++ b/README.md @@ -103,24 +103,6 @@ As the knowledge base is currently the most significant user demand scenario, we 2. Custom addition of knowledge bases 3. Various usage scenarios such as constructing knowledge bases through plugin capabilities and web crawling. Users only need to organize the knowledge documents, and they can use our existing capabilities to build the knowledge base required for the large model. -Create your own knowledge base: - -1.Place personal knowledge files or folders in the pilot/datasets directory. - -2.Run the knowledge repository script in the tools directory. - -``` -python tools/knowledge_init.py - ---vector_name : your vector store name default_value:default ---append: append mode, True:append, False: not append default_value:False - -``` - -3.Add the knowledge base in the interface by entering the name of your knowledge base (if not specified, enter "default") so you can use it for Q&A based on your knowledge base. - -Note that the default vector model used is text2vec-large-chinese (which is a large model, so if your personal computer configuration is not enough, it is recommended to use text2vec-base-chinese). Therefore, ensure that you download the model and place it in the models directory. - ### LLMs Management @@ -191,9 +173,27 @@ $ python pilot/server/webserver.py Notice: the webserver need to connect llmserver, so you need change the .env file. change the MODEL_SERVER = "http://127.0.0.1:8000" to your address. It's very important. ## Usage Instructions + We provide a user interface for Gradio, which allows you to use DB-GPT through our user interface. Additionally, we have prepared several reference articles (written in Chinese) that introduce the code and principles related to our project. - [LLM Practical In Action Series (1) — Combined Langchain-Vicuna Application Practical](https://medium.com/@cfqcsunny/llm-practical-in-action-series-1-combined-langchain-vicuna-application-practical-701cd0413c9f) +####Create your own knowledge repository: + +1.Place personal knowledge files or folders in the pilot/datasets directory. + +2.Run the knowledge repository script in the tools directory. + +``` +python tools/knowledge_init.py + +--vector_name : your vector store name default_value:default +--append: append mode, True:append, False: not append default_value:False + +``` + +3.Add the knowledge repository in the interface by entering the name of your knowledge repository (if not specified, enter "default") so you can use it for Q&A based on your knowledge base. + +Note that the default vector model used is text2vec-large-chinese (which is a large model, so if your personal computer configuration is not enough, it is recommended to use text2vec-base-chinese). Therefore, ensure that you download the model and place it in the models directory. ## Acknowledgement The achievements of this project are thanks to the technical community, especially the following projects: diff --git a/README.zh.md b/README.zh.md index 3675f3496..2c6ecca43 100644 --- a/README.zh.md +++ b/README.zh.md @@ -110,22 +110,6 @@ DB-GPT基于 [FastChat](https://github.com/lm-sys/FastChat) 构建大模型运 用户只需要整理好知识文档,即可用我们现有的能力构建大模型所需要的知识库能力。 -打造属于你的知识库: - -1、将个人知识文件或者文件夹放入pilot/datasets目录中 - -2、在tools目录执行知识入库脚本 - -``` -python tools/knowledge_init.py - ---vector_name : your vector store name default_value:default ---append: append mode, True:append, False: not append default_value:False - -``` -3、在界面上新增知识库输入你的知识库名(如果没指定输入default),就可以根据你的知识库进行问答 - -注意,这里默认向量模型是text2vec-large-chinese(模型比较大,如果个人电脑配置不够建议采用text2vec-base-chinese),因此确保需要将模型download下来放到models目录中 ### 大模型管理能力 @@ -196,6 +180,22 @@ $ python webserver.py 2. [大模型实战系列(2) —— DB-GPT 阿里云部署指南](https://zhuanlan.zhihu.com/p/629467580) 3. [大模型实战系列(3) —— DB-GPT插件模型原理与使用](https://zhuanlan.zhihu.com/p/629623125) +####打造属于你的知识库: + +1、将个人知识文件或者文件夹放入pilot/datasets目录中 + +2、在tools目录执行知识入库脚本 + +``` +python tools/knowledge_init.py + +--vector_name : your vector store name default_value:default +--append: append mode, True:append, False: not append default_value:False + +``` +3、在界面上新增知识库输入你的知识库名(如果没指定输入default),就可以根据你的知识库进行问答 + +注意,这里默认向量模型是text2vec-large-chinese(模型比较大,如果个人电脑配置不够建议采用text2vec-base-chinese),因此确保需要将模型download下来放到models目录中。 ## 感谢 项目取得的成果,需要感谢技术社区,尤其以下项目。 From 67f5bbc96a945b3c49fe5d4b7ae54c6f051afd43 Mon Sep 17 00:00:00 2001 From: hyj1991 Date: Fri, 19 May 2023 22:33:50 +0800 Subject: [PATCH 31/66] feat: support customizing port for llmserver --- pilot/configs/config.py | 3 ++- pilot/server/llmserver.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pilot/configs/config.py b/pilot/configs/config.py index 9023bc061..b914390f7 100644 --- a/pilot/configs/config.py +++ b/pilot/configs/config.py @@ -105,7 +105,8 @@ class Config(metaclass=Singleton): self.LLM_MODEL = os.getenv("LLM_MODEL", "vicuna-13b") self.LIMIT_MODEL_CONCURRENCY = int(os.getenv("LIMIT_MODEL_CONCURRENCY", 5)) self.MAX_POSITION_EMBEDDINGS = int(os.getenv("MAX_POSITION_EMBEDDINGS", 4096)) - self.MODEL_SERVER = os.getenv("MODEL_SERVER", "http://121.41.167.183:8000") + self.MODEL_PORT = os.getenv("MODEL_PORT", 8000) + self.MODEL_SERVER = os.getenv("MODEL_SERVER", "http://127.0.0.1" + ":" + str(self.MODEL_PORT)) self.ISLOAD_8BIT = os.getenv("ISLOAD_8BIT", "True") == "True" def set_debug_mode(self, value: bool) -> None: diff --git a/pilot/server/llmserver.py b/pilot/server/llmserver.py index e341cc457..e1c7556f6 100644 --- a/pilot/server/llmserver.py +++ b/pilot/server/llmserver.py @@ -130,4 +130,4 @@ def embeddings(prompt_request: EmbeddingRequest): if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", log_level="info") \ No newline at end of file + uvicorn.run(app, host="0.0.0.0", port=CFG.MODEL_PORT, log_level="info") \ No newline at end of file From c0532246afec941dec406e07ff3e74c29d65689a Mon Sep 17 00:00:00 2001 From: csunny Date: Sat, 20 May 2023 15:45:43 +0800 Subject: [PATCH 32/66] llm: add chatglm --- pilot/model/adapter.py | 4 ---- pilot/model/chat.py | 3 --- pilot/model/chatglm_llm.py | 41 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) delete mode 100644 pilot/model/chat.py create mode 100644 pilot/model/chatglm_llm.py diff --git a/pilot/model/adapter.py b/pilot/model/adapter.py index 83d8a3717..84cd699ac 100644 --- a/pilot/model/adapter.py +++ b/pilot/model/adapter.py @@ -69,10 +69,6 @@ class ChatGLMAdapater(BaseLLMAdaper): ).half().cuda() return model, tokenizer -class ZiYaLLaMaAdapter(BaseLLMAdaper): - # TODO - pass - class CodeGenAdapter(BaseLLMAdaper): pass diff --git a/pilot/model/chat.py b/pilot/model/chat.py deleted file mode 100644 index 97206f2d5..000000000 --- a/pilot/model/chat.py +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding:utf-8 -*- - diff --git a/pilot/model/chatglm_llm.py b/pilot/model/chatglm_llm.py new file mode 100644 index 000000000..ef54e92d7 --- /dev/null +++ b/pilot/model/chatglm_llm.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- + +import torch + +@torch.inference_mode() +def chatglm_generate_stream(model, tokenizer, params, device, context_len=2048, stream_interval=2): + + """Generate text using chatglm model's chat api """ + messages = params["prompt"] + max_new_tokens = int(params.get("max_new_tokens", 256)) + temperature = float(params.get("temperature", 1.0)) + top_p = float(params.get("top_p", 1.0)) + echo = params.get("echo", True) + + generate_kwargs = { + "max_new_tokens": max_new_tokens, + "do_sample": True if temperature > 1e-5 else False, + "top_p": top_p, + "logits_processor": None + } + + if temperature > 1e-5: + generate_kwargs["temperature"] = temperature + + hist = [] + for i in range(0, len(messages) - 2, 2): + hist.append(messages[i][1], messages[i + 1][1]) + + query = messages[-2][1] + output = "" + i = 0 + for i, (response, new_hist) in enumerate(model.stream_chat(tokenizer, query, hist, **generate_kwargs)): + if echo: + output = query + " " + response + else: + output = response + + yield output + + yield output \ No newline at end of file From cbf1d0662a0b9a39eab5d0491859206b7c09c033 Mon Sep 17 00:00:00 2001 From: csunny Date: Sat, 20 May 2023 16:06:32 +0800 Subject: [PATCH 33/66] llms: add models --- .gitignore | 1 + pilot/configs/model_config.py | 5 ++++- pilot/server/chat_adapter.py | 13 +++++++++++++ pilot/server/llmserver.py | 1 - 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 pilot/server/chat_adapter.py diff --git a/.gitignore b/.gitignore index 5043f7db0..22bb204db 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ lib/ lib64/ parts/ sdist/ +models var/ wheels/ models/ diff --git a/pilot/configs/model_config.py b/pilot/configs/model_config.py index 9699061b7..265007ae5 100644 --- a/pilot/configs/model_config.py +++ b/pilot/configs/model_config.py @@ -20,10 +20,13 @@ DEVICE = "cuda" if torch.cuda.is_available() else "cpu" LLM_MODEL_CONFIG = { "flan-t5-base": os.path.join(MODEL_PATH, "flan-t5-base"), "vicuna-13b": os.path.join(MODEL_PATH, "vicuna-13b"), + "vicuna-7b": os.path.join(MODEL_PATH, "vicuna-7b"), "text2vec": os.path.join(MODEL_PATH, "text2vec-large-chinese"), "sentence-transforms": os.path.join(MODEL_PATH, "all-MiniLM-L6-v2"), - "codegen2-7b": os.path.join(MODEL_PATH, ""), + "codegen2-1b": os.path.join(MODEL_PATH, "codegen2-1B"), "codet5p-2b": os.path.join(MODEL_PATH, "codet5p-2b"), + "chatglm-6b-int4": os.path.join(MODEL_PATH, "chatglm-6b-int4"), + "chatglm-6b": os.path.join(MODEL_PATH, "chatglm-6b"), } # Load model config diff --git a/pilot/server/chat_adapter.py b/pilot/server/chat_adapter.py new file mode 100644 index 000000000..9c32c911d --- /dev/null +++ b/pilot/server/chat_adapter.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +class BaseChatAdpter: + """The Base class for chat with llm models. it will match the model, + and fetch output from model""" + + def match(self, model_path: str): + return True + + def get_generate_stream_func(self): + pass \ No newline at end of file diff --git a/pilot/server/llmserver.py b/pilot/server/llmserver.py index e1c7556f6..33d3d545d 100644 --- a/pilot/server/llmserver.py +++ b/pilot/server/llmserver.py @@ -30,7 +30,6 @@ model_path = LLM_MODEL_CONFIG[CFG.LLM_MODEL] ml = ModelLoader(model_path=model_path) model, tokenizer = ml.loader(num_gpus=1, load_8bit=ISLOAD_8BIT, debug=ISDEBUG) -#model, tokenizer = load_model(model_path=model_path, device=DEVICE, num_gpus=1, load_8bit=True, debug=False) class ModelWorker: def __init__(self): From 370e327bf3ebb037dc5e275fe58005c175463382 Mon Sep 17 00:00:00 2001 From: csunny Date: Sat, 20 May 2023 16:23:07 +0800 Subject: [PATCH 34/66] add chatglm model --- pilot/model/adapter.py | 1 + pilot/server/chat_adapter.py | 71 +++++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/pilot/model/adapter.py b/pilot/model/adapter.py index 84cd699ac..bf0e291ce 100644 --- a/pilot/model/adapter.py +++ b/pilot/model/adapter.py @@ -100,6 +100,7 @@ class GPT4AllAdapter(BaseLLMAdaper): register_llm_model_adapters(VicunaLLMAdapater) +register_llm_model_adapters(ChatGLMAdapater) # TODO Default support vicuna, other model need to tests and Evaluate register_llm_model_adapters(BaseLLMAdaper) \ No newline at end of file diff --git a/pilot/server/chat_adapter.py b/pilot/server/chat_adapter.py index 9c32c911d..ded0a1b19 100644 --- a/pilot/server/chat_adapter.py +++ b/pilot/server/chat_adapter.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +from typing import List +from functools import cache +from pilot.model.inference import generate_stream class BaseChatAdpter: """The Base class for chat with llm models. it will match the model, @@ -10,4 +13,70 @@ class BaseChatAdpter: return True def get_generate_stream_func(self): - pass \ No newline at end of file + """Return the generate stream handler func""" + pass + + +llm_model_chat_adapters: List[BaseChatAdpter] = [] + + +def register_llm_model_chat_adapter(cls): + """Register a chat adapter""" + llm_model_chat_adapters.append(cls()) + + +@cache +def get_llm_chat_adapter(model_path: str) -> BaseChatAdpter: + """Get a chat generate func for a model""" + for adapter in llm_model_chat_adapters: + if adapter.match(model_path): + return adapter + + raise ValueError(f"Invalid model for chat adapter {model_path}") + + +class VicunaChatAdapter(BaseChatAdpter): + + """ Model chat Adapter for vicuna""" + def match(self, model_path: str): + return "vicuna" in model_path + + def get_generate_stream_func(self): + return generate_stream + + +class ChatGLMChatAdapter(BaseChatAdpter): + """ Model chat Adapter for ChatGLM""" + def match(self, model_path: str): + return "chatglm" in model_path + + def get_generate_stream_func(self): + from pilot.model.chatglm_llm import chatglm_generate_stream + return chatglm_generate_stream + + +class CodeT5ChatAdapter(BaseChatAdpter): + + """ Model chat adapter for CodeT5 """ + def match(self, model_path: str): + return "codet5" in model_path + + def get_generate_stream_func(self): + # TODO + pass + +class CodeGenChatAdapter(BaseChatAdpter): + + """ Model chat adapter for CodeGen """ + def match(self, model_path: str): + return "codegen" in model_path + + def get_generate_stream_func(self): + # TODO + pass + + +register_llm_model_chat_adapter(VicunaChatAdapter) +register_llm_model_chat_adapter(ChatGLMChatAdapter) + +register_llm_model_chat_adapter(BaseChatAdpter) \ No newline at end of file From 8e127b38636c7df742017199951a7330b78fc9cf Mon Sep 17 00:00:00 2001 From: csunny Date: Sun, 21 May 2023 10:13:59 +0800 Subject: [PATCH 35/66] llms: add chatglm --- pilot/server/chat_adapter.py | 2 +- pilot/server/llmserver.py | 99 ++++++++++++++++++++++++------------ 2 files changed, 67 insertions(+), 34 deletions(-) diff --git a/pilot/server/chat_adapter.py b/pilot/server/chat_adapter.py index ded0a1b19..805cacb3d 100644 --- a/pilot/server/chat_adapter.py +++ b/pilot/server/chat_adapter.py @@ -15,7 +15,7 @@ class BaseChatAdpter: def get_generate_stream_func(self): """Return the generate stream handler func""" pass - + llm_model_chat_adapters: List[BaseChatAdpter] = [] diff --git a/pilot/server/llmserver.py b/pilot/server/llmserver.py index 33d3d545d..fa1da5608 100644 --- a/pilot/server/llmserver.py +++ b/pilot/server/llmserver.py @@ -23,19 +23,67 @@ from pilot.model.inference import generate_output, get_embeddings from pilot.model.loader import ModelLoader from pilot.configs.model_config import * from pilot.configs.config import Config +from pilot.server.chat_adapter import get_llm_chat_adapter CFG = Config() model_path = LLM_MODEL_CONFIG[CFG.LLM_MODEL] - -ml = ModelLoader(model_path=model_path) -model, tokenizer = ml.loader(num_gpus=1, load_8bit=ISLOAD_8BIT, debug=ISDEBUG) +print(model_path) class ModelWorker: - def __init__(self): - pass - # TODO + def __init__(self, model_path, model_name, device, num_gpus=1): + + if model_path.endswith("/"): + model_path = model_path[:-1] + self.model_name = model_name or model_path.split("/")[-1] + self.device = device + + self.ml = ModelLoader(model_path=model_path) + self.model, self.tokenizer = self.ml.loader(num_gpus, load_8bit=ISLOAD_8BIT, debug=ISDEBUG) + + if hasattr(self.model.config, "max_sequence_length"): + self.context_len = self.model.config.max_sequence_length + elif hasattr(self.model.config, "max_position_embeddings"): + self.context_len = self.model.config.max_position_embeddings + + else: + self.context_len = 2048 + + self.llm_chat_adapter = get_llm_chat_adapter(model_path) + self.generate_stream_func = self.llm_chat_adapter.get_generate_stream_func() + + def get_queue_length(self): + if model_semaphore is None or model_semaphore._value is None or model_semaphore._waiters is None: + return 0 + else: + CFG.LIMIT_MODEL_CONCURRENCY - model_semaphore._value + len(model_semaphore._waiters) + + def generate_stream_gate(self, params): + try: + for output in self.generate_stream_func( + self.model, + self.tokenizer, + params, + DEVICE, + CFG.MAX_POSITION_EMBEDDINGS + ): + print("output: ", output) + ret = { + "text": output, + "error_code": 0, + } + yield json.dumps(ret).encode() + b"\0" + + except torch.cuda.CudaError: + ret = { + "text": "**GPU OutOfMemory, Please Refresh.**", + "error_code": 0 + } + yield json.dumps(ret).encode() + b"\0" + + def get_embeddings(self, prompt): + return get_embeddings(self.model, self.tokenizer, prompt) app = FastAPI() @@ -60,41 +108,17 @@ def release_model_semaphore(): model_semaphore.release() -def generate_stream_gate(params): - try: - for output in generate_stream( - model, - tokenizer, - params, - DEVICE, - CFG.MAX_POSITION_EMBEDDINGS, - ): - print("output: ", output) - ret = { - "text": output, - "error_code": 0, - } - yield json.dumps(ret).encode() + b"\0" - except torch.cuda.CudaError: - ret = { - "text": "**GPU OutOfMemory, Please Refresh.**", - "error_code": 0 - } - yield json.dumps(ret).encode() + b"\0" - - @app.post("/generate_stream") async def api_generate_stream(request: Request): global model_semaphore, global_counter global_counter += 1 params = await request.json() - print(model, tokenizer, params, DEVICE) if model_semaphore is None: model_semaphore = asyncio.Semaphore(CFG.LIMIT_MODEL_CONCURRENCY) await model_semaphore.acquire() - generator = generate_stream_gate(params) + generator = worker.generate_stream_gate(params) background_tasks = BackgroundTasks() background_tasks.add_task(release_model_semaphore) return StreamingResponse(generator, background=background_tasks) @@ -110,7 +134,7 @@ def generate(prompt_request: PromptRequest): response = [] rsp_str = "" - output = generate_stream_gate(params) + output = worker.generate_stream_gate(params) for rsp in output: # rsp = rsp.decode("utf-8") rsp_str = str(rsp, "utf-8") @@ -124,9 +148,18 @@ def generate(prompt_request: PromptRequest): def embeddings(prompt_request: EmbeddingRequest): params = {"prompt": prompt_request.prompt} print("Received prompt: ", params["prompt"]) - output = get_embeddings(model, tokenizer, params["prompt"]) + output = worker.get_embeddings(params["prompt"]) return {"response": [float(x) for x in output]} if __name__ == "__main__": + + + worker = ModelWorker( + model_path=model_path, + model_name=CFG.LLM_MODEL, + device=DEVICE, + num_gpus=1 + ) + uvicorn.run(app, host="0.0.0.0", port=CFG.MODEL_PORT, log_level="info") \ No newline at end of file From 42b76979a3a22d4b95133513eed8c51648ab9e29 Mon Sep 17 00:00:00 2001 From: csunny Date: Sun, 21 May 2023 11:22:56 +0800 Subject: [PATCH 36/66] llms: multi model support --- examples/embdserver.py | 4 ++-- pilot/server/llmserver.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/embdserver.py b/examples/embdserver.py index 79140ba66..bb0016f00 100644 --- a/examples/embdserver.py +++ b/examples/embdserver.py @@ -12,7 +12,7 @@ from pilot.conversation import conv_qa_prompt_template, conv_templates from langchain.prompts import PromptTemplate -vicuna_stream_path = "generate_stream" +llmstream_stream_path = "generate_stream" CFG = Config() @@ -44,7 +44,7 @@ def generate(query): } response = requests.post( - url=urljoin(CFG.MODEL_SERVER, vicuna_stream_path), data=json.dumps(params) + url=urljoin(CFG.MODEL_SERVER, llmstream_stream_path), data=json.dumps(params) ) skip_echo_len = len(params["prompt"]) + 1 - params["prompt"].count("") * 3 diff --git a/pilot/server/llmserver.py b/pilot/server/llmserver.py index fa1da5608..79b3450d3 100644 --- a/pilot/server/llmserver.py +++ b/pilot/server/llmserver.py @@ -27,8 +27,6 @@ from pilot.server.chat_adapter import get_llm_chat_adapter CFG = Config() -model_path = LLM_MODEL_CONFIG[CFG.LLM_MODEL] -print(model_path) class ModelWorker: @@ -154,6 +152,8 @@ def embeddings(prompt_request: EmbeddingRequest): if __name__ == "__main__": + model_path = LLM_MODEL_CONFIG[CFG.LLM_MODEL] + print(model_path) worker = ModelWorker( model_path=model_path, From 7b454d88670b4f75c799261fe8b3301105631b97 Mon Sep 17 00:00:00 2001 From: csunny Date: Sun, 21 May 2023 14:08:18 +0800 Subject: [PATCH 37/66] llms: add chatglm model --- examples/embdserver.py | 35 ++++++++++++++++++++++++----------- pilot/model/chatglm_llm.py | 14 ++++++++++---- pilot/server/webserver.py | 10 +++++++++- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/examples/embdserver.py b/examples/embdserver.py index bb0016f00..b8525fe15 100644 --- a/examples/embdserver.py +++ b/examples/embdserver.py @@ -5,8 +5,15 @@ import requests import json import time import uuid +import os +import sys from urllib.parse import urljoin import gradio as gr + +ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(ROOT_PATH) + + from pilot.configs.config import Config from pilot.conversation import conv_qa_prompt_template, conv_templates from langchain.prompts import PromptTemplate @@ -21,24 +28,24 @@ def generate(query): template_name = "conv_one_shot" state = conv_templates[template_name].copy() - pt = PromptTemplate( - template=conv_qa_prompt_template, - input_variables=["context", "question"] - ) + # pt = PromptTemplate( + # template=conv_qa_prompt_template, + # input_variables=["context", "question"] + # ) - result = pt.format(context="This page covers how to use the Chroma ecosystem within LangChain. It is broken into two parts: installation and setup, and then references to specific Chroma wrappers.", - question=query) + # result = pt.format(context="This page covers how to use the Chroma ecosystem within LangChain. It is broken into two parts: installation and setup, and then references to specific Chroma wrappers.", + # question=query) - print(result) + # print(result) - state.append_message(state.roles[0], result) + state.append_message(state.roles[0], query) state.append_message(state.roles[1], None) prompt = state.get_prompt() params = { - "model": "vicuna-13b", + "model": "chatglm-6b", "prompt": prompt, - "temperature": 0.7, + "temperature": 1.0, "max_new_tokens": 1024, "stop": "###" } @@ -48,11 +55,17 @@ def generate(query): ) skip_echo_len = len(params["prompt"]) + 1 - params["prompt"].count("") * 3 + for chunk in response.iter_lines(decode_unicode=False, delimiter=b"\0"): + if chunk: data = json.loads(chunk.decode()) if data["error_code"] == 0: - output = data["text"][skip_echo_len:].strip() + + if "vicuna" in CFG.LLM_MODEL: + output = data["text"][skip_echo_len:].strip() + else: + output = data["text"].strip() state.messages[-1][-1] = output + "▌" yield(output) diff --git a/pilot/model/chatglm_llm.py b/pilot/model/chatglm_llm.py index ef54e92d7..656252785 100644 --- a/pilot/model/chatglm_llm.py +++ b/pilot/model/chatglm_llm.py @@ -7,10 +7,11 @@ import torch def chatglm_generate_stream(model, tokenizer, params, device, context_len=2048, stream_interval=2): """Generate text using chatglm model's chat api """ - messages = params["prompt"] + prompt = params["prompt"] max_new_tokens = int(params.get("max_new_tokens", 256)) temperature = float(params.get("temperature", 1.0)) top_p = float(params.get("top_p", 1.0)) + stop = params.get("stop", "###") echo = params.get("echo", True) generate_kwargs = { @@ -23,11 +24,16 @@ def chatglm_generate_stream(model, tokenizer, params, device, context_len=2048, if temperature > 1e-5: generate_kwargs["temperature"] = temperature + # TODO, Fix this hist = [] - for i in range(0, len(messages) - 2, 2): - hist.append(messages[i][1], messages[i + 1][1]) - query = messages[-2][1] + messages = prompt.split(stop) + + # Add history chat to hist for model. + for i in range(1, len(messages) - 2, 2): + hist.append((messages[i].split(":")[1], messages[i+1].split(":")[1])) + + query = messages[-2].split(":")[1] output = "" i = 0 for i, (response, new_hist) in enumerate(model.stream_chat(tokenizer, query, hist, **generate_kwargs)): diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index ea8d8fc6d..2dd2ba9e0 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -364,8 +364,16 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re for chunk in response.iter_lines(decode_unicode=False, delimiter=b"\0"): if chunk: data = json.loads(chunk.decode()) + + """ TODO Multi mode output handler, rewrite this for multi model, use adapter mode. + """ if data["error_code"] == 0: - output = data["text"][skip_echo_len:].strip() + + if "vicuna" in CFG.LLM_MODEL: + output = data["text"][skip_echo_len:].strip() + else: + output = data["text"].strip() + output = post_process_code(output) state.messages[-1][-1] = output + "▌" yield (state, state.to_gradio_chatbot()) + (disable_btn,) * 5 From ce728200854a4eb3159c2eabdaa5d5c2c9003239 Mon Sep 17 00:00:00 2001 From: csunny Date: Sun, 21 May 2023 14:48:54 +0800 Subject: [PATCH 38/66] llms: add mps support --- pilot/configs/model_config.py | 2 +- pilot/model/llm/monkey_patch.py | 125 ++++++++++++++++++++++++++++++++ pilot/model/loader.py | 67 +++++++++++++---- pilot/server/llmserver.py | 2 +- 4 files changed, 181 insertions(+), 15 deletions(-) create mode 100644 pilot/model/llm/monkey_patch.py diff --git a/pilot/configs/model_config.py b/pilot/configs/model_config.py index 265007ae5..3199d0004 100644 --- a/pilot/configs/model_config.py +++ b/pilot/configs/model_config.py @@ -16,7 +16,7 @@ DATA_DIR = os.path.join(PILOT_PATH, "data") nltk.data.path = [os.path.join(PILOT_PATH, "nltk_data")] + nltk.data.path -DEVICE = "cuda" if torch.cuda.is_available() else "cpu" +DEVICE = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" LLM_MODEL_CONFIG = { "flan-t5-base": os.path.join(MODEL_PATH, "flan-t5-base"), "vicuna-13b": os.path.join(MODEL_PATH, "vicuna-13b"), diff --git a/pilot/model/llm/monkey_patch.py b/pilot/model/llm/monkey_patch.py new file mode 100644 index 000000000..a50481281 --- /dev/null +++ b/pilot/model/llm/monkey_patch.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- + +import math +from typing import Optional, Tuple + +import torch +from torch import nn +import transformers + + +def rotate_half(x): + """Rotates half the hidden dims of the input.""" + x1 = x[..., : x.shape[-1] // 2].clone() + x2 = x[..., x.shape[-1] // 2 :].clone() + return torch.cat((-x2, x1), dim=-1) + + +def apply_rotary_pos_emb(q, k, cos, sin, position_ids): + gather_indices = position_ids[:, None, :, None] # [bs, 1, seq_len, 1] + gather_indices = gather_indices.repeat(1, cos.shape[1], 1, cos.shape[3]) + cos = torch.gather(cos.repeat(gather_indices.shape[0], 1, 1, 1), 2, gather_indices) + sin = torch.gather(sin.repeat(gather_indices.shape[0], 1, 1, 1), 2, gather_indices) + q_embed = (q * cos) + (rotate_half(q) * sin) + k_embed = (k * cos) + (rotate_half(k) * sin) + return q_embed, k_embed + + +def forward( + self, + hidden_states: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_value: Optional[Tuple[torch.Tensor]] = None, + output_attentions: bool = False, + use_cache: bool = False, +) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]: + bsz, q_len, _ = hidden_states.size() + + query_states = ( + self.q_proj(hidden_states) + .view(bsz, q_len, self.num_heads, self.head_dim) + .transpose(1, 2) + ) + key_states = ( + self.k_proj(hidden_states) + .view(bsz, q_len, self.num_heads, self.head_dim) + .transpose(1, 2) + ) + value_states = ( + self.v_proj(hidden_states) + .view(bsz, q_len, self.num_heads, self.head_dim) + .transpose(1, 2) + ) + + kv_seq_len = key_states.shape[-2] + if past_key_value is not None: + kv_seq_len += past_key_value[0].shape[-2] + cos, sin = self.rotary_emb(value_states, seq_len=kv_seq_len) + query_states, key_states = apply_rotary_pos_emb( + query_states, key_states, cos, sin, position_ids + ) + # [bsz, nh, t, hd] + + if past_key_value is not None: + # reuse k, v, self_attention + key_states = torch.cat([past_key_value[0], key_states], dim=2) + value_states = torch.cat([past_key_value[1], value_states], dim=2) + + past_key_value = (key_states, value_states) if use_cache else None + + attn_weights = torch.matmul(query_states, key_states.transpose(2, 3)) / math.sqrt( + self.head_dim + ) + + if attn_weights.size() != (bsz, self.num_heads, q_len, kv_seq_len): + raise ValueError( + f"Attention weights should be of size {(bsz * self.num_heads, q_len, kv_seq_len)}, but is" + f" {attn_weights.size()}" + ) + + if attention_mask is not None: + if attention_mask.size() != (bsz, 1, q_len, kv_seq_len): + raise ValueError( + f"Attention mask should be of size {(bsz, 1, q_len, kv_seq_len)}, but is {attention_mask.size()}" + ) + attn_weights = attn_weights + attention_mask + attn_weights = torch.max( + attn_weights, torch.tensor(torch.finfo(attn_weights.dtype).min) + ) + + # upcast attention to fp32 + attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to( + query_states.dtype + ) + attn_output = torch.matmul(attn_weights, value_states) + + if attn_output.size() != (bsz, self.num_heads, q_len, self.head_dim): + raise ValueError( + f"`attn_output` should be of size {(bsz, self.num_heads, q_len, self.head_dim)}, but is" + f" {attn_output.size()}" + ) + + attn_output = attn_output.transpose(1, 2) + attn_output = attn_output.reshape(bsz, q_len, self.hidden_size) + + attn_output = self.o_proj(attn_output) + + if not output_attentions: + attn_weights = None + + return attn_output, attn_weights, past_key_value + + +def replace_llama_attn_with_non_inplace_operations(): + """Avoid bugs in mps backend by not using in-place operations.""" + transformers.models.llama.modeling_llama.LlamaAttention.forward = forward + +import transformers + + + +def replace_llama_attn_with_non_inplace_operations(): + """Avoid bugs in mps backend by not using in-place operations.""" + transformers.models.llama.modeling_llama.LlamaAttention.forward = forward diff --git a/pilot/model/loader.py b/pilot/model/loader.py index 66d9c733e..1c32939ec 100644 --- a/pilot/model/loader.py +++ b/pilot/model/loader.py @@ -2,11 +2,39 @@ # -*- coding: utf-8 -*- import torch +import sys import warnings from pilot.singleton import Singleton - +from typing import Optional from pilot.model.compression import compress_module from pilot.model.adapter import get_llm_model_adapter +from pilot.utils import get_gpu_memory +from pilot.model.llm.monkey_patch import replace_llama_attn_with_non_inplace_operations + +def raise_warning_for_incompatible_cpu_offloading_configuration( + device: str, load_8bit: bool, cpu_offloading: bool +): + if cpu_offloading: + if not load_8bit: + warnings.warn( + "The cpu-offloading feature can only be used while also using 8-bit-quantization.\n" + "Use '--load-8bit' to enable 8-bit-quantization\n" + "Continuing without cpu-offloading enabled\n" + ) + return False + if not "linux" in sys.platform: + warnings.warn( + "CPU-offloading is only supported on linux-systems due to the limited compatability with the bitsandbytes-package\n" + "Continuing without cpu-offloading enabled\n" + ) + return False + if device != "cuda": + warnings.warn( + "CPU-offloading is only enabled when using CUDA-devices\n" + "Continuing without cpu-offloading enabled\n" + ) + return False + return cpu_offloading class ModelLoader(metaclass=Singleton): @@ -30,26 +58,39 @@ class ModelLoader(metaclass=Singleton): } # TODO multi gpu support - def loader(self, num_gpus, load_8bit=False, debug=False): + def loader(self, num_gpus, load_8bit=False, debug=False, cpu_offloading=False, max_gpu_memory: Optional[str]=None): + + cpu_offloading(self.device, load_8bit, cpu_offloading) + if self.device == "cpu": - kwargs = {} + kwargs = {"torch_dtype": torch.float32} elif self.device == "cuda": kwargs = {"torch_dtype": torch.float16} - if num_gpus == "auto": + num_gpus = int(num_gpus) + + if num_gpus != 1: kwargs["device_map"] = "auto" + if max_gpu_memory is None: + kwargs["device_map"] = "sequential" + + available_gpu_memory = get_gpu_memory(num_gpus) + kwargs["max_memory"] = { + i: str(int(available_gpu_memory[i] * 0.85)) + "GiB" + for i in range(num_gpus) + } + else: - num_gpus = int(num_gpus) - if num_gpus != 1: - kwargs.update({ - "device_map": "auto", - "max_memory": {i: "13GiB" for i in range(num_gpus)}, - }) + kwargs["max_memory"] = {i: max_gpu_memory for i in range(num_gpus)} + + elif self.device == "mps": + kwargs = kwargs = {"torch_dtype": torch.float16} + replace_llama_attn_with_non_inplace_operations() else: - # Todo Support mps for practise raise ValueError(f"Invalid device: {self.device}") - + # TODO when cpu loading, need use quantization config + llm_adapter = get_llm_model_adapter(self.model_path) model, tokenizer = llm_adapter.loader(self.model_path, kwargs) @@ -61,7 +102,7 @@ class ModelLoader(metaclass=Singleton): else: compress_module(model, self.device) - if (self.device == "cuda" and num_gpus == 1): + if (self.device == "cuda" and num_gpus == 1 and not cpu_offloading) or self.device == "mps": model.to(self.device) if debug: diff --git a/pilot/server/llmserver.py b/pilot/server/llmserver.py index 79b3450d3..2dcdce2ca 100644 --- a/pilot/server/llmserver.py +++ b/pilot/server/llmserver.py @@ -153,7 +153,7 @@ def embeddings(prompt_request: EmbeddingRequest): if __name__ == "__main__": model_path = LLM_MODEL_CONFIG[CFG.LLM_MODEL] - print(model_path) + print(model_path, DEVICE) worker = ModelWorker( model_path=model_path, From f52c7523b5c2d4ee377191ebb5f70955c401cf61 Mon Sep 17 00:00:00 2001 From: csunny Date: Sun, 21 May 2023 14:54:16 +0800 Subject: [PATCH 39/66] llms: fix --- pilot/model/loader.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pilot/model/loader.py b/pilot/model/loader.py index 1c32939ec..531080314 100644 --- a/pilot/model/loader.py +++ b/pilot/model/loader.py @@ -59,8 +59,6 @@ class ModelLoader(metaclass=Singleton): # TODO multi gpu support def loader(self, num_gpus, load_8bit=False, debug=False, cpu_offloading=False, max_gpu_memory: Optional[str]=None): - - cpu_offloading(self.device, load_8bit, cpu_offloading) if self.device == "cpu": kwargs = {"torch_dtype": torch.float32} From 89970bd71c510f32aa120e62a36b0380dda304da Mon Sep 17 00:00:00 2001 From: csunny Date: Sun, 21 May 2023 16:05:53 +0800 Subject: [PATCH 40/66] llms: add cpu support --- pilot/model/adapter.py | 19 ++++++++++++++----- pilot/server/llmserver.py | 1 + requirements.txt | 1 + 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pilot/model/adapter.py b/pilot/model/adapter.py index bf0e291ce..be8980726 100644 --- a/pilot/model/adapter.py +++ b/pilot/model/adapter.py @@ -9,6 +9,8 @@ from transformers import ( AutoModel ) +from pilot.configs.model_config import DEVICE + class BaseLLMAdaper: """The Base class for multi model, in our project. We will support those model, which performance resemble ChatGPT """ @@ -61,13 +63,20 @@ class ChatGLMAdapater(BaseLLMAdaper): """LLM Adatpter for THUDM/chatglm-6b""" def match(self, model_path: str): return "chatglm" in model_path - + def loader(self, model_path: str, from_pretrained_kwargs: dict): tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) - model = AutoModel.from_pretrained( - model_path, trust_remote_code=True, **from_pretrained_kwargs - ).half().cuda() - return model, tokenizer + + if DEVICE != "cuda": + model = AutoModel.from_pretrained( + model_path, trust_remote_code=True, **from_pretrained_kwargs + ).float() + return model, tokenizer + else: + model = AutoModel.from_pretrained( + model_path, trust_remote_code=True, **from_pretrained_kwargs + ).half().cuda() + return model, tokenizer class CodeGenAdapter(BaseLLMAdaper): pass diff --git a/pilot/server/llmserver.py b/pilot/server/llmserver.py index 2dcdce2ca..bc227d518 100644 --- a/pilot/server/llmserver.py +++ b/pilot/server/llmserver.py @@ -155,6 +155,7 @@ if __name__ == "__main__": model_path = LLM_MODEL_CONFIG[CFG.LLM_MODEL] print(model_path, DEVICE) + worker = ModelWorker( model_path=model_path, model_name=CFG.LLM_MODEL, diff --git a/requirements.txt b/requirements.txt index 410d3129c..f82d2e2a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,6 +42,7 @@ tenacity==8.2.2 peft pycocoevalcap sentence-transformers +cpm_kernels umap-learn notebook gradio==3.23 From 604d269797bfd6bf81eea7bc416a776b8d5f2a09 Mon Sep 17 00:00:00 2001 From: csunny Date: Sun, 21 May 2023 16:11:52 +0800 Subject: [PATCH 41/66] fix and update --- pilot/model/chatglm_llm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pilot/model/chatglm_llm.py b/pilot/model/chatglm_llm.py index 656252785..f8279be7f 100644 --- a/pilot/model/chatglm_llm.py +++ b/pilot/model/chatglm_llm.py @@ -8,16 +8,15 @@ def chatglm_generate_stream(model, tokenizer, params, device, context_len=2048, """Generate text using chatglm model's chat api """ prompt = params["prompt"] - max_new_tokens = int(params.get("max_new_tokens", 256)) temperature = float(params.get("temperature", 1.0)) top_p = float(params.get("top_p", 1.0)) stop = params.get("stop", "###") echo = params.get("echo", True) generate_kwargs = { - "max_new_tokens": max_new_tokens, "do_sample": True if temperature > 1e-5 else False, "top_p": top_p, + "repetition_penalty": 1.0, "logits_processor": None } @@ -34,6 +33,7 @@ def chatglm_generate_stream(model, tokenizer, params, device, context_len=2048, hist.append((messages[i].split(":")[1], messages[i+1].split(":")[1])) query = messages[-2].split(":")[1] + print("Query Message: ", query) output = "" i = 0 for i, (response, new_hist) in enumerate(model.stream_chat(tokenizer, query, hist, **generate_kwargs)): From 5ec1f413b637489eaa67eb4ff7091566c663649f Mon Sep 17 00:00:00 2001 From: csunny Date: Sun, 21 May 2023 16:18:33 +0800 Subject: [PATCH 42/66] update --- pilot/model/chatglm_llm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pilot/model/chatglm_llm.py b/pilot/model/chatglm_llm.py index f8279be7f..b0a3c8296 100644 --- a/pilot/model/chatglm_llm.py +++ b/pilot/model/chatglm_llm.py @@ -11,7 +11,7 @@ def chatglm_generate_stream(model, tokenizer, params, device, context_len=2048, temperature = float(params.get("temperature", 1.0)) top_p = float(params.get("top_p", 1.0)) stop = params.get("stop", "###") - echo = params.get("echo", True) + echo = params.get("echo", False) generate_kwargs = { "do_sample": True if temperature > 1e-5 else False, From 6747d877ccf75e758eac9c9c6a005ecd6ed7c460 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Sun, 21 May 2023 16:29:00 +0800 Subject: [PATCH 43/66] feature:add milvus store --- pilot/configs/model_config.py | 2 + pilot/server/webserver.py | 15 +- pilot/source_embedding/knowledge_embedding.py | 49 +++-- pilot/source_embedding/source_embedding.py | 38 +++- pilot/vector_store/milvus_store.py | 206 ++++++++++++++++-- requirements.txt | 1 + tools/knowlege_init.py | 24 +- 7 files changed, 277 insertions(+), 58 deletions(-) diff --git a/pilot/configs/model_config.py b/pilot/configs/model_config.py index da68ab332..0f9cef937 100644 --- a/pilot/configs/model_config.py +++ b/pilot/configs/model_config.py @@ -48,3 +48,5 @@ DB_SETTINGS = { VS_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "vs_store") KNOWLEDGE_UPLOAD_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") KNOWLEDGE_CHUNK_SPLIT_SIZE = 100 +VECTOR_STORE_TYPE = "milvus" +VECTOR_STORE_CONFIG = {"url": "127.0.0.1", "port": "19530"} diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index 25940a437..bcf8f6385 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -19,7 +19,8 @@ from langchain import PromptTemplate ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(ROOT_PATH) -from pilot.configs.model_config import DB_SETTINGS, KNOWLEDGE_UPLOAD_ROOT_PATH, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K +from pilot.configs.model_config import DB_SETTINGS, KNOWLEDGE_UPLOAD_ROOT_PATH, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K, \ + VECTOR_STORE_CONFIG from pilot.server.vectordb_qa import KnownLedgeBaseQA from pilot.connections.mysql import MySQLOperator from pilot.source_embedding.knowledge_embedding import KnowledgeEmbedding @@ -267,12 +268,16 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re skip_echo_len = len(prompt.replace("", " ")) + 1 if mode == conversation_types["custome"] and not db_selector: - persist_dir = os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, vector_store_name["vs_name"] + ".vectordb") - print("vector store path: ", persist_dir) + # persist_dir = os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, vector_store_name["vs_name"]) + print("vector store type: ", VECTOR_STORE_CONFIG) + print("vector store name: ", vector_store_name["vs_name"]) + vector_store_config = VECTOR_STORE_CONFIG + vector_store_config["vector_store_name"] = vector_store_name["vs_name"] + vector_store_config["text_field"] = "content" + vector_store_config["vector_store_path"] = KNOWLEDGE_UPLOAD_ROOT_PATH knowledge_embedding_client = KnowledgeEmbedding(file_path="", model_name=LLM_MODEL_CONFIG["text2vec"], local_persist=False, - vector_store_config={"vector_store_name": vector_store_name["vs_name"], - "vector_store_path": KNOWLEDGE_UPLOAD_ROOT_PATH}) + vector_store_config=vector_store_config) query = state.messages[-2][1] docs = knowledge_embedding_client.similar_search(query, VECTOR_SEARCH_TOP_K) context = [d.page_content for d in docs] diff --git a/pilot/source_embedding/knowledge_embedding.py b/pilot/source_embedding/knowledge_embedding.py index 08d962908..63d6c2121 100644 --- a/pilot/source_embedding/knowledge_embedding.py +++ b/pilot/source_embedding/knowledge_embedding.py @@ -1,7 +1,7 @@ import os from bs4 import BeautifulSoup -from langchain.document_loaders import PyPDFLoader, TextLoader, markdown +from langchain.document_loaders import TextLoader, markdown from langchain.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import Chroma from pilot.configs.model_config import DATASETS_DIR, KNOWLEDGE_CHUNK_SPLIT_SIZE @@ -12,6 +12,7 @@ from pilot.source_embedding.pdf_embedding import PDFEmbedding import markdown from pilot.source_embedding.pdf_loader import UnstructuredPaddlePDFLoader +from pilot.vector_store.milvus_store import MilvusStore class KnowledgeEmbedding: @@ -20,7 +21,7 @@ class KnowledgeEmbedding: self.file_path = file_path self.model_name = model_name self.vector_store_config = vector_store_config - self.vector_store_type = "default" + self.file_type = "default" self.embeddings = HuggingFaceEmbeddings(model_name=self.model_name) self.local_persist = local_persist if not self.local_persist: @@ -42,7 +43,7 @@ class KnowledgeEmbedding: elif self.file_path.endswith(".csv"): embedding = CSVEmbedding(file_path=self.file_path, model_name=self.model_name, vector_store_config=self.vector_store_config) - elif self.vector_store_type == "default": + elif self.file_type == "default": embedding = MarkdownEmbedding(file_path=self.file_path, model_name=self.model_name, vector_store_config=self.vector_store_config) return embedding @@ -52,25 +53,33 @@ class KnowledgeEmbedding: def knowledge_persist_initialization(self, append_mode): vector_name = self.vector_store_config["vector_store_name"] - persist_dir = os.path.join(self.vector_store_config["vector_store_path"], vector_name + ".vectordb") - print("vector db path: ", persist_dir) - if os.path.exists(persist_dir): - if append_mode: - print("append knowledge return vector store") - new_documents = self._load_knownlege(self.file_path) - vector_store = Chroma.from_documents(documents=new_documents, + documents = self._load_knownlege(self.file_path) + if self.vector_store_config["vector_store_type"] == "Chroma": + persist_dir = os.path.join(self.vector_store_config["vector_store_path"], vector_name + ".vectordb") + print("vector db path: ", persist_dir) + if os.path.exists(persist_dir): + if append_mode: + print("append knowledge return vector store") + new_documents = self._load_knownlege(self.file_path) + vector_store = Chroma.from_documents(documents=new_documents, + embedding=self.embeddings, + persist_directory=persist_dir) + else: + print("directly return vector store") + vector_store = Chroma(persist_directory=persist_dir, embedding_function=self.embeddings) + else: + print(vector_name + " is new vector store, knowledge begin load...") + vector_store = Chroma.from_documents(documents=documents, embedding=self.embeddings, persist_directory=persist_dir) - else: - print("directly return vector store") - vector_store = Chroma(persist_directory=persist_dir, embedding_function=self.embeddings) - else: - print(vector_name + " is new vector store, knowledge begin load...") - documents = self._load_knownlege(self.file_path) - vector_store = Chroma.from_documents(documents=documents, - embedding=self.embeddings, - persist_directory=persist_dir) - vector_store.persist() + vector_store.persist() + + elif self.vector_store_config["vector_store_type"] == "milvus": + vector_store = MilvusStore({"url": self.vector_store_config["url"], + "port": self.vector_store_config["port"], + "embedding": self.embeddings}) + vector_store.init_schema_and_load(vector_name, documents) + return vector_store def _load_knownlege(self, path): diff --git a/pilot/source_embedding/source_embedding.py b/pilot/source_embedding/source_embedding.py index 66bc97b6d..a253e4d78 100644 --- a/pilot/source_embedding/source_embedding.py +++ b/pilot/source_embedding/source_embedding.py @@ -5,9 +5,14 @@ from abc import ABC, abstractmethod from langchain.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import Chroma +from langchain.vectorstores import Milvus from typing import List, Optional, Dict + +from pilot.configs.model_config import VECTOR_STORE_TYPE, VECTOR_STORE_CONFIG +from pilot.vector_store.milvus_store import MilvusStore + registered_methods = [] @@ -29,9 +34,20 @@ class SourceEmbedding(ABC): self.vector_store_config = vector_store_config self.embedding_args = embedding_args self.embeddings = HuggingFaceEmbeddings(model_name=self.model_name) - persist_dir = os.path.join(self.vector_store_config["vector_store_path"], - self.vector_store_config["vector_store_name"] + ".vectordb") - self.vector_store_client = Chroma(persist_directory=persist_dir, embedding_function=self.embeddings) + + if VECTOR_STORE_TYPE == "milvus": + print(VECTOR_STORE_CONFIG) + if self.vector_store_config.get("text_field") is None: + self.vector_store_client = MilvusStore({"url": VECTOR_STORE_CONFIG["url"], + "port": VECTOR_STORE_CONFIG["port"], + "embedding": self.embeddings}) + else: + self.vector_store_client = Milvus(embedding_function=self.embeddings, collection_name=self.vector_store_config["vector_store_name"], text_field="content", + connection_args={"host": VECTOR_STORE_CONFIG["url"], "port": VECTOR_STORE_CONFIG["port"]}) + else: + persist_dir = os.path.join(self.vector_store_config["vector_store_path"], + self.vector_store_config["vector_store_name"] + ".vectordb") + self.vector_store_client = Chroma(persist_directory=persist_dir, embedding_function=self.embeddings) @abstractmethod @register @@ -54,10 +70,18 @@ class SourceEmbedding(ABC): @register def index_to_store(self, docs): """index to vector store""" - persist_dir = os.path.join(self.vector_store_config["vector_store_path"], - self.vector_store_config["vector_store_name"] + ".vectordb") - self.vector_store = Chroma.from_documents(docs, self.embeddings, persist_directory=persist_dir) - self.vector_store.persist() + + if VECTOR_STORE_TYPE == "chroma": + persist_dir = os.path.join(self.vector_store_config["vector_store_path"], + self.vector_store_config["vector_store_name"] + ".vectordb") + self.vector_store = Chroma.from_documents(docs, self.embeddings, persist_directory=persist_dir) + self.vector_store.persist() + + elif VECTOR_STORE_TYPE == "milvus": + self.vector_store = MilvusStore({"url": VECTOR_STORE_CONFIG["url"], + "port": VECTOR_STORE_CONFIG["port"], + "embedding": self.embeddings}) + self.vector_store.init_schema_and_load(self.vector_store_config["vector_store_name"], docs) @register def similar_search(self, doc, topk): diff --git a/pilot/vector_store/milvus_store.py b/pilot/vector_store/milvus_store.py index eda0b4e38..6b06dcf00 100644 --- a/pilot/vector_store/milvus_store.py +++ b/pilot/vector_store/milvus_store.py @@ -1,31 +1,35 @@ +from typing import List, Optional, Iterable + from langchain.embeddings import HuggingFaceEmbeddings from pymilvus import DataType, FieldSchema, CollectionSchema, connections, Collection -from pilot.configs.model_config import LLM_MODEL_CONFIG from pilot.vector_store.vector_store_base import VectorStoreBase class MilvusStore(VectorStoreBase): - def __init__(self, cfg: {}) -> None: - """Construct a milvus memory storage connection. + def __init__(self, ctx: {}) -> None: + """init a milvus storage connection. Args: - cfg (Config): MilvusStore global config. + ctx ({}): MilvusStore global config. """ # self.configure(cfg) connect_kwargs = {} self.uri = None - self.uri = cfg["url"] - self.port = cfg["port"] - self.username = cfg.get("username", None) - self.password = cfg.get("password", None) - self.collection_name = cfg["table_name"] - self.password = cfg.get("secure", None) + self.uri = ctx["url"] + self.port = ctx["port"] + self.username = ctx.get("username", None) + self.password = ctx.get("password", None) + self.collection_name = ctx.get("table_name", None) + self.secure = ctx.get("secure", None) + self.model_config = ctx.get("model_config", None) + self.embedding = ctx.get("embedding", None) + self.fields = [] # use HNSW by default. self.index_params = { - "metric_type": "IP", + "metric_type": "L2", "index_type": "HNSW", "params": {"M": 8, "efConstruction": 64}, } @@ -39,20 +43,144 @@ class MilvusStore(VectorStoreBase): connect_kwargs["password"] = self.password connections.connect( - **connect_kwargs, host=self.uri or "127.0.0.1", port=self.port or "19530", alias="default" # secure=self.secure, ) + if self.collection_name is not None: + self.col = Collection(self.collection_name) + schema = self.col.schema + for x in schema.fields: + self.fields.append(x.name) + if x.auto_id: + self.fields.remove(x.name) + if x.is_primary: + self.primary_field = x.name + if x.dtype == DataType.FLOAT_VECTOR or x.dtype == DataType.BINARY_VECTOR: + self.vector_field = x.name - self.init_schema() + + # self.init_schema() + # self.init_collection_schema() + + def init_schema_and_load(self, vector_name, documents): + """Create a Milvus collection, indexes it with HNSW, load document. + Args: + documents (List[str]): Text to insert. + vector_name (Embeddings): your collection name. + Returns: + VectorStore: The MilvusStore vector store. + """ + try: + from pymilvus import ( + Collection, + CollectionSchema, + DataType, + FieldSchema, + connections, + ) + from pymilvus.orm.types import infer_dtype_bydata + except ImportError: + raise ValueError( + "Could not import pymilvus python package. " + "Please install it with `pip install pymilvus`." + ) + # Connect to Milvus instance + if not connections.has_connection("default"): + connections.connect( + host=self.uri or "127.0.0.1", + port=self.port or "19530", + alias="default" + # secure=self.secure, + ) + texts = [d.page_content for d in documents] + metadatas = [d.metadata for d in documents] + embeddings = self.embedding.embed_query(texts[0]) + dim = len(embeddings) + # Generate unique names + primary_field = "pk_id" + vector_field = "vector" + text_field = "content" + self.text_field = text_field + collection_name = vector_name + fields = [] + # Determine metadata schema + # if metadatas: + # # Check if all metadata keys line up + # key = metadatas[0].keys() + # for x in metadatas: + # if key != x.keys(): + # raise ValueError( + # "Mismatched metadata. " + # "Make sure all metadata has the same keys and datatype." + # ) + # # Create FieldSchema for each entry in singular metadata. + # for key, value in metadatas[0].items(): + # # Infer the corresponding datatype of the metadata + # dtype = infer_dtype_bydata(value) + # if dtype == DataType.UNKNOWN: + # raise ValueError(f"Unrecognized datatype for {key}.") + # elif dtype == DataType.VARCHAR: + # # Find out max length text based metadata + # max_length = 0 + # for subvalues in metadatas: + # max_length = max(max_length, len(subvalues[key])) + # fields.append( + # FieldSchema(key, DataType.VARCHAR, max_length=max_length + 1) + # ) + # else: + # fields.append(FieldSchema(key, dtype)) + + # Find out max length of texts + max_length = 0 + for y in texts: + max_length = max(max_length, len(y)) + # Create the text field + fields.append( + FieldSchema(text_field, DataType.VARCHAR, max_length=max_length + 1) + ) + # Create the primary key field + fields.append( + FieldSchema(primary_field, DataType.INT64, is_primary=True, auto_id=True) + ) + # Create the vector field + fields.append(FieldSchema(vector_field, DataType.FLOAT_VECTOR, dim=dim)) + # Create the schema for the collection + schema = CollectionSchema(fields) + # Create the collection + collection = Collection(collection_name, schema) + self.col = collection + # Index parameters for the collection + index = self.index_params + # Create the index + collection.create_index(vector_field, index) + # Create the VectorStore + # milvus = cls( + # embedding, + # kwargs.get("connection_args", {"port": 19530}), + # collection_name, + # text_field, + # ) + # Add the texts. + schema = collection.schema + for x in schema.fields: + self.fields.append(x.name) + if x.auto_id: + self.fields.remove(x.name) + if x.is_primary: + self.primary_field = x.name + if x.dtype == DataType.FLOAT_VECTOR or x.dtype == DataType.BINARY_VECTOR: + self.vector_field = x.name + self._add_texts(texts, metadatas) + + return self.collection_name def init_schema(self) -> None: """Initialize collection in milvus database.""" fields = [ FieldSchema(name="pk", dtype=DataType.INT64, is_primary=True, auto_id=True), - FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=384), + FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=self.model_config["dim"]), FieldSchema(name="raw_text", dtype=DataType.VARCHAR, max_length=65535), ] @@ -75,7 +203,7 @@ class MilvusStore(VectorStoreBase): info = self.collection.describe() self.collection.load() - def insert(self, text) -> str: + def insert(self, text, model_config) -> str: """Add an embedding of data into milvus. Args: text (str): The raw text to construct embedding index. @@ -83,10 +211,54 @@ class MilvusStore(VectorStoreBase): str: log. """ # embedding = get_ada_embedding(data) - embeddings = HuggingFaceEmbeddings(model_name=LLM_MODEL_CONFIG["sentence-transforms"]) + embeddings = HuggingFaceEmbeddings(model_name=self.model_config["model_name"]) result = self.collection.insert([embeddings.embed_documents(text), text]) _text = ( "Inserting data into memory at primary key: " f"{result.primary_keys[0]}:\n data: {text}" ) - return _text \ No newline at end of file + return _text + + def _add_texts( + self, + texts: Iterable[str], + metadatas: Optional[List[dict]] = None, + partition_name: Optional[str] = None, + timeout: Optional[int] = None, + ) -> List[str]: + """Insert text data into Milvus. + Args: + texts (Iterable[str]): The text being embedded and inserted. + metadatas (Optional[List[dict]], optional): The metadata that + corresponds to each insert. Defaults to None. + partition_name (str, optional): The partition of the collection + to insert data into. Defaults to None. + timeout: specified timeout. + + Returns: + List[str]: The resulting keys for each inserted element. + """ + insert_dict: Any = {self.text_field: list(texts)} + try: + insert_dict[self.vector_field] = self.embedding.embed_documents( + list(texts) + ) + except NotImplementedError: + insert_dict[self.vector_field] = [ + self.embedding.embed_query(x) for x in texts + ] + # Collect the metadata into the insert dict. + if len(self.fields) > 2 and metadatas is not None: + for d in metadatas: + for key, value in d.items(): + if key in self.fields: + insert_dict.setdefault(key, []).append(value) + # Convert dict to list of lists for insertion + insert_list = [insert_dict[x] for x in self.fields] + # Insert into the collection. + res = self.col.insert( + insert_list, partition_name=partition_name, timeout=timeout + ) + # Flush to make sure newly inserted is immediately searchable. + self.col.flush() + return res.primary_keys diff --git a/requirements.txt b/requirements.txt index eac927c3d..ba31d0d04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,6 +60,7 @@ gTTS==2.3.1 langchain nltk python-dotenv==1.0.0 +pymilvus # Testing dependencies pytest diff --git a/tools/knowlege_init.py b/tools/knowlege_init.py index e9ecad49a..fdc754e05 100644 --- a/tools/knowlege_init.py +++ b/tools/knowlege_init.py @@ -2,8 +2,10 @@ # -*- coding: utf-8 -*- import argparse -from pilot.configs.model_config import DATASETS_DIR, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K, \ - KNOWLEDGE_UPLOAD_ROOT_PATH +from langchain.embeddings import HuggingFaceEmbeddings +from langchain.vectorstores import Milvus + +from pilot.configs.model_config import DATASETS_DIR, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K, VECTOR_STORE_CONFIG from pilot.source_embedding.knowledge_embedding import KnowledgeEmbedding @@ -12,15 +14,15 @@ class LocalKnowledgeInit: model_name = LLM_MODEL_CONFIG["text2vec"] top_k: int = VECTOR_SEARCH_TOP_K - def __init__(self) -> None: - pass + def __init__(self, vector_store_config) -> None: + self.vector_store_config = vector_store_config - def knowledge_persist(self, file_path, vector_name, append_mode): + def knowledge_persist(self, file_path, append_mode): """ knowledge persist """ kv = KnowledgeEmbedding( file_path=file_path, model_name=LLM_MODEL_CONFIG["text2vec"], - vector_store_config= {"vector_store_name":vector_name, "vector_store_path": KNOWLEDGE_UPLOAD_ROOT_PATH}) + vector_store_config= self.vector_store_config) vector_store = kv.knowledge_persist_initialization(append_mode) return vector_store @@ -34,11 +36,15 @@ class LocalKnowledgeInit: if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--vector_name", type=str, default="default") + parser.add_argument("--vector_name", type=str, default="keting") parser.add_argument("--append", type=bool, default=False) + parser.add_argument("--store_type", type=str, default="Chroma") args = parser.parse_args() vector_name = args.vector_name append_mode = args.append - kv = LocalKnowledgeInit() - vector_store = kv.knowledge_persist(file_path=DATASETS_DIR, vector_name=vector_name, append_mode=append_mode) + store_type = args.store_type + vector_store_config = {"url": VECTOR_STORE_CONFIG["url"], "port": VECTOR_STORE_CONFIG["port"], "vector_store_name":vector_name, "vector_store_type":store_type} + print(vector_store_config) + kv = LocalKnowledgeInit(vector_store_config=vector_store_config) + vector_store = kv.knowledge_persist(file_path=DATASETS_DIR, append_mode=append_mode) print("your knowledge embedding success...") \ No newline at end of file From a3fae0bdf203a2262e9d40601cdd0dd54b1ba831 Mon Sep 17 00:00:00 2001 From: csunny Date: Sun, 21 May 2023 16:30:03 +0800 Subject: [PATCH 44/66] add chatglm support --- pilot/conversation.py | 3 +++ pilot/model/chatglm_llm.py | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pilot/conversation.py b/pilot/conversation.py index 7f526fb89..0470bc720 100644 --- a/pilot/conversation.py +++ b/pilot/conversation.py @@ -15,6 +15,9 @@ DB_SETTINGS = { "port": CFG.LOCAL_DB_PORT } +ROLE_USER = "USER" +ROLE_ASSISTANT = "Assistant" + class SeparatorStyle(Enum): SINGLE = auto() TWO = auto() diff --git a/pilot/model/chatglm_llm.py b/pilot/model/chatglm_llm.py index b0a3c8296..0f8b74efa 100644 --- a/pilot/model/chatglm_llm.py +++ b/pilot/model/chatglm_llm.py @@ -3,6 +3,8 @@ import torch +from pilot.conversation import ROLE_USER, ROLE_ASSISTANT + @torch.inference_mode() def chatglm_generate_stream(model, tokenizer, params, device, context_len=2048, stream_interval=2): @@ -30,9 +32,9 @@ def chatglm_generate_stream(model, tokenizer, params, device, context_len=2048, # Add history chat to hist for model. for i in range(1, len(messages) - 2, 2): - hist.append((messages[i].split(":")[1], messages[i+1].split(":")[1])) + hist.append((messages[i].split(ROLE_USER + ":")[1], messages[i+1].split(ROLE_ASSISTANT + ":")[1])) - query = messages[-2].split(":")[1] + query = messages[-2].split(ROLE_USER + ":")[1] print("Query Message: ", query) output = "" i = 0 From 0297fa425b8c7c34eafd1d33a255bd1055979a6c Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Sun, 21 May 2023 16:40:18 +0800 Subject: [PATCH 45/66] feature:add milvus store --- pilot/configs/model_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pilot/configs/model_config.py b/pilot/configs/model_config.py index 0f9cef937..c63187d03 100644 --- a/pilot/configs/model_config.py +++ b/pilot/configs/model_config.py @@ -48,5 +48,5 @@ DB_SETTINGS = { VS_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "vs_store") KNOWLEDGE_UPLOAD_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") KNOWLEDGE_CHUNK_SPLIT_SIZE = 100 -VECTOR_STORE_TYPE = "milvus" +VECTOR_STORE_TYPE = "Chroma" VECTOR_STORE_CONFIG = {"url": "127.0.0.1", "port": "19530"} From b22907f25e07007fa50c44d78edcb5516ac70106 Mon Sep 17 00:00:00 2001 From: csunny Date: Sun, 21 May 2023 17:19:54 +0800 Subject: [PATCH 46/66] llms: support multi large models --- README.md | 7 +++++++ README.zh.md | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/README.md b/README.md index 57ca42fbf..20e1a2017 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ Currently, we have released multiple key features, which are listed below to dem - Unified vector storage/indexing of knowledge base - Support for unstructured data such as PDF, Markdown, CSV, and WebURL +- Milti LLMs Support + - Supports multiple large language models, currently supporting Vicuna (7b, 13b), ChatGLM-6b (int4, int8) + ## Demo @@ -175,6 +178,10 @@ Notice: the webserver need to connect llmserver, so you need change the .env f We provide a user interface for Gradio, which allows you to use DB-GPT through our user interface. Additionally, we have prepared several reference articles (written in Chinese) that introduce the code and principles related to our project. - [LLM Practical In Action Series (1) — Combined Langchain-Vicuna Application Practical](https://medium.com/@cfqcsunny/llm-practical-in-action-series-1-combined-langchain-vicuna-application-practical-701cd0413c9f) +### Multi LLMs Usage + +To use multiple models, modify the LLM_MODEL parameter in the .env configuration file to switch between the models. + ## Acknowledgement The achievements of this project are thanks to the technical community, especially the following projects: diff --git a/README.zh.md b/README.zh.md index ed7e3f03c..38a450308 100644 --- a/README.zh.md +++ b/README.zh.md @@ -26,6 +26,9 @@ DB-GPT 是一个开源的以数据库为基础的GPT实验项目,使用本地 - 知识库统一向量存储/索引 - 非结构化数据支持包括PDF、MarkDown、CSV、WebURL +- 多模型支持 + - 支持多种大语言模型, 当前已支持Vicuna(7b,13b), ChatGLM-6b(int4, int8) + ## 效果演示 示例通过 RTX 4090 GPU 演示,[YouTube 地址](https://www.youtube.com/watch?v=1PWI6F89LPo) @@ -178,6 +181,10 @@ $ python webserver.py 2. [大模型实战系列(2) —— DB-GPT 阿里云部署指南](https://zhuanlan.zhihu.com/p/629467580) 3. [大模型实战系列(3) —— DB-GPT插件模型原理与使用](https://zhuanlan.zhihu.com/p/629623125) + +### 多模型使用 +在.env 配置文件当中, 修改LLM_MODEL参数来切换使用的模型。 + ## 感谢 项目取得的成果,需要感谢技术社区,尤其以下项目。 From a537ce026af6dee313290e5ea13b52f7abfd603b Mon Sep 17 00:00:00 2001 From: csunny Date: Sun, 21 May 2023 17:22:16 +0800 Subject: [PATCH 47/66] docs: add todo --- README.md | 1 + README.zh.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 20e1a2017..c783f7e09 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Currently, we have released multiple key features, which are listed below to dem - Milti LLMs Support - Supports multiple large language models, currently supporting Vicuna (7b, 13b), ChatGLM-6b (int4, int8) + - TODO: codegen2, codet5p ## Demo diff --git a/README.zh.md b/README.zh.md index 38a450308..279920cce 100644 --- a/README.zh.md +++ b/README.zh.md @@ -28,6 +28,7 @@ DB-GPT 是一个开源的以数据库为基础的GPT实验项目,使用本地 - 多模型支持 - 支持多种大语言模型, 当前已支持Vicuna(7b,13b), ChatGLM-6b(int4, int8) + - TODO: codet5p, codegen2 ## 效果演示 From 983a00f53a2d7aff3935b208615fbd770baebc13 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Tue, 23 May 2023 10:50:43 +0800 Subject: [PATCH 48/66] feature:vector store connector --- pilot/configs/model_config.py | 4 +- pilot/source_embedding/knowledge_embedding.py | 37 +--- pilot/source_embedding/source_embedding.py | 41 +--- pilot/vector_store/chroma_store.py | 30 +++ pilot/vector_store/connector.py | 22 +++ pilot/vector_store/milvus_store.py | 185 +++++++++++++----- pilot/vector_store/vector_store_base.py | 8 +- tools/knowlege_init.py | 10 +- 8 files changed, 209 insertions(+), 128 deletions(-) create mode 100644 pilot/vector_store/chroma_store.py create mode 100644 pilot/vector_store/connector.py diff --git a/pilot/configs/model_config.py b/pilot/configs/model_config.py index 7c4928304..6e32daefc 100644 --- a/pilot/configs/model_config.py +++ b/pilot/configs/model_config.py @@ -48,5 +48,7 @@ VECTOR_SEARCH_TOP_K = 10 VS_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "vs_store") KNOWLEDGE_UPLOAD_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") KNOWLEDGE_CHUNK_SPLIT_SIZE = 100 -VECTOR_STORE_TYPE = "milvus" +#vector db type, now provided Chroma and Milvus +VECTOR_STORE_TYPE = "Milvus" +#vector db config VECTOR_STORE_CONFIG = {"url": "127.0.0.1", "port": "19530"} diff --git a/pilot/source_embedding/knowledge_embedding.py b/pilot/source_embedding/knowledge_embedding.py index 63d6c2121..85db5ab02 100644 --- a/pilot/source_embedding/knowledge_embedding.py +++ b/pilot/source_embedding/knowledge_embedding.py @@ -3,8 +3,7 @@ import os from bs4 import BeautifulSoup from langchain.document_loaders import TextLoader, markdown from langchain.embeddings import HuggingFaceEmbeddings -from langchain.vectorstores import Chroma -from pilot.configs.model_config import DATASETS_DIR, KNOWLEDGE_CHUNK_SPLIT_SIZE +from pilot.configs.model_config import DATASETS_DIR, KNOWLEDGE_CHUNK_SPLIT_SIZE, VECTOR_STORE_TYPE from pilot.source_embedding.chn_document_splitter import CHNDocumentSplitter from pilot.source_embedding.csv_embedding import CSVEmbedding from pilot.source_embedding.markdown_embedding import MarkdownEmbedding @@ -12,7 +11,7 @@ from pilot.source_embedding.pdf_embedding import PDFEmbedding import markdown from pilot.source_embedding.pdf_loader import UnstructuredPaddlePDFLoader -from pilot.vector_store.milvus_store import MilvusStore +from pilot.vector_store.connector import VectorStoreConnector class KnowledgeEmbedding: @@ -23,6 +22,7 @@ class KnowledgeEmbedding: self.vector_store_config = vector_store_config self.file_type = "default" self.embeddings = HuggingFaceEmbeddings(model_name=self.model_name) + self.vector_store_config["embeddings"] = self.embeddings self.local_persist = local_persist if not self.local_persist: self.knowledge_embedding_client = self.init_knowledge_embedding() @@ -52,35 +52,10 @@ class KnowledgeEmbedding: return self.knowledge_embedding_client.similar_search(text, topk) def knowledge_persist_initialization(self, append_mode): - vector_name = self.vector_store_config["vector_store_name"] documents = self._load_knownlege(self.file_path) - if self.vector_store_config["vector_store_type"] == "Chroma": - persist_dir = os.path.join(self.vector_store_config["vector_store_path"], vector_name + ".vectordb") - print("vector db path: ", persist_dir) - if os.path.exists(persist_dir): - if append_mode: - print("append knowledge return vector store") - new_documents = self._load_knownlege(self.file_path) - vector_store = Chroma.from_documents(documents=new_documents, - embedding=self.embeddings, - persist_directory=persist_dir) - else: - print("directly return vector store") - vector_store = Chroma(persist_directory=persist_dir, embedding_function=self.embeddings) - else: - print(vector_name + " is new vector store, knowledge begin load...") - vector_store = Chroma.from_documents(documents=documents, - embedding=self.embeddings, - persist_directory=persist_dir) - vector_store.persist() - - elif self.vector_store_config["vector_store_type"] == "milvus": - vector_store = MilvusStore({"url": self.vector_store_config["url"], - "port": self.vector_store_config["port"], - "embedding": self.embeddings}) - vector_store.init_schema_and_load(vector_name, documents) - - return vector_store + self.vector_client = VectorStoreConnector(VECTOR_STORE_TYPE, self.vector_store_config) + self.vector_client.load_document(documents) + return self.vector_client def _load_knownlege(self, path): docments = [] diff --git a/pilot/source_embedding/source_embedding.py b/pilot/source_embedding/source_embedding.py index a253e4d78..ddefd4f1e 100644 --- a/pilot/source_embedding/source_embedding.py +++ b/pilot/source_embedding/source_embedding.py @@ -1,17 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import os from abc import ABC, abstractmethod from langchain.embeddings import HuggingFaceEmbeddings -from langchain.vectorstores import Chroma -from langchain.vectorstores import Milvus - from typing import List, Optional, Dict - - -from pilot.configs.model_config import VECTOR_STORE_TYPE, VECTOR_STORE_CONFIG -from pilot.vector_store.milvus_store import MilvusStore +from pilot.configs.model_config import VECTOR_STORE_TYPE +from pilot.vector_store.connector import VectorStoreConnector registered_methods = [] @@ -35,19 +29,8 @@ class SourceEmbedding(ABC): self.embedding_args = embedding_args self.embeddings = HuggingFaceEmbeddings(model_name=self.model_name) - if VECTOR_STORE_TYPE == "milvus": - print(VECTOR_STORE_CONFIG) - if self.vector_store_config.get("text_field") is None: - self.vector_store_client = MilvusStore({"url": VECTOR_STORE_CONFIG["url"], - "port": VECTOR_STORE_CONFIG["port"], - "embedding": self.embeddings}) - else: - self.vector_store_client = Milvus(embedding_function=self.embeddings, collection_name=self.vector_store_config["vector_store_name"], text_field="content", - connection_args={"host": VECTOR_STORE_CONFIG["url"], "port": VECTOR_STORE_CONFIG["port"]}) - else: - persist_dir = os.path.join(self.vector_store_config["vector_store_path"], - self.vector_store_config["vector_store_name"] + ".vectordb") - self.vector_store_client = Chroma(persist_directory=persist_dir, embedding_function=self.embeddings) + vector_store_config["embeddings"] = self.embeddings + self.vector_client = VectorStoreConnector(VECTOR_STORE_TYPE, vector_store_config) @abstractmethod @register @@ -70,24 +53,12 @@ class SourceEmbedding(ABC): @register def index_to_store(self, docs): """index to vector store""" - - if VECTOR_STORE_TYPE == "chroma": - persist_dir = os.path.join(self.vector_store_config["vector_store_path"], - self.vector_store_config["vector_store_name"] + ".vectordb") - self.vector_store = Chroma.from_documents(docs, self.embeddings, persist_directory=persist_dir) - self.vector_store.persist() - - elif VECTOR_STORE_TYPE == "milvus": - self.vector_store = MilvusStore({"url": VECTOR_STORE_CONFIG["url"], - "port": VECTOR_STORE_CONFIG["port"], - "embedding": self.embeddings}) - self.vector_store.init_schema_and_load(self.vector_store_config["vector_store_name"], docs) + self.vector_client.load_document(docs) @register def similar_search(self, doc, topk): """vector store similarity_search""" - - return self.vector_store_client.similarity_search(doc, topk) + return self.vector_client.similar_search(doc, topk) def source_embedding(self): if 'read' in registered_methods: diff --git a/pilot/vector_store/chroma_store.py b/pilot/vector_store/chroma_store.py new file mode 100644 index 000000000..9a91659f1 --- /dev/null +++ b/pilot/vector_store/chroma_store.py @@ -0,0 +1,30 @@ +import os + +from langchain.vectorstores import Chroma + +from pilot.configs.model_config import KNOWLEDGE_UPLOAD_ROOT_PATH +from pilot.logs import logger +from pilot.vector_store.vector_store_base import VectorStoreBase + + +class ChromaStore(VectorStoreBase): + """chroma database""" + + def __init__(self, ctx: {}) -> None: + self.ctx = ctx + self.embeddings = ctx["embeddings"] + self.persist_dir = os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, + ctx["vector_store_name"] + ".vectordb") + self.vector_store_client = Chroma(persist_directory=self.persist_dir, embedding_function=self.embeddings) + + def similar_search(self, text, topk) -> None: + logger.info("ChromaStore similar search") + return self.vector_store_client.similarity_search(text, topk) + + def load_document(self, documents): + logger.info("ChromaStore load document") + texts = [doc.page_content for doc in documents] + metadatas = [doc.metadata for doc in documents] + self.vector_store_client.add_texts(texts=texts, metadatas=metadatas) + self.vector_store_client.persist() + diff --git a/pilot/vector_store/connector.py b/pilot/vector_store/connector.py new file mode 100644 index 000000000..003415712 --- /dev/null +++ b/pilot/vector_store/connector.py @@ -0,0 +1,22 @@ +from pilot.vector_store.chroma_store import ChromaStore +from pilot.vector_store.milvus_store import MilvusStore + +connector = { + "Chroma": ChromaStore, + "Milvus": MilvusStore + } + + +class VectorStoreConnector: + """ vector store connector, can connect different vector db provided load document api and similar search api + """ + def __init__(self, vector_store_type, ctx: {}) -> None: + self.ctx = ctx + self.connector_class = connector[vector_store_type] + self.client = self.connector_class(ctx) + + def load_document(self, docs): + self.client.load_document(docs) + + def similar_search(self, docs, topk): + return self.client.similar_search(docs, topk) diff --git a/pilot/vector_store/milvus_store.py b/pilot/vector_store/milvus_store.py index 5204e6b11..1c6d6bdbc 100644 --- a/pilot/vector_store/milvus_store.py +++ b/pilot/vector_store/milvus_store.py @@ -1,12 +1,14 @@ -from typing import List, Optional, Iterable +from typing import List, Optional, Iterable, Tuple, Any -from langchain.embeddings import HuggingFaceEmbeddings -from pymilvus import DataType, FieldSchema, CollectionSchema, connections, Collection +from pymilvus import connections, Collection, DataType +from pilot.configs.model_config import VECTOR_STORE_CONFIG +from langchain.docstore.document import Document from pilot.vector_store.vector_store_base import VectorStoreBase class MilvusStore(VectorStoreBase): + """Milvus database""" def __init__(self, ctx: {}) -> None: """init a milvus storage connection. @@ -17,14 +19,13 @@ class MilvusStore(VectorStoreBase): connect_kwargs = {} self.uri = None - self.uri = ctx["url"] - self.port = ctx["port"] + self.uri = ctx.get("url", VECTOR_STORE_CONFIG["url"]) + self.port = ctx.get("port", VECTOR_STORE_CONFIG["port"]) self.username = ctx.get("username", None) self.password = ctx.get("password", None) - self.collection_name = ctx.get("table_name", None) + self.collection_name = ctx.get("vector_store_name", None) self.secure = ctx.get("secure", None) - self.model_config = ctx.get("model_config", None) - self.embedding = ctx.get("embedding", None) + self.embedding = ctx.get("embeddings", None) self.fields = [] # use HNSW by default. @@ -33,6 +34,20 @@ class MilvusStore(VectorStoreBase): "index_type": "HNSW", "params": {"M": 8, "efConstruction": 64}, } + # use HNSW by default. + self.index_params_map = { + "IVF_FLAT": {"params": {"nprobe": 10}}, + "IVF_SQ8": {"params": {"nprobe": 10}}, + "IVF_PQ": {"params": {"nprobe": 10}}, + "HNSW": {"params": {"ef": 10}}, + "RHNSW_FLAT": {"params": {"ef": 10}}, + "RHNSW_SQ": {"params": {"ef": 10}}, + "RHNSW_PQ": {"params": {"ef": 10}}, + "IVF_HNSW": {"params": {"nprobe": 10, "ef": 10}}, + "ANNOY": {"params": {"search_k": 10}}, + } + + self.text_field = "content" if (self.username is None) != (self.password is None): raise ValueError( @@ -48,21 +63,6 @@ class MilvusStore(VectorStoreBase): alias="default" # secure=self.secure, ) - if self.collection_name is not None: - self.col = Collection(self.collection_name) - schema = self.col.schema - for x in schema.fields: - self.fields.append(x.name) - if x.auto_id: - self.fields.remove(x.name) - if x.is_primary: - self.primary_field = x.name - if x.dtype == DataType.FLOAT_VECTOR or x.dtype == DataType.BINARY_VECTOR: - self.vector_field = x.name - - - # self.init_schema() - # self.init_collection_schema() def init_schema_and_load(self, vector_name, documents): """Create a Milvus collection, indexes it with HNSW, load document. @@ -86,7 +86,6 @@ class MilvusStore(VectorStoreBase): "Could not import pymilvus python package. " "Please install it with `pip install pymilvus`." ) - # Connect to Milvus instance if not connections.has_connection("default"): connections.connect( host=self.uri or "127.0.0.1", @@ -140,11 +139,11 @@ class MilvusStore(VectorStoreBase): fields.append( FieldSchema(text_field, DataType.VARCHAR, max_length=max_length + 1) ) - # Create the primary key field + # create the primary key field fields.append( FieldSchema(primary_field, DataType.INT64, is_primary=True, auto_id=True) ) - # Create the vector field + # create the vector field fields.append(FieldSchema(vector_field, DataType.FLOAT_VECTOR, dim=dim)) # Create the schema for the collection schema = CollectionSchema(fields) @@ -176,32 +175,44 @@ class MilvusStore(VectorStoreBase): return self.collection_name - def init_schema(self) -> None: - """Initialize collection in milvus database.""" - fields = [ - FieldSchema(name="pk", dtype=DataType.INT64, is_primary=True, auto_id=True), - FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=self.model_config["dim"]), - FieldSchema(name="raw_text", dtype=DataType.VARCHAR, max_length=65535), - ] - - # create collection if not exist and load it. - self.schema = CollectionSchema(fields, "db-gpt memory storage") - self.collection = Collection(self.collection_name, self.schema) - self.index_params = { - "metric_type": "IP", - "index_type": "HNSW", - "params": {"M": 8, "efConstruction": 64}, - } - # create index if not exist. - if not self.collection.has_index(): - self.collection.release() - self.collection.create_index( - "vector", - self.index_params, - index_name="vector", - ) - info = self.collection.describe() - self.collection.load() + # def init_schema(self) -> None: + # """Initialize collection in milvus database.""" + # fields = [ + # FieldSchema(name="pk", dtype=DataType.INT64, is_primary=True, auto_id=True), + # FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=self.model_config["dim"]), + # FieldSchema(name="raw_text", dtype=DataType.VARCHAR, max_length=65535), + # ] + # + # # create collection if not exist and load it. + # self.schema = CollectionSchema(fields, "db-gpt memory storage") + # self.collection = Collection(self.collection_name, self.schema) + # self.index_params_map = { + # "IVF_FLAT": {"params": {"nprobe": 10}}, + # "IVF_SQ8": {"params": {"nprobe": 10}}, + # "IVF_PQ": {"params": {"nprobe": 10}}, + # "HNSW": {"params": {"ef": 10}}, + # "RHNSW_FLAT": {"params": {"ef": 10}}, + # "RHNSW_SQ": {"params": {"ef": 10}}, + # "RHNSW_PQ": {"params": {"ef": 10}}, + # "IVF_HNSW": {"params": {"nprobe": 10, "ef": 10}}, + # "ANNOY": {"params": {"search_k": 10}}, + # } + # + # self.index_params = { + # "metric_type": "IP", + # "index_type": "HNSW", + # "params": {"M": 8, "efConstruction": 64}, + # } + # # create index if not exist. + # if not self.collection.has_index(): + # self.collection.release() + # self.collection.create_index( + # "vector", + # self.index_params, + # index_name="vector", + # ) + # info = self.collection.describe() + # self.collection.load() # def insert(self, text, model_config) -> str: # """Add an embedding of data into milvus. @@ -226,7 +237,7 @@ class MilvusStore(VectorStoreBase): partition_name: Optional[str] = None, timeout: Optional[int] = None, ) -> List[str]: - """Insert text data into Milvus. + """add text data into Milvus. Args: texts (Iterable[str]): The text being embedded and inserted. metadatas (Optional[List[dict]], optional): The metadata that @@ -259,6 +270,72 @@ class MilvusStore(VectorStoreBase): res = self.col.insert( insert_list, partition_name=partition_name, timeout=timeout ) - # Flush to make sure newly inserted is immediately searchable. + # make sure data is searchable. self.col.flush() return res.primary_keys + + def load_document(self, documents) -> None: + """load document in vector database.""" + self.init_schema_and_load(self.collection_name, documents) + + def similar_search(self, text, topk) -> None: + self.col = Collection(self.collection_name) + schema = self.col.schema + for x in schema.fields: + self.fields.append(x.name) + if x.auto_id: + self.fields.remove(x.name) + if x.is_primary: + self.primary_field = x.name + if x.dtype == DataType.FLOAT_VECTOR or x.dtype == DataType.BINARY_VECTOR: + self.vector_field = x.name + _, docs_and_scores = self._search(text, topk) + return [doc for doc, _, _ in docs_and_scores] + + def _search( + self, + query: str, + k: int = 4, + param: Optional[dict] = None, + expr: Optional[str] = None, + partition_names: Optional[List[str]] = None, + round_decimal: int = -1, + timeout: Optional[int] = None, + **kwargs: Any, + ) -> Tuple[List[float], List[Tuple[Document, Any, Any]]]: + self.col.load() + # use default index params. + if param is None: + index_type = self.col.indexes[0].params["index_type"] + param = self.index_params_map[index_type] + # query text embedding. + data = [self.embedding.embed_query(query)] + # Determine result metadata fields. + output_fields = self.fields[:] + output_fields.remove(self.vector_field) + # milvus search. + res = self.col.search( + data, + self.vector_field, + param, + k, + expr=expr, + output_fields=output_fields, + partition_names=partition_names, + round_decimal=round_decimal, + timeout=timeout, + **kwargs, + ) + # Organize results. + ret = [] + for result in res[0]: + meta = {x: result.entity.get(x) for x in output_fields} + ret.append( + ( + Document(page_content=meta.pop(self.text_field), metadata=meta), + result.distance, + result.id, + ) + ) + + return data[0], ret diff --git a/pilot/vector_store/vector_store_base.py b/pilot/vector_store/vector_store_base.py index 818730f0f..b483b3116 100644 --- a/pilot/vector_store/vector_store_base.py +++ b/pilot/vector_store/vector_store_base.py @@ -2,8 +2,14 @@ from abc import ABC, abstractmethod class VectorStoreBase(ABC): + """base class for vector store database""" @abstractmethod - def init_schema(self) -> None: + def load_document(self, documents) -> None: + """load document in vector database.""" + pass + + @abstractmethod + def similar_search(self, text, topk) -> None: """Initialize schema in vector database.""" pass \ No newline at end of file diff --git a/tools/knowlege_init.py b/tools/knowlege_init.py index 60010e4de..23ca33a80 100644 --- a/tools/knowlege_init.py +++ b/tools/knowlege_init.py @@ -2,10 +2,8 @@ # -*- coding: utf-8 -*- import argparse -from langchain.embeddings import HuggingFaceEmbeddings -from langchain.vectorstores import Milvus - -from pilot.configs.model_config import DATASETS_DIR, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K, VECTOR_STORE_CONFIG +from pilot.configs.model_config import DATASETS_DIR, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K, VECTOR_STORE_CONFIG, \ + VECTOR_STORE_TYPE from pilot.source_embedding.knowledge_embedding import KnowledgeEmbedding @@ -42,8 +40,8 @@ if __name__ == "__main__": args = parser.parse_args() vector_name = args.vector_name append_mode = args.append - store_type = args.store_type - vector_store_config = {"url": VECTOR_STORE_CONFIG["url"], "port": VECTOR_STORE_CONFIG["port"], "vector_store_name":vector_name, "vector_store_type":store_type} + store_type = VECTOR_STORE_TYPE + vector_store_config = {"url": VECTOR_STORE_CONFIG["url"], "port": VECTOR_STORE_CONFIG["port"], "vector_store_name":vector_name} print(vector_store_config) kv = LocalKnowledgeInit(vector_store_config=vector_store_config) vector_store = kv.knowledge_persist(file_path=DATASETS_DIR, append_mode=append_mode) From a1b4c92f6bdeb5e5939c5bbed8b6103ec4e397eb Mon Sep 17 00:00:00 2001 From: halfss Date: Tue, 23 May 2023 14:24:00 +0800 Subject: [PATCH 49/66] Update utils.py get_gpu_memory should return after the loop is over --- pilot/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pilot/utils.py b/pilot/utils.py index 0179d12c2..607b83251 100644 --- a/pilot/utils.py +++ b/pilot/utils.py @@ -33,7 +33,7 @@ def get_gpu_memory(max_gpus=None): allocated_memory = torch.cuda.memory_allocated() / (1024 ** 3) available_memory = total_memory - allocated_memory gpu_memory.append(available_memory) - return gpu_memory + return gpu_memory From ef64935145a0e1d117d1b7d7f0276c049f85d0d5 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Tue, 23 May 2023 22:06:07 +0800 Subject: [PATCH 50/66] update:vector store config --- pilot/configs/config.py | 8 ++++++ pilot/configs/model_config.py | 6 +---- pilot/source_embedding/knowledge_embedding.py | 5 +++- pilot/source_embedding/source_embedding.py | 6 +++-- pilot/vector_store/milvus_store.py | 26 ++++++------------- 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/pilot/configs/config.py b/pilot/configs/config.py index b914390f7..e9ec2bd48 100644 --- a/pilot/configs/config.py +++ b/pilot/configs/config.py @@ -109,6 +109,14 @@ class Config(metaclass=Singleton): self.MODEL_SERVER = os.getenv("MODEL_SERVER", "http://127.0.0.1" + ":" + str(self.MODEL_PORT)) self.ISLOAD_8BIT = os.getenv("ISLOAD_8BIT", "True") == "True" + ### Vector Store Configuration + self.VECTOR_STORE_TYPE = os.getenv("VECTOR_STORE_TYPE", "Chroma") + self.MILVUS_URL = os.getenv("MILVUS_URL", "127.0.0.1") + self.MILVUS_PORT = os.getenv("MILVUS_PORT", "19530") + self.MILVUS_USERNAME = os.getenv("MILVUS_USERNAME", None) + self.MILVUS_PASSWORD = os.getenv("MILVUS_PASSWORD", None) + + def set_debug_mode(self, value: bool) -> None: """Set the debug mode value""" self.debug_mode = value diff --git a/pilot/configs/model_config.py b/pilot/configs/model_config.py index 6e32daefc..ebd8513e4 100644 --- a/pilot/configs/model_config.py +++ b/pilot/configs/model_config.py @@ -47,8 +47,4 @@ ISDEBUG = False VECTOR_SEARCH_TOP_K = 10 VS_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "vs_store") KNOWLEDGE_UPLOAD_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") -KNOWLEDGE_CHUNK_SPLIT_SIZE = 100 -#vector db type, now provided Chroma and Milvus -VECTOR_STORE_TYPE = "Milvus" -#vector db config -VECTOR_STORE_CONFIG = {"url": "127.0.0.1", "port": "19530"} +KNOWLEDGE_CHUNK_SPLIT_SIZE = 100 \ No newline at end of file diff --git a/pilot/source_embedding/knowledge_embedding.py b/pilot/source_embedding/knowledge_embedding.py index 85db5ab02..cb1fcb504 100644 --- a/pilot/source_embedding/knowledge_embedding.py +++ b/pilot/source_embedding/knowledge_embedding.py @@ -3,6 +3,8 @@ import os from bs4 import BeautifulSoup from langchain.document_loaders import TextLoader, markdown from langchain.embeddings import HuggingFaceEmbeddings + +from pilot.configs.config import Config from pilot.configs.model_config import DATASETS_DIR, KNOWLEDGE_CHUNK_SPLIT_SIZE, VECTOR_STORE_TYPE from pilot.source_embedding.chn_document_splitter import CHNDocumentSplitter from pilot.source_embedding.csv_embedding import CSVEmbedding @@ -13,6 +15,7 @@ import markdown from pilot.source_embedding.pdf_loader import UnstructuredPaddlePDFLoader from pilot.vector_store.connector import VectorStoreConnector +CFG = Config() class KnowledgeEmbedding: def __init__(self, file_path, model_name, vector_store_config, local_persist=True): @@ -53,7 +56,7 @@ class KnowledgeEmbedding: def knowledge_persist_initialization(self, append_mode): documents = self._load_knownlege(self.file_path) - self.vector_client = VectorStoreConnector(VECTOR_STORE_TYPE, self.vector_store_config) + self.vector_client = VectorStoreConnector(CFG.VECTOR_STORE_TYPE, self.vector_store_config) self.vector_client.load_document(documents) return self.vector_client diff --git a/pilot/source_embedding/source_embedding.py b/pilot/source_embedding/source_embedding.py index ddefd4f1e..a84282009 100644 --- a/pilot/source_embedding/source_embedding.py +++ b/pilot/source_embedding/source_embedding.py @@ -4,10 +4,12 @@ from abc import ABC, abstractmethod from langchain.embeddings import HuggingFaceEmbeddings from typing import List, Optional, Dict -from pilot.configs.model_config import VECTOR_STORE_TYPE + +from pilot.configs.config import Config from pilot.vector_store.connector import VectorStoreConnector registered_methods = [] +CFG = Config() def register(method): @@ -30,7 +32,7 @@ class SourceEmbedding(ABC): self.embeddings = HuggingFaceEmbeddings(model_name=self.model_name) vector_store_config["embeddings"] = self.embeddings - self.vector_client = VectorStoreConnector(VECTOR_STORE_TYPE, vector_store_config) + self.vector_client = VectorStoreConnector(CFG.VECTOR_STORE_TYPE, vector_store_config) @abstractmethod @register diff --git a/pilot/vector_store/milvus_store.py b/pilot/vector_store/milvus_store.py index 1c6d6bdbc..a61027850 100644 --- a/pilot/vector_store/milvus_store.py +++ b/pilot/vector_store/milvus_store.py @@ -2,11 +2,12 @@ from typing import List, Optional, Iterable, Tuple, Any from pymilvus import connections, Collection, DataType -from pilot.configs.model_config import VECTOR_STORE_CONFIG from langchain.docstore.document import Document + +from pilot.configs.config import Config from pilot.vector_store.vector_store_base import VectorStoreBase - +CFG = Config() class MilvusStore(VectorStoreBase): """Milvus database""" def __init__(self, ctx: {}) -> None: @@ -18,11 +19,10 @@ class MilvusStore(VectorStoreBase): # self.configure(cfg) connect_kwargs = {} - self.uri = None - self.uri = ctx.get("url", VECTOR_STORE_CONFIG["url"]) - self.port = ctx.get("port", VECTOR_STORE_CONFIG["port"]) - self.username = ctx.get("username", None) - self.password = ctx.get("password", None) + self.uri = CFG.MILVUS_URL + self.port = CFG.MILVUS_PORT + self.username = CFG.MILVUS_USERNAME + self.password = CFG.MILVUS_PASSWORD self.collection_name = ctx.get("vector_store_name", None) self.secure = ctx.get("secure", None) self.embedding = ctx.get("embeddings", None) @@ -238,16 +238,6 @@ class MilvusStore(VectorStoreBase): timeout: Optional[int] = None, ) -> List[str]: """add text data into Milvus. - Args: - texts (Iterable[str]): The text being embedded and inserted. - metadatas (Optional[List[dict]], optional): The metadata that - corresponds to each insert. Defaults to None. - partition_name (str, optional): The partition of the collection - to insert data into. Defaults to None. - timeout: specified timeout. - - Returns: - List[str]: The resulting keys for each inserted element. """ insert_dict: Any = {self.text_field: list(texts)} try: @@ -279,6 +269,7 @@ class MilvusStore(VectorStoreBase): self.init_schema_and_load(self.collection_name, documents) def similar_search(self, text, topk) -> None: + """similar_search in vector database.""" self.col = Collection(self.collection_name) schema = self.col.schema for x in schema.fields: @@ -326,7 +317,6 @@ class MilvusStore(VectorStoreBase): timeout=timeout, **kwargs, ) - # Organize results. ret = [] for result in res[0]: meta = {x: result.entity.get(x) for x in output_fields} From 74c1f1f7e13be6dec5a772aed84bd435df2521a0 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Tue, 23 May 2023 22:09:22 +0800 Subject: [PATCH 51/66] update:vector store config --- pilot/server/webserver.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index 270eff67f..44c027bdf 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -19,8 +19,7 @@ from langchain import PromptTemplate ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(ROOT_PATH) -from pilot.configs.model_config import DB_SETTINGS, KNOWLEDGE_UPLOAD_ROOT_PATH, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K, \ - VECTOR_STORE_CONFIG +from pilot.configs.model_config import KNOWLEDGE_UPLOAD_ROOT_PATH, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K from pilot.server.vectordb_qa import KnownLedgeBaseQA from pilot.connections.mysql import MySQLOperator from pilot.source_embedding.knowledge_embedding import KnowledgeEmbedding @@ -268,10 +267,8 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re skip_echo_len = len(prompt.replace("", " ")) + 1 if mode == conversation_types["custome"] and not db_selector: - # persist_dir = os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, vector_store_name["vs_name"]) - print("vector store type: ", VECTOR_STORE_CONFIG) print("vector store name: ", vector_store_name["vs_name"]) - vector_store_config = VECTOR_STORE_CONFIG + vector_store_config = [] vector_store_config["vector_store_name"] = vector_store_name["vs_name"] vector_store_config["text_field"] = "content" vector_store_config["vector_store_path"] = KNOWLEDGE_UPLOAD_ROOT_PATH From b0d3d02d205e2ce78a664a1a2bb8d0050a07dca9 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Tue, 23 May 2023 22:10:15 +0800 Subject: [PATCH 52/66] update:vector store config --- pilot/source_embedding/knowledge_embedding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pilot/source_embedding/knowledge_embedding.py b/pilot/source_embedding/knowledge_embedding.py index cb1fcb504..93fa185a6 100644 --- a/pilot/source_embedding/knowledge_embedding.py +++ b/pilot/source_embedding/knowledge_embedding.py @@ -5,7 +5,7 @@ from langchain.document_loaders import TextLoader, markdown from langchain.embeddings import HuggingFaceEmbeddings from pilot.configs.config import Config -from pilot.configs.model_config import DATASETS_DIR, KNOWLEDGE_CHUNK_SPLIT_SIZE, VECTOR_STORE_TYPE +from pilot.configs.model_config import DATASETS_DIR, KNOWLEDGE_CHUNK_SPLIT_SIZE from pilot.source_embedding.chn_document_splitter import CHNDocumentSplitter from pilot.source_embedding.csv_embedding import CSVEmbedding from pilot.source_embedding.markdown_embedding import MarkdownEmbedding From 24d6762d962b53a963b11284345b5a53effa7cb2 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Tue, 23 May 2023 22:16:08 +0800 Subject: [PATCH 53/66] update:vector store config --- pilot/server/webserver.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index 44c027bdf..1ac32ab26 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -268,10 +268,8 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re if mode == conversation_types["custome"] and not db_selector: print("vector store name: ", vector_store_name["vs_name"]) - vector_store_config = [] - vector_store_config["vector_store_name"] = vector_store_name["vs_name"] - vector_store_config["text_field"] = "content" - vector_store_config["vector_store_path"] = KNOWLEDGE_UPLOAD_ROOT_PATH + vector_store_config = {"vector_store_name": vector_store_name["vs_name"], "text_field": "content", + "vector_store_path": KNOWLEDGE_UPLOAD_ROOT_PATH} knowledge_embedding_client = KnowledgeEmbedding(file_path="", model_name=LLM_MODEL_CONFIG["text2vec"], local_persist=False, vector_store_config=vector_store_config) From 0b92066bf5ad918a207cc3a9c54e85b5de2fc9d4 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Tue, 23 May 2023 22:39:56 +0800 Subject: [PATCH 54/66] update:requirements --- requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 29e792451..f9becba90 100644 --- a/requirements.txt +++ b/requirements.txt @@ -61,7 +61,9 @@ gTTS==2.3.1 langchain nltk python-dotenv==1.0.0 -pymilvus +pymilvus==2.2.1 +paddle==2.2 +paddleocr==2.6.1.3 vcrpy chromadb markdown2 From 926c9716915ccedaa72883be3f749e9e51abb793 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Tue, 23 May 2023 22:43:07 +0800 Subject: [PATCH 55/66] update:PDF loader --- pilot/source_embedding/knowledge_embedding.py | 5 ++--- pilot/source_embedding/pdf_embedding.py | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pilot/source_embedding/knowledge_embedding.py b/pilot/source_embedding/knowledge_embedding.py index 93fa185a6..2f313a35a 100644 --- a/pilot/source_embedding/knowledge_embedding.py +++ b/pilot/source_embedding/knowledge_embedding.py @@ -1,7 +1,7 @@ import os from bs4 import BeautifulSoup -from langchain.document_loaders import TextLoader, markdown +from langchain.document_loaders import TextLoader, markdown, PyPDFLoader from langchain.embeddings import HuggingFaceEmbeddings from pilot.configs.config import Config @@ -12,7 +12,6 @@ from pilot.source_embedding.markdown_embedding import MarkdownEmbedding from pilot.source_embedding.pdf_embedding import PDFEmbedding import markdown -from pilot.source_embedding.pdf_loader import UnstructuredPaddlePDFLoader from pilot.vector_store.connector import VectorStoreConnector CFG = Config() @@ -89,7 +88,7 @@ class KnowledgeEmbedding: docs[i].page_content = docs[i].page_content.replace("\n", " ") i += 1 elif filename.lower().endswith(".pdf"): - loader = UnstructuredPaddlePDFLoader(filename) + loader = PyPDFLoader(filename) textsplitter = CHNDocumentSplitter(pdf=True, sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE) docs = loader.load_and_split(textsplitter) i = 0 diff --git a/pilot/source_embedding/pdf_embedding.py b/pilot/source_embedding/pdf_embedding.py index a8749695b..75d17c4c6 100644 --- a/pilot/source_embedding/pdf_embedding.py +++ b/pilot/source_embedding/pdf_embedding.py @@ -2,12 +2,12 @@ # -*- coding: utf-8 -*- from typing import List +from langchain.document_loaders import PyPDFLoader from langchain.schema import Document from pilot.configs.model_config import KNOWLEDGE_CHUNK_SPLIT_SIZE from pilot.source_embedding import SourceEmbedding, register from pilot.source_embedding.chn_document_splitter import CHNDocumentSplitter -from pilot.source_embedding.pdf_loader import UnstructuredPaddlePDFLoader class PDFEmbedding(SourceEmbedding): @@ -23,7 +23,8 @@ class PDFEmbedding(SourceEmbedding): @register def read(self): """Load from pdf path.""" - loader = UnstructuredPaddlePDFLoader(self.file_path) + # loader = UnstructuredPaddlePDFLoader(self.file_path) + loader = PyPDFLoader(self.file_path) textsplitter = CHNDocumentSplitter(pdf=True, sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE) return loader.load_and_split(textsplitter) From 2bf7ba827b250ac7cc7d407c562aeb2cfef13a64 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Tue, 23 May 2023 22:59:16 +0800 Subject: [PATCH 56/66] update:requirement --- requirements.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index f9becba90..aea4f00e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,8 +62,6 @@ langchain nltk python-dotenv==1.0.0 pymilvus==2.2.1 -paddle==2.2 -paddleocr==2.6.1.3 vcrpy chromadb markdown2 From d599a48cbc7a75a0707ac9c6146f64dc826d10e5 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Tue, 23 May 2023 23:50:12 +0800 Subject: [PATCH 57/66] update:env template --- .env.template | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.env.template b/.env.template index d809a362b..159299f71 100644 --- a/.env.template +++ b/.env.template @@ -81,3 +81,14 @@ DENYLISTED_PLUGINS= #*******************************************************************# # CHAT_MESSAGES_ENABLED - Enable chat messages (Default: False) # CHAT_MESSAGES_ENABLED=False + + +#*******************************************************************# +#** VECTOR STORE SETTINGS **# +#*******************************************************************# +VECTOR_STORE_TYPE=Chroma +MILVUS_URL=127.0.0.1 +MILVUS_PORT=19530 +#MILVUS_USERNAME +#MILVUS_PASSWORD +#MILVUS_SECURE= From e6339b06ad1879bb9d032d7721b08ec774cdd277 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Tue, 23 May 2023 23:52:48 +0800 Subject: [PATCH 58/66] update:env template --- .env.template | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.template b/.env.template index 159299f71..3fe762e73 100644 --- a/.env.template +++ b/.env.template @@ -87,8 +87,8 @@ DENYLISTED_PLUGINS= #** VECTOR STORE SETTINGS **# #*******************************************************************# VECTOR_STORE_TYPE=Chroma -MILVUS_URL=127.0.0.1 -MILVUS_PORT=19530 +#MILVUS_URL=127.0.0.1 +#MILVUS_PORT=19530 #MILVUS_USERNAME #MILVUS_PASSWORD #MILVUS_SECURE= From 60ecde5892e8ecc25a85a9e9bd0f46e5906f8397 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Wed, 24 May 2023 12:33:41 +0800 Subject: [PATCH 59/66] fix: can not answer on mac m1-> mps device --- pilot/model/loader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pilot/model/loader.py b/pilot/model/loader.py index 531080314..bd31bae0a 100644 --- a/pilot/model/loader.py +++ b/pilot/model/loader.py @@ -9,6 +9,7 @@ from typing import Optional from pilot.model.compression import compress_module from pilot.model.adapter import get_llm_model_adapter from pilot.utils import get_gpu_memory +from pilot.configs.model_config import DEVICE from pilot.model.llm.monkey_patch import replace_llama_attn_with_non_inplace_operations def raise_warning_for_incompatible_cpu_offloading_configuration( @@ -50,7 +51,7 @@ class ModelLoader(metaclass=Singleton): def __init__(self, model_path) -> None: - self.device = "cuda" if torch.cuda.is_available() else "cpu" + self.device = DEVICE self.model_path = model_path self.kwargs = { "torch_dtype": torch.float16, From 9dc542ca45ca7a68e4b46b8d1ace4b3dfb4c6e1a Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Wed, 24 May 2023 15:17:59 +0800 Subject: [PATCH 60/66] fix: dotenv can not load Signed-off-by: yihong0618 --- pilot/configs/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pilot/configs/__init__.py diff --git a/pilot/configs/__init__.py b/pilot/configs/__init__.py new file mode 100644 index 000000000..909f8bf4b --- /dev/null +++ b/pilot/configs/__init__.py @@ -0,0 +1,14 @@ +import os +import random +import sys + +from dotenv import load_dotenv + +if "pytest" in sys.argv or "pytest" in sys.modules or os.getenv("CI"): + print("Setting random seed to 42") + random.seed(42) + +# Load the users .env file into environment variables +load_dotenv(verbose=True, override=True) + +del load_dotenv From 977a88509e4f1e501438ce3525146a54fc43fb45 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Wed, 24 May 2023 15:22:10 +0800 Subject: [PATCH 61/66] update:config env --- pilot/vector_store/milvus_store.py | 18 +++++------------- requirements.txt | 1 + tools/knowlege_init.py | 11 ++++++----- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/pilot/vector_store/milvus_store.py b/pilot/vector_store/milvus_store.py index a61027850..8af9240e2 100644 --- a/pilot/vector_store/milvus_store.py +++ b/pilot/vector_store/milvus_store.py @@ -139,29 +139,21 @@ class MilvusStore(VectorStoreBase): fields.append( FieldSchema(text_field, DataType.VARCHAR, max_length=max_length + 1) ) - # create the primary key field + # primary key field fields.append( FieldSchema(primary_field, DataType.INT64, is_primary=True, auto_id=True) ) - # create the vector field + # vector field fields.append(FieldSchema(vector_field, DataType.FLOAT_VECTOR, dim=dim)) - # Create the schema for the collection + # milvus the schema for the collection schema = CollectionSchema(fields) # Create the collection collection = Collection(collection_name, schema) self.col = collection - # Index parameters for the collection + # index parameters for the collection index = self.index_params - # Create the index + # milvus index collection.create_index(vector_field, index) - # Create the VectorStore - # milvus = cls( - # embedding, - # kwargs.get("connection_args", {"port": 19530}), - # collection_name, - # text_field, - # ) - # Add the texts. schema = collection.schema for x in schema.fields: self.fields.append(x.name) diff --git a/requirements.txt b/requirements.txt index aea4f00e0..685661026 100644 --- a/requirements.txt +++ b/requirements.txt @@ -69,6 +69,7 @@ colorama playsound distro pypdf +milvus-cli==0.3.2 # Testing dependencies pytest diff --git a/tools/knowlege_init.py b/tools/knowlege_init.py index 23ca33a80..e64521031 100644 --- a/tools/knowlege_init.py +++ b/tools/knowlege_init.py @@ -2,11 +2,11 @@ # -*- coding: utf-8 -*- import argparse -from pilot.configs.model_config import DATASETS_DIR, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K, VECTOR_STORE_CONFIG, \ - VECTOR_STORE_TYPE +from pilot.configs.config import Config +from pilot.configs.model_config import DATASETS_DIR, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K from pilot.source_embedding.knowledge_embedding import KnowledgeEmbedding - +CFG = Config() class LocalKnowledgeInit: embeddings: object = None model_name = LLM_MODEL_CONFIG["text2vec"] @@ -32,6 +32,7 @@ class LocalKnowledgeInit: dc, s = doc yield s, dc + if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--vector_name", type=str, default="default") @@ -40,8 +41,8 @@ if __name__ == "__main__": args = parser.parse_args() vector_name = args.vector_name append_mode = args.append - store_type = VECTOR_STORE_TYPE - vector_store_config = {"url": VECTOR_STORE_CONFIG["url"], "port": VECTOR_STORE_CONFIG["port"], "vector_store_name":vector_name} + store_type = CFG.VECTOR_STORE_TYPE + vector_store_config = {"vector_store_name": vector_name} print(vector_store_config) kv = LocalKnowledgeInit(vector_store_config=vector_store_config) vector_store = kv.knowledge_persist(file_path=DATASETS_DIR, append_mode=append_mode) From 94a418897a02ab9025b310e6a18242aa90be5309 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Wed, 24 May 2023 16:28:13 +0800 Subject: [PATCH 62/66] update:config env --- pilot/configs/config.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pilot/configs/config.py b/pilot/configs/config.py index e9ec2bd48..2e933649a 100644 --- a/pilot/configs/config.py +++ b/pilot/configs/config.py @@ -38,14 +38,6 @@ class Config(metaclass=Singleton): self.use_mac_os_tts = False self.use_mac_os_tts = os.getenv("USE_MAC_OS_TTS") - # milvus or zilliz cloud configuration - self.milvus_addr = os.getenv("MILVUS_ADDR", "localhost:19530") - self.milvus_username = os.getenv("MILVUS_USERNAME") - self.milvus_password = os.getenv("MILVUS_PASSWORD") - self.milvus_collection = os.getenv("MILVUS_COLLECTION", "dbgpt") - self.milvus_secure = os.getenv("MILVUS_SECURE") == "True" - - self.authorise_key = os.getenv("AUTHORISE_COMMAND_KEY", "y") self.exit_key = os.getenv("EXIT_KEY", "n") self.image_provider = os.getenv("IMAGE_PROVIDER", True) From e596e1d6435d5fdd4dcc90ebce1a11458359e320 Mon Sep 17 00:00:00 2001 From: aries-ckt <916701291@qq.com> Date: Wed, 24 May 2023 17:26:04 +0800 Subject: [PATCH 63/66] fix:knowledge init env --- assets/DB_GPT_wechat.png | Bin 262711 -> 160456 bytes tools/knowlege_init.py | 5 +++++ 2 files changed, 5 insertions(+) diff --git a/assets/DB_GPT_wechat.png b/assets/DB_GPT_wechat.png index 8abeec70887bd4fbaf5b075d7d9d56a569e70c42..a1d7f75589f222150da31f210165ebeb1d482282 100644 GIT binary patch literal 160456 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 literal 262711 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�@IpK+Ioyac6~7BxTrst+RC!j|B^P`w7J?p}*| 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

6<>m6@ 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ψC)_jEoTJt_|0iib73wzx?sT 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 OfhZ2^p{%d Date: Wed, 24 May 2023 18:42:55 +0800 Subject: [PATCH 64/66] ci: make ci happy lint the code, delete unused imports Signed-off-by: yihong0618 --- .github/workflows/pylint.yml | 17 +- examples/app.py | 48 +-- examples/embdserver.py | 25 +- examples/gpt_index.py | 8 +- examples/gradio_test.py | 6 +- .../knowledge_embedding/csv_embedding_test.py | 13 +- .../knowledge_embedding/pdf_embedding_test.py | 11 +- .../knowledge_embedding/url_embedding_test.py | 11 +- examples/t5_example.py | 33 +- pilot/__init__.py | 7 +- pilot/agent/agent.py | 10 +- pilot/agent/agent_manager.py | 9 +- pilot/agent/json_fix_llm.py | 14 +- pilot/chain/audio.py | 2 +- pilot/chain/visual.py | 2 +- pilot/commands/command.py | 41 ++- pilot/commands/commands_load.py | 14 +- pilot/commands/exception_not_commands.py | 3 +- pilot/commands/image_gen.py | 3 +- pilot/configs/ai_config.py | 8 +- pilot/configs/config.py | 44 ++- pilot/configs/model_config.py | 20 +- pilot/connections/base.py | 2 +- pilot/connections/clickhouse.py | 3 +- pilot/connections/es.py | 3 +- pilot/connections/mongo.py | 4 +- pilot/connections/mysql.py | 22 +- pilot/connections/oracle.py | 4 +- pilot/connections/postgres.py | 4 +- pilot/connections/redis.py | 3 +- pilot/conversation.py | 49 +-- pilot/json_utils/json_fix_general.py | 2 +- pilot/logs.py | 53 ++-- pilot/model/adapter.py | 56 ++-- pilot/model/base.py | 6 +- pilot/model/chatglm_llm.py | 33 +- pilot/model/compression.py | 53 ++-- pilot/model/inference.py | 70 +++-- pilot/model/llm/base.py | 11 +- pilot/model/llm/llm_utils.py | 56 ++-- pilot/model/llm/monkey_patch.py | 4 +- pilot/model/llm_utils.py | 26 +- pilot/model/loader.py | 42 +-- pilot/model/vicuna_llm.py | 40 +-- pilot/plugins.py | 7 +- pilot/prompts/auto_mode_prompt.py | 69 +++-- pilot/prompts/generator.py | 2 +- pilot/prompts/prompt.py | 15 +- pilot/pturning/lora/finetune.py | 64 ++-- pilot/server/chat_adapter.py | 32 +- pilot/server/gradio_css.py | 8 +- pilot/server/gradio_patch.py | 13 +- pilot/server/llmserver.py | 76 ++--- pilot/server/vectordb_qa.py | 19 +- pilot/server/webserver.py | 282 +++++++++++------- pilot/singleton.py | 7 +- pilot/source_embedding/__init__.py | 9 +- .../source_embedding/chn_document_splitter.py | 32 +- pilot/source_embedding/csv_embedding.py | 16 +- pilot/source_embedding/knowledge_embedding.py | 58 ++-- pilot/source_embedding/markdown_embedding.py | 19 +- pilot/source_embedding/pdf_embedding.py | 9 +- pilot/source_embedding/pdf_loader.py | 14 +- pilot/source_embedding/search_milvus.py | 2 +- pilot/source_embedding/source_embedding.py | 35 ++- pilot/source_embedding/url_embedding.py | 17 +- pilot/utils.py | 38 +-- pilot/vector_store/chroma_store.py | 10 +- pilot/vector_store/connector.py | 9 +- pilot/vector_store/extract_tovec.py | 48 ++- pilot/vector_store/file_loader.py | 65 ++-- pilot/vector_store/milvus_store.py | 27 +- pilot/vector_store/vector_store_base.py | 2 +- tests/unit/test_plugins.py | 8 +- tools/knowlege_init.py | 27 +- 75 files changed, 1110 insertions(+), 824 deletions(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 383e65cd0..e82f1b9d4 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -1,6 +1,15 @@ name: Pylint -on: [push] +on: + push: + branches: [ make_ci_happy ] + pull_request: + branches: [ main ] + workflow_dispatch: + +concurrency: + group: ${{ github.event.number || github.run_id }} + cancel-in-progress: true jobs: build: @@ -17,7 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pylint - - name: Analysing the code with pylint + pip install -U black isort + - name: check the code lint run: | - pylint $(git ls-files '*.py') + black . --check diff --git a/examples/app.py b/examples/app.py index 7cb3aad7f..31b6e0f26 100644 --- a/examples/app.py +++ b/examples/app.py @@ -2,24 +2,28 @@ # -*- coding:utf-8 -*- import gradio as gr -from langchain.agents import ( - load_tools, - initialize_agent, - AgentType -) -from pilot.model.vicuna_llm import VicunaRequestLLM, VicunaEmbeddingLLM -from llama_index import LLMPredictor, LangchainEmbedding, ServiceContext +from langchain.agents import AgentType, initialize_agent, load_tools from langchain.embeddings.huggingface import HuggingFaceEmbeddings -from llama_index import Document, GPTSimpleVectorIndex +from llama_index import ( + Document, + GPTSimpleVectorIndex, + LangchainEmbedding, + LLMPredictor, + ServiceContext, +) + +from pilot.model.vicuna_llm import VicunaEmbeddingLLM, VicunaRequestLLM + def agent_demo(): llm = VicunaRequestLLM() - tools = load_tools(['python_repl'], llm=llm) - agent = initialize_agent(tools, llm, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True) - agent.run( - "Write a SQL script that Query 'select count(1)!'" + tools = load_tools(["python_repl"], llm=llm) + agent = initialize_agent( + tools, llm, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True ) + agent.run("Write a SQL script that Query 'select count(1)!'") + def knowledged_qa_demo(text_list): llm_predictor = LLMPredictor(llm=VicunaRequestLLM()) @@ -27,27 +31,34 @@ def knowledged_qa_demo(text_list): embed_model = LangchainEmbedding(hfemb) documents = [Document(t) for t in text_list] - service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor, embed_model=embed_model) - index = GPTSimpleVectorIndex.from_documents(documents, service_context=service_context) + service_context = ServiceContext.from_defaults( + llm_predictor=llm_predictor, embed_model=embed_model + ) + index = GPTSimpleVectorIndex.from_documents( + documents, service_context=service_context + ) return index def get_answer(q): - base_knowledge = """ """ + base_knowledge = """ """ text_list = [base_knowledge] index = knowledged_qa_demo(text_list) response = index.query(q) return response.response + def get_similar(q): from pilot.vector_store.extract_tovec import knownledge_tovec, knownledge_tovec_st + docsearch = knownledge_tovec_st("./datasets/plan.md") docs = docsearch.similarity_search_with_score(q, k=1) for doc in docs: - dc, s = doc + dc, s = doc print(s) - yield dc.page_content + yield dc.page_content + if __name__ == "__main__": # agent_demo() @@ -58,8 +69,7 @@ if __name__ == "__main__": text_input = gr.TextArea() text_output = gr.TextArea() text_button = gr.Button() - + text_button.click(get_similar, inputs=text_input, outputs=text_output) demo.queue(concurrency_count=3).launch(server_name="0.0.0.0") - diff --git a/examples/embdserver.py b/examples/embdserver.py index 32eca1291..ae0dfcae8 100644 --- a/examples/embdserver.py +++ b/examples/embdserver.py @@ -1,30 +1,29 @@ #!/usr/bin/env python3 # -*- coding:utf-8 -*- -import requests import json -import time -import uuid import os import sys from urllib.parse import urljoin + import gradio as gr +import requests ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(ROOT_PATH) -from pilot.configs.config import Config -from pilot.conversation import conv_qa_prompt_template, conv_templates from langchain.prompts import PromptTemplate +from pilot.configs.config import Config +from pilot.conversation import conv_qa_prompt_template, conv_templates llmstream_stream_path = "generate_stream" CFG = Config() -def generate(query): +def generate(query): template_name = "conv_one_shot" state = conv_templates[template_name].copy() @@ -47,7 +46,7 @@ def generate(query): "prompt": prompt, "temperature": 1.0, "max_new_tokens": 1024, - "stop": "###" + "stop": "###", } response = requests.post( @@ -57,19 +56,18 @@ def generate(query): skip_echo_len = len(params["prompt"]) + 1 - params["prompt"].count("") * 3 for chunk in response.iter_lines(decode_unicode=False, delimiter=b"\0"): - if chunk: data = json.loads(chunk.decode()) if data["error_code"] == 0: - if "vicuna" in CFG.LLM_MODEL: output = data["text"][skip_echo_len:].strip() else: output = data["text"].strip() state.messages[-1][-1] = output + "▌" - yield(output) - + yield (output) + + if __name__ == "__main__": print(CFG.LLM_MODEL) with gr.Blocks() as demo: @@ -78,10 +76,7 @@ if __name__ == "__main__": text_input = gr.TextArea() text_output = gr.TextArea() text_button = gr.Button("提交") - text_button.click(generate, inputs=text_input, outputs=text_output) - demo.queue(concurrency_count=3).launch(server_name="0.0.0.0") - - \ No newline at end of file + demo.queue(concurrency_count=3).launch(server_name="0.0.0.0") diff --git a/examples/gpt_index.py b/examples/gpt_index.py index 29c0a3fe0..2a0841a24 100644 --- a/examples/gpt_index.py +++ b/examples/gpt_index.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import os import logging import sys -from llama_index import SimpleDirectoryReader, GPTSimpleVectorIndex +from llama_index import GPTSimpleVectorIndex, SimpleDirectoryReader + logging.basicConfig(stream=sys.stdout, level=logging.INFO) logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout)) # read the document of data dir documents = SimpleDirectoryReader("data").load_data() -# split the document to chunk, max token size=500, convert chunk to vector +# split the document to chunk, max token size=500, convert chunk to vector index = GPTSimpleVectorIndex(documents) # save index -index.save_to_disk("index.json") \ No newline at end of file +index.save_to_disk("index.json") diff --git a/examples/gradio_test.py b/examples/gradio_test.py index f39a1ca9e..593c6c1f4 100644 --- a/examples/gradio_test.py +++ b/examples/gradio_test.py @@ -3,17 +3,19 @@ import gradio as gr + def change_tab(): return gr.Tabs.update(selected=1) + with gr.Blocks() as demo: with gr.Tabs() as tabs: with gr.TabItem("Train", id=0): t = gr.Textbox() with gr.TabItem("Inference", id=1): i = gr.Image() - + btn = gr.Button() btn.click(change_tab, None, tabs) -demo.launch() \ No newline at end of file +demo.launch() diff --git a/examples/knowledge_embedding/csv_embedding_test.py b/examples/knowledge_embedding/csv_embedding_test.py index d796596c6..3f08422f7 100644 --- a/examples/knowledge_embedding/csv_embedding_test.py +++ b/examples/knowledge_embedding/csv_embedding_test.py @@ -1,5 +1,3 @@ - - from pilot.source_embedding.csv_embedding import CSVEmbedding # path = "/Users/chenketing/Downloads/share_ireserve双写数据异常2.xlsx" @@ -8,6 +6,13 @@ model_name = "your_path/all-MiniLM-L6-v2" vector_store_path = "your_path/" -pdf_embedding = CSVEmbedding(file_path=path, model_name=model_name, vector_store_config={"vector_store_name": "url", "vector_store_path": "vector_store_path"}) +pdf_embedding = CSVEmbedding( + file_path=path, + model_name=model_name, + vector_store_config={ + "vector_store_name": "url", + "vector_store_path": "vector_store_path", + }, +) pdf_embedding.source_embedding() -print("success") \ No newline at end of file +print("success") diff --git a/examples/knowledge_embedding/pdf_embedding_test.py b/examples/knowledge_embedding/pdf_embedding_test.py index 6c3f3588e..660b811ee 100644 --- a/examples/knowledge_embedding/pdf_embedding_test.py +++ b/examples/knowledge_embedding/pdf_embedding_test.py @@ -6,6 +6,13 @@ model_name = "your_path/all-MiniLM-L6-v2" vector_store_path = "your_path/" -pdf_embedding = PDFEmbedding(file_path=path, model_name=model_name, vector_store_config={"vector_store_name": "ob-pdf", "vector_store_path": vector_store_path}) +pdf_embedding = PDFEmbedding( + file_path=path, + model_name=model_name, + vector_store_config={ + "vector_store_name": "ob-pdf", + "vector_store_path": vector_store_path, + }, +) pdf_embedding.source_embedding() -print("success") \ No newline at end of file +print("success") diff --git a/examples/knowledge_embedding/url_embedding_test.py b/examples/knowledge_embedding/url_embedding_test.py index 5db7f998d..aeb353c89 100644 --- a/examples/knowledge_embedding/url_embedding_test.py +++ b/examples/knowledge_embedding/url_embedding_test.py @@ -5,6 +5,13 @@ model_name = "your_path/all-MiniLM-L6-v2" vector_store_path = "your_path" -pdf_embedding = URLEmbedding(file_path=path, model_name=model_name, vector_store_config={"vector_store_name": "url", "vector_store_path": "vector_store_path"}) +pdf_embedding = URLEmbedding( + file_path=path, + model_name=model_name, + vector_store_config={ + "vector_store_name": "url", + "vector_store_path": "vector_store_path", + }, +) pdf_embedding.source_embedding() -print("success") \ No newline at end of file +print("success") diff --git a/examples/t5_example.py b/examples/t5_example.py index a63c9f961..ab2b7f2e3 100644 --- a/examples/t5_example.py +++ b/examples/t5_example.py @@ -1,19 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from llama_index import SimpleDirectoryReader, LangchainEmbedding, GPTListIndex, GPTSimpleVectorIndex, PromptHelper -from langchain.embeddings.huggingface import HuggingFaceEmbeddings -from llama_index import LLMPredictor import torch +from langchain.embeddings.huggingface import HuggingFaceEmbeddings from langchain.llms.base import LLM +from llama_index import ( + GPTListIndex, + GPTSimpleVectorIndex, + LangchainEmbedding, + LLMPredictor, + PromptHelper, + SimpleDirectoryReader, +) from transformers import pipeline class FlanLLM(LLM): model_name = "google/flan-t5-large" - pipeline = pipeline("text2text-generation", model=model_name, device=0, model_kwargs={ - "torch_dtype": torch.bfloat16 - }) + pipeline = pipeline( + "text2text-generation", + model=model_name, + device=0, + model_kwargs={"torch_dtype": torch.bfloat16}, + ) def _call(self, prompt, stop=None): return self.pipeline(prompt, max_length=9999)[0]["generated_text"] @@ -24,6 +33,7 @@ class FlanLLM(LLM): def _llm_type(self): return "custome" + llm_predictor = LLMPredictor(llm=FlanLLM()) hfemb = HuggingFaceEmbeddings() embed_model = LangchainEmbedding(hfemb) @@ -214,9 +224,10 @@ OceanBase 数据库 EXPLAIN 命令输出的第一部分是执行计划的树形 回答: nlj也是左表的表是驱动表,这个要了解下计划执行方面的基本原理,取左表的一行数据,再遍历右表,一旦满足连接条件,就可以返回数据 anti/semi只是因为not exists/exist的语义只是返回左表数据,改成anti join是一种计划优化,连接的方式比子查询更优 -""" +""" from llama_index import Document + text_list = [text1] documents = [Document(t) for t in text_list] @@ -226,12 +237,18 @@ max_input_size = 512 max_chunk_overlap = 20 prompt_helper = PromptHelper(max_input_size, num_output, max_chunk_overlap) -index = GPTListIndex(documents, embed_model=embed_model, llm_predictor=llm_predictor, prompt_helper=prompt_helper) +index = GPTListIndex( + documents, + embed_model=embed_model, + llm_predictor=llm_predictor, + prompt_helper=prompt_helper, +) index.save_to_disk("index.json") if __name__ == "__main__": import logging + logging.getLogger().setLevel(logging.CRITICAL) for d in documents: print(d) diff --git a/pilot/__init__.py b/pilot/__init__.py index b1d1cd3d2..f44b2e809 100644 --- a/pilot/__init__.py +++ b/pilot/__init__.py @@ -1,6 +1,3 @@ -from pilot.source_embedding import (SourceEmbedding, register) +from pilot.source_embedding import SourceEmbedding, register -__all__ = [ - "SourceEmbedding", - "register" -] +__all__ = ["SourceEmbedding", "register"] diff --git a/pilot/agent/agent.py b/pilot/agent/agent.py index 3463790bc..8d8220b4a 100644 --- a/pilot/agent/agent.py +++ b/pilot/agent/agent.py @@ -3,10 +3,10 @@ class Agent: - """Agent class for interacting with DB-GPT - - Attributes: + """Agent class for interacting with DB-GPT + + Attributes: """ - + def __init__(self) -> None: - pass \ No newline at end of file + pass diff --git a/pilot/agent/agent_manager.py b/pilot/agent/agent_manager.py index 89754bd1c..31b55eb65 100644 --- a/pilot/agent/agent_manager.py +++ b/pilot/agent/agent_manager.py @@ -4,10 +4,8 @@ from __future__ import annotations from pilot.configs.config import Config -from pilot.singleton import Singleton -from pilot.configs.config import Config -from typing import List from pilot.model.base import Message +from pilot.singleton import Singleton class AgentManager(metaclass=Singleton): @@ -17,6 +15,7 @@ class AgentManager(metaclass=Singleton): self.next_key = 0 self.agents = {} # key, (task, full_message_history, model) self.cfg = Config() + """Agent manager for managing DB-GPT agents In order to compatible auto gpt plugins, we use the same template with it. @@ -28,7 +27,7 @@ class AgentManager(metaclass=Singleton): def __init__(self) -> None: self.next_key = 0 - self.agents = {} #TODO need to define + self.agents = {} # TODO need to define self.cfg = Config() # Create new GPT agent @@ -46,7 +45,6 @@ class AgentManager(metaclass=Singleton): The key of the new agent """ - def message_agent(self, key: str | int, message: str) -> str: """Send a message to an agent and return its response @@ -58,7 +56,6 @@ class AgentManager(metaclass=Singleton): The agent's response """ - def list_agents(self) -> list[tuple[str | int, str]]: """Return a list of all agents diff --git a/pilot/agent/json_fix_llm.py b/pilot/agent/json_fix_llm.py index 3ca8f85b0..327881a78 100644 --- a/pilot/agent/json_fix_llm.py +++ b/pilot/agent/json_fix_llm.py @@ -1,18 +1,22 @@ - +import contextlib import json from typing import Any, Dict -import contextlib + from colorama import Fore from regex import regex from pilot.configs.config import Config +from pilot.json_utils.json_fix_general import ( + add_quotes_to_property_names, + balance_braces, + fix_invalid_escape, +) from pilot.logs import logger from pilot.speech import say_text -from pilot.json_utils.json_fix_general import fix_invalid_escape,add_quotes_to_property_names,balance_braces - CFG = Config() + def fix_and_parse_json( json_to_load: str, try_to_fix_with_gpt: bool = True ) -> Dict[Any, Any]: @@ -48,7 +52,7 @@ def fix_and_parse_json( maybe_fixed_json = maybe_fixed_json[: last_brace_index + 1] return json.loads(maybe_fixed_json) except (json.JSONDecodeError, ValueError) as e: - logger.error("参数解析错误", e) + logger.error("参数解析错误", e) def fix_json_using_multiple_techniques(assistant_reply: str) -> Dict[Any, Any]: diff --git a/pilot/chain/audio.py b/pilot/chain/audio.py index 8b197119c..c53f601b3 100644 --- a/pilot/chain/audio.py +++ b/pilot/chain/audio.py @@ -1,2 +1,2 @@ #!/usr/bin/env python3 -# -*- coding:utf-8 -*- \ No newline at end of file +# -*- coding:utf-8 -*- diff --git a/pilot/chain/visual.py b/pilot/chain/visual.py index 1f776fc63..56fafa58b 100644 --- a/pilot/chain/visual.py +++ b/pilot/chain/visual.py @@ -1,2 +1,2 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- \ No newline at end of file +# -*- coding: utf-8 -*- diff --git a/pilot/commands/command.py b/pilot/commands/command.py index 134e93e1d..0200ef6cd 100644 --- a/pilot/commands/command.py +++ b/pilot/commands/command.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from pilot.prompts.generator import PromptGenerator -from typing import Dict, List, NoReturn, Union -from pilot.configs.config import Config - -from pilot.speech import say_text +import json +from typing import Dict from pilot.agent.json_fix_llm import fix_json_using_multiple_techniques from pilot.commands.exception_not_commands import NotCommands -import json +from pilot.configs.config import Config +from pilot.prompts.generator import PromptGenerator +from pilot.speech import say_text def _resolve_pathlike_command_args(command_args): @@ -25,9 +24,9 @@ def _resolve_pathlike_command_args(command_args): def execute_ai_response_json( - prompt: PromptGenerator, - ai_response: str, - user_input: str = None, + prompt: PromptGenerator, + ai_response: str, + user_input: str = None, ) -> str: """ @@ -52,18 +51,14 @@ def execute_ai_response_json( arguments = _resolve_pathlike_command_args(arguments) # Execute command if command_name is not None and command_name.lower().startswith("error"): - result = ( - f"Command {command_name} threw the following error: {arguments}" - ) + result = f"Command {command_name} threw the following error: {arguments}" elif command_name == "human_feedback": result = f"Human feedback: {user_input}" else: for plugin in cfg.plugins: if not plugin.can_handle_pre_command(): continue - command_name, arguments = plugin.pre_command( - command_name, arguments - ) + command_name, arguments = plugin.pre_command(command_name, arguments) command_result = execute_command( command_name, arguments, @@ -74,9 +69,9 @@ def execute_ai_response_json( def execute_command( - command_name: str, - arguments, - prompt: PromptGenerator, + command_name: str, + arguments, + prompt: PromptGenerator, ): """Execute the command and return the result @@ -102,13 +97,15 @@ def execute_command( else: for command in prompt.commands: if ( - command_name == command["label"].lower() - or command_name == command["name"].lower() + command_name == command["label"].lower() + or command_name == command["name"].lower() ): try: # 删除非定义参数 - diff_ags = list(set(arguments.keys()).difference(set(command['args'].keys()))) - for arg_name in diff_ags: + diff_ags = list( + set(arguments.keys()).difference(set(command["args"].keys())) + ) + for arg_name in diff_ags: del arguments[arg_name] print(str(arguments)) return command["function"](**arguments) diff --git a/pilot/commands/commands_load.py b/pilot/commands/commands_load.py index b173eb03a..a6fad3db2 100644 --- a/pilot/commands/commands_load.py +++ b/pilot/commands/commands_load.py @@ -1,19 +1,21 @@ +from typing import Optional + from pilot.configs.config import Config from pilot.prompts.generator import PromptGenerator -from typing import Any, Optional, Type from pilot.prompts.prompt import build_default_prompt_generator class CommandsLoad: """ - Load Plugins Commands Info , help build system prompt! + Load Plugins Commands Info , help build system prompt! """ - def __init__(self)->None: + def __init__(self) -> None: self.command_registry = None - - def getCommandInfos(self, prompt_generator: Optional[PromptGenerator] = None)-> str: + def getCommandInfos( + self, prompt_generator: Optional[PromptGenerator] = None + ) -> str: cfg = Config() if prompt_generator is None: prompt_generator = build_default_prompt_generator() @@ -24,4 +26,4 @@ class CommandsLoad: self.prompt_generator = prompt_generator command_infos = "" command_infos += f"\n\n{prompt_generator.commands()}" - return command_infos \ No newline at end of file + return command_infos diff --git a/pilot/commands/exception_not_commands.py b/pilot/commands/exception_not_commands.py index 88c4d8f1d..7d92f05c0 100644 --- a/pilot/commands/exception_not_commands.py +++ b/pilot/commands/exception_not_commands.py @@ -1,5 +1,4 @@ - class NotCommands(Exception): def __init__(self, message): super().__init__(message) - self.message = message \ No newline at end of file + self.message = message diff --git a/pilot/commands/image_gen.py b/pilot/commands/image_gen.py index 25a6c80fd..d6492e2d9 100644 --- a/pilot/commands/image_gen.py +++ b/pilot/commands/image_gen.py @@ -25,7 +25,7 @@ def generate_image(prompt: str, size: int = 256) -> str: str: The filename of the image """ filename = f"{CFG.workspace_path}/{str(uuid.uuid4())}.jpg" - + # HuggingFace if CFG.image_provider == "huggingface": return generate_image_with_hf(prompt, filename) @@ -72,6 +72,7 @@ def generate_image_with_hf(prompt: str, filename: str) -> str: return f"Saved to disk:{filename}" + def generate_image_with_sd_webui( prompt: str, filename: str, diff --git a/pilot/configs/ai_config.py b/pilot/configs/ai_config.py index d774454e4..ed9b4e2f8 100644 --- a/pilot/configs/ai_config.py +++ b/pilot/configs/ai_config.py @@ -7,13 +7,13 @@ from __future__ import annotations import os import platform from pathlib import Path -from typing import Any, Optional, Type +from typing import Optional import distro import yaml -from pilot.prompts.generator import PromptGenerator from pilot.configs.config import Config +from pilot.prompts.generator import PromptGenerator from pilot.prompts.prompt import build_default_prompt_generator # Soon this will go in a folder where it remembers more stuff about the run(s) @@ -88,7 +88,7 @@ class AIConfig: for goal in config_params.get("ai_goals", []) ] api_budget = config_params.get("api_budget", 0.0) - # type: Type[AIConfig] + # type is Type[AIConfig] return AIConfig(ai_name, ai_role, ai_goals, api_budget) def save(self, config_file: str = SAVE_FILE) -> None: @@ -133,8 +133,6 @@ class AIConfig: "" ) - - cfg = Config() if prompt_generator is None: prompt_generator = build_default_prompt_generator() diff --git a/pilot/configs/config.py b/pilot/configs/config.py index e9ec2bd48..d5e403598 100644 --- a/pilot/configs/config.py +++ b/pilot/configs/config.py @@ -2,15 +2,17 @@ # -*- coding: utf-8 -*- import os -import nltk from typing import List +import nltk from auto_gpt_plugin_template import AutoGPTPluginTemplate + from pilot.singleton import Singleton class Config(metaclass=Singleton): """Configuration class to store the state of bools for different scripts access""" + def __init__(self) -> None: """Initialize the Config class""" @@ -18,7 +20,6 @@ class Config(metaclass=Singleton): self.skip_reprompt = False self.temperature = float(os.getenv("TEMPERATURE", 0.7)) - self.execute_local_commands = ( os.getenv("EXECUTE_LOCAL_COMMANDS", "False") == "True" ) @@ -45,7 +46,6 @@ class Config(metaclass=Singleton): self.milvus_collection = os.getenv("MILVUS_COLLECTION", "dbgpt") self.milvus_secure = os.getenv("MILVUS_SECURE") == "True" - self.authorise_key = os.getenv("AUTHORISE_COMMAND_KEY", "y") self.exit_key = os.getenv("EXIT_KEY", "n") self.image_provider = os.getenv("IMAGE_PROVIDER", True) @@ -62,7 +62,6 @@ class Config(metaclass=Singleton): ) self.speak_mode = False - ### Related configuration of built-in commands self.command_registry = [] @@ -76,7 +75,6 @@ class Config(metaclass=Singleton): os.getenv("EXECUTE_LOCAL_COMMANDS", "False") == "True" ) - ### The associated configuration parameters of the plug-in control the loading and use of the plug-in self.plugins_dir = os.getenv("PLUGINS_DIR", "../../plugins") self.plugins: List[AutoGPTPluginTemplate] = [] @@ -94,35 +92,35 @@ class Config(metaclass=Singleton): else: self.plugins_denylist = [] - ### Local database connection configuration - self.LOCAL_DB_HOST = os.getenv("LOCAL_DB_HOST", "127.0.0.1") - self.LOCAL_DB_PORT = int(os.getenv("LOCAL_DB_PORT", 3306)) - self.LOCAL_DB_USER = os.getenv("LOCAL_DB_USER", "root") - self.LOCAL_DB_PASSWORD = os.getenv("LOCAL_DB_PASSWORD", "aa123456") + self.LOCAL_DB_HOST = os.getenv("LOCAL_DB_HOST", "127.0.0.1") + self.LOCAL_DB_PORT = int(os.getenv("LOCAL_DB_PORT", 3306)) + self.LOCAL_DB_USER = os.getenv("LOCAL_DB_USER", "root") + self.LOCAL_DB_PASSWORD = os.getenv("LOCAL_DB_PASSWORD", "aa123456") ### LLM Model Service Configuration - self.LLM_MODEL = os.getenv("LLM_MODEL", "vicuna-13b") - self.LIMIT_MODEL_CONCURRENCY = int(os.getenv("LIMIT_MODEL_CONCURRENCY", 5)) - self.MAX_POSITION_EMBEDDINGS = int(os.getenv("MAX_POSITION_EMBEDDINGS", 4096)) - self.MODEL_PORT = os.getenv("MODEL_PORT", 8000) - self.MODEL_SERVER = os.getenv("MODEL_SERVER", "http://127.0.0.1" + ":" + str(self.MODEL_PORT)) + self.LLM_MODEL = os.getenv("LLM_MODEL", "vicuna-13b") + self.LIMIT_MODEL_CONCURRENCY = int(os.getenv("LIMIT_MODEL_CONCURRENCY", 5)) + self.MAX_POSITION_EMBEDDINGS = int(os.getenv("MAX_POSITION_EMBEDDINGS", 4096)) + self.MODEL_PORT = os.getenv("MODEL_PORT", 8000) + self.MODEL_SERVER = os.getenv( + "MODEL_SERVER", "http://127.0.0.1" + ":" + str(self.MODEL_PORT) + ) self.ISLOAD_8BIT = os.getenv("ISLOAD_8BIT", "True") == "True" ### Vector Store Configuration - self.VECTOR_STORE_TYPE = os.getenv("VECTOR_STORE_TYPE", "Chroma") - self.MILVUS_URL = os.getenv("MILVUS_URL", "127.0.0.1") - self.MILVUS_PORT = os.getenv("MILVUS_PORT", "19530") - self.MILVUS_USERNAME = os.getenv("MILVUS_USERNAME", None) - self.MILVUS_PASSWORD = os.getenv("MILVUS_PASSWORD", None) - + self.VECTOR_STORE_TYPE = os.getenv("VECTOR_STORE_TYPE", "Chroma") + self.MILVUS_URL = os.getenv("MILVUS_URL", "127.0.0.1") + self.MILVUS_PORT = os.getenv("MILVUS_PORT", "19530") + self.MILVUS_USERNAME = os.getenv("MILVUS_USERNAME", None) + self.MILVUS_PASSWORD = os.getenv("MILVUS_PASSWORD", None) def set_debug_mode(self, value: bool) -> None: """Set the debug mode value""" self.debug_mode = value def set_plugins(self, value: list) -> None: - """Set the plugins value. """ + """Set the plugins value.""" self.plugins = value def set_templature(self, value: int) -> None: @@ -135,4 +133,4 @@ class Config(metaclass=Singleton): def set_last_plugin_return(self, value: bool) -> None: """Set the speak mode value.""" - self.last_plugin_return = value \ No newline at end of file + self.last_plugin_return = value diff --git a/pilot/configs/model_config.py b/pilot/configs/model_config.py index ebd8513e4..9d8c930fd 100644 --- a/pilot/configs/model_config.py +++ b/pilot/configs/model_config.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding:utf-8 -*- -import torch import os -import nltk +import nltk +import torch ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) MODEL_PATH = os.path.join(ROOT_PATH, "models") @@ -16,7 +16,13 @@ DATA_DIR = os.path.join(PILOT_PATH, "data") nltk.data.path = [os.path.join(PILOT_PATH, "nltk_data")] + nltk.data.path -DEVICE = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" +DEVICE = ( + "cuda" + if torch.cuda.is_available() + else "mps" + if torch.backends.mps.is_available() + else "cpu" +) LLM_MODEL_CONFIG = { "flan-t5-base": os.path.join(MODEL_PATH, "flan-t5-base"), "vicuna-13b": os.path.join(MODEL_PATH, "vicuna-13b"), @@ -28,7 +34,7 @@ LLM_MODEL_CONFIG = { "chatglm-6b-int4": os.path.join(MODEL_PATH, "chatglm-6b-int4"), "chatglm-6b": os.path.join(MODEL_PATH, "chatglm-6b"), "text2vec-base": os.path.join(MODEL_PATH, "text2vec-base-chinese"), - "sentence-transforms": os.path.join(MODEL_PATH, "all-MiniLM-L6-v2") + "sentence-transforms": os.path.join(MODEL_PATH, "all-MiniLM-L6-v2"), } @@ -46,5 +52,7 @@ ISDEBUG = False VECTOR_SEARCH_TOP_K = 10 VS_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "vs_store") -KNOWLEDGE_UPLOAD_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") -KNOWLEDGE_CHUNK_SPLIT_SIZE = 100 \ No newline at end of file +KNOWLEDGE_UPLOAD_ROOT_PATH = os.path.join( + os.path.dirname(os.path.dirname(__file__)), "data" +) +KNOWLEDGE_CHUNK_SPLIT_SIZE = 100 diff --git a/pilot/connections/base.py b/pilot/connections/base.py index 318ce17a2..ec41f9273 100644 --- a/pilot/connections/base.py +++ b/pilot/connections/base.py @@ -3,6 +3,6 @@ """We need to design a base class. That other connector can Write with this""" + class BaseConnection: pass - diff --git a/pilot/connections/clickhouse.py b/pilot/connections/clickhouse.py index 23f2660f9..7ea244276 100644 --- a/pilot/connections/clickhouse.py +++ b/pilot/connections/clickhouse.py @@ -4,4 +4,5 @@ class ClickHouseConnector: """ClickHouseConnector""" - pass \ No newline at end of file + + pass diff --git a/pilot/connections/es.py b/pilot/connections/es.py index 819d85ecf..3810c7619 100644 --- a/pilot/connections/es.py +++ b/pilot/connections/es.py @@ -4,4 +4,5 @@ class ElasticSearchConnector: """ElasticSearchConnector""" - pass \ No newline at end of file + + pass diff --git a/pilot/connections/mongo.py b/pilot/connections/mongo.py index b66aefdb3..27f7a610c 100644 --- a/pilot/connections/mongo.py +++ b/pilot/connections/mongo.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- + class MongoConnector: """MongoConnector is a class which connect to mongo and chat with LLM""" - pass \ No newline at end of file + + pass diff --git a/pilot/connections/mysql.py b/pilot/connections/mysql.py index 83da27ec3..a4595c603 100644 --- a/pilot/connections/mysql.py +++ b/pilot/connections/mysql.py @@ -3,27 +3,27 @@ import pymysql -class MySQLOperator: - """Connect MySQL Database fetch MetaData For LLM Prompt - Args: - Usage: +class MySQLOperator: + """Connect MySQL Database fetch MetaData For LLM Prompt + Args: + + Usage: """ default_db = ["information_schema", "performance_schema", "sys", "mysql"] + def __init__(self, user, password, host="localhost", port=3306) -> None: - self.conn = pymysql.connect( host=host, user=user, port=port, passwd=password, charset="utf8mb4", - cursorclass=pymysql.cursors.DictCursor + cursorclass=pymysql.cursors.DictCursor, ) def get_schema(self, schema_name): - with self.conn.cursor() as cursor: _sql = f""" select concat(table_name, "(" , group_concat(column_name), ")") as schema_info from information_schema.COLUMNS where table_schema="{schema_name}" group by TABLE_NAME; @@ -31,7 +31,7 @@ class MySQLOperator: cursor.execute(_sql) results = cursor.fetchall() return results - + def get_index(self, schema_name): pass @@ -43,10 +43,10 @@ class MySQLOperator: cursor.execute(_sql) results = cursor.fetchall() - dbs = [d["Database"] for d in results if d["Database"] not in self.default_db] + dbs = [ + d["Database"] for d in results if d["Database"] not in self.default_db + ] return dbs def get_meta(self, schema_name): pass - - diff --git a/pilot/connections/oracle.py b/pilot/connections/oracle.py index 4ce4e742a..6af8aa0a8 100644 --- a/pilot/connections/oracle.py +++ b/pilot/connections/oracle.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 # -*- coding:utf-8 -*- + class OracleConnector: """OracleConnector""" - pass \ No newline at end of file + + pass diff --git a/pilot/connections/postgres.py b/pilot/connections/postgres.py index 3e1df00ab..48a225293 100644 --- a/pilot/connections/postgres.py +++ b/pilot/connections/postgres.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- - class PostgresConnector: """PostgresConnector is a class which Connector to chat with LLM""" - pass \ No newline at end of file + + pass diff --git a/pilot/connections/redis.py b/pilot/connections/redis.py index ac00ade63..2502562a7 100644 --- a/pilot/connections/redis.py +++ b/pilot/connections/redis.py @@ -4,4 +4,5 @@ class RedisConnector: """RedisConnector""" - pass \ No newline at end of file + + pass diff --git a/pilot/conversation.py b/pilot/conversation.py index 0470bc720..d674e901a 100644 --- a/pilot/conversation.py +++ b/pilot/conversation.py @@ -2,31 +2,34 @@ # -*- coding:utf-8 -*- import dataclasses -from enum import auto, Enum -from typing import List, Any +from enum import Enum, auto +from typing import Any, List + from pilot.configs.config import Config CFG = Config() DB_SETTINGS = { "user": CFG.LOCAL_DB_USER, - "password": CFG.LOCAL_DB_PASSWORD, + "password": CFG.LOCAL_DB_PASSWORD, "host": CFG.LOCAL_DB_HOST, - "port": CFG.LOCAL_DB_PORT + "port": CFG.LOCAL_DB_PORT, } ROLE_USER = "USER" ROLE_ASSISTANT = "Assistant" + class SeparatorStyle(Enum): SINGLE = auto() TWO = auto() THREE = auto() FOUR = auto() -@ dataclasses.dataclass + +@dataclasses.dataclass class Conversation: - """This class keeps all conversation history. """ + """This class keeps all conversation history.""" system: str roles: List[str] @@ -67,7 +70,7 @@ class Conversation: def to_gradio_chatbot(self): ret = [] - for i, (role, msg) in enumerate(self.messages[self.offset:]): + for i, (role, msg) in enumerate(self.messages[self.offset :]): if i % 2 == 0: ret.append([msg, None]) else: @@ -95,15 +98,14 @@ class Conversation: "offset": self.offset, "sep": self.sep, "sep2": self.sep2, - "conv_id": self.conv_id + "conv_id": self.conv_id, } def gen_sqlgen_conversation(dbname): from pilot.connections.mysql import MySQLOperator - mo = MySQLOperator( - **(DB_SETTINGS) - ) + + mo = MySQLOperator(**(DB_SETTINGS)) message = "" @@ -115,7 +117,7 @@ def gen_sqlgen_conversation(dbname): conv_one_shot = Conversation( system="A chat between a curious user and an artificial intelligence assistant, who very familiar with database related knowledge. " - "The assistant gives helpful, detailed, professional and polite answers to the user's questions. ", + "The assistant gives helpful, detailed, professional and polite answers to the user's questions. ", roles=("USER", "Assistant"), messages=( ( @@ -136,20 +138,19 @@ conv_one_shot = Conversation( "whereas PostgreSQL is known for its robustness and reliability.\n" "5. Licensing: MySQL is licensed under the GPL (General Public License), which means that it is free and open-source software, " "whereas PostgreSQL is licensed under the PostgreSQL License, which is also free and open-source but with different terms.\n" - "Ultimately, the choice between MySQL and PostgreSQL depends on the specific needs and requirements of your application. " "Both are excellent database management systems, and choosing the right one " - "for your project requires careful consideration of your application's requirements, performance needs, and scalability." + "for your project requires careful consideration of your application's requirements, performance needs, and scalability.", ), ), offset=2, sep_style=SeparatorStyle.SINGLE, - sep="###" + sep="###", ) conv_vicuna_v1 = Conversation( system="A chat between a curious user and an artificial intelligence assistant. who very familiar with database related knowledge. " - "The assistant gives helpful, detailed, professional and polite answers to the user's questions. ", + "The assistant gives helpful, detailed, professional and polite answers to the user's questions. ", roles=("USER", "ASSISTANT"), messages=(), offset=0, @@ -160,7 +161,7 @@ conv_vicuna_v1 = Conversation( auto_dbgpt_one_shot = Conversation( system="You are DB-GPT, an AI designed to answer questions about HackerNews by query `hackerbews` database in MySQL. " - "Your decisions must always be made independently without seeking user assistance. Play to your strengths as an LLM and pursue simple strategies with no legal complications.", + "Your decisions must always be made independently without seeking user assistance. Play to your strengths as an LLM and pursue simple strategies with no legal complications.", roles=("USER", "ASSISTANT"), messages=( ( @@ -203,7 +204,7 @@ auto_dbgpt_one_shot = Conversation( } } } - """ + """, ), ( "ASSISTANT", @@ -223,8 +224,8 @@ auto_dbgpt_one_shot = Conversation( } } } - """ - ) + """, + ), ), offset=0, sep_style=SeparatorStyle.SINGLE, @@ -233,7 +234,7 @@ auto_dbgpt_one_shot = Conversation( auto_dbgpt_without_shot = Conversation( system="You are DB-GPT, an AI designed to answer questions about users by query `users` database in MySQL. " - "Your decisions must always be made independently without seeking user assistance. Play to your strengths as an LLM and pursue simple strategies with no legal complications.", + "Your decisions must always be made independently without seeking user assistance. Play to your strengths as an LLM and pursue simple strategies with no legal complications.", roles=("USER", "ASSISTANT"), messages=(), offset=0, @@ -259,9 +260,9 @@ conv_qa_prompt_template = """ 基于以下已知的信息, 专业、简要的回 # """ default_conversation = conv_one_shot -conversation_sql_mode ={ +conversation_sql_mode = { "auto_execute_ai_response": "直接执行结果", - "dont_execute_ai_response": "不直接执行结果" + "dont_execute_ai_response": "不直接执行结果", } conversation_types = { @@ -273,7 +274,7 @@ conversation_types = { conv_templates = { "conv_one_shot": conv_one_shot, "vicuna_v1": conv_vicuna_v1, - "auto_dbgpt_one_shot": auto_dbgpt_one_shot + "auto_dbgpt_one_shot": auto_dbgpt_one_shot, } if __name__ == "__main__": diff --git a/pilot/json_utils/json_fix_general.py b/pilot/json_utils/json_fix_general.py index eecf83568..e24d02bbf 100644 --- a/pilot/json_utils/json_fix_general.py +++ b/pilot/json_utils/json_fix_general.py @@ -8,8 +8,8 @@ import re from typing import Optional from pilot.configs.config import Config -from pilot.logs import logger from pilot.json_utils.utilities import extract_char_position +from pilot.logs import logger CFG = Config() diff --git a/pilot/logs.py b/pilot/logs.py index b5a1fad82..52d25b5fd 100644 --- a/pilot/logs.py +++ b/pilot/logs.py @@ -84,7 +84,7 @@ class Logger(metaclass=Singleton): self.chat_plugins = [] def typewriter_log( - self, title="", title_color="", content="", speak_text=False, level=logging.INFO + self, title="", title_color="", content="", speak_text=False, level=logging.INFO ): if speak_text and self.speak_mode: say_text(f"{title}. {content}") @@ -103,26 +103,26 @@ class Logger(metaclass=Singleton): ) def debug( - self, - message, - title="", - title_color="", + self, + message, + title="", + title_color="", ): self._log(title, title_color, message, logging.DEBUG) def info( - self, - message, - title="", - title_color="", + self, + message, + title="", + title_color="", ): self._log(title, title_color, message, logging.INFO) def warn( - self, - message, - title="", - title_color="", + self, + message, + title="", + title_color="", ): self._log(title, title_color, message, logging.WARN) @@ -130,11 +130,11 @@ class Logger(metaclass=Singleton): self._log(title, Fore.RED, message, logging.ERROR) def _log( - self, - title: str = "", - title_color: str = "", - message: str = "", - level=logging.INFO, + self, + title: str = "", + title_color: str = "", + message: str = "", + level=logging.INFO, ): if message: if isinstance(message, list): @@ -178,10 +178,12 @@ class Logger(metaclass=Singleton): log_dir = os.path.join(this_files_dir_path, "../logs") return os.path.abspath(log_dir) + """ Output stream to console using simulated typing """ + class TypingConsoleHandler(logging.StreamHandler): def emit(self, record): min_typing_speed = 0.05 @@ -203,6 +205,7 @@ class TypingConsoleHandler(logging.StreamHandler): except Exception: self.handleError(record) + class ConsoleHandler(logging.StreamHandler): def emit(self, record) -> None: msg = self.format(record) @@ -221,10 +224,10 @@ class DbGptFormatter(logging.Formatter): def format(self, record: LogRecord) -> str: if hasattr(record, "color"): record.title_color = ( - getattr(record, "color") - + getattr(record, "title", "") - + " " - + Style.RESET_ALL + getattr(record, "color") + + getattr(record, "title", "") + + " " + + Style.RESET_ALL ) else: record.title_color = getattr(record, "title", "") @@ -248,9 +251,9 @@ logger = Logger() def print_assistant_thoughts( - ai_name: object, - assistant_reply_json_valid: object, - speak_mode: bool = False, + ai_name: object, + assistant_reply_json_valid: object, + speak_mode: bool = False, ) -> None: assistant_thoughts_reasoning = None assistant_thoughts_plan = None diff --git a/pilot/model/adapter.py b/pilot/model/adapter.py index be8980726..83fad3d5f 100644 --- a/pilot/model/adapter.py +++ b/pilot/model/adapter.py @@ -1,19 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from typing import List from functools import cache +from typing import List -from transformers import ( - AutoTokenizer, - AutoModelForCausalLM, - AutoModel -) +from transformers import AutoModel, AutoModelForCausalLM, AutoTokenizer from pilot.configs.model_config import DEVICE + class BaseLLMAdaper: """The Base class for multi model, in our project. - We will support those model, which performance resemble ChatGPT """ + We will support those model, which performance resemble ChatGPT""" def match(self, model_path: str): return True @@ -28,6 +25,7 @@ class BaseLLMAdaper: llm_model_adapters: List[BaseLLMAdaper] = [] + # Register llm models to adapters, by this we can use multi models. def register_llm_model_adapters(cls): """Register a llm model adapter.""" @@ -39,28 +37,30 @@ def get_llm_model_adapter(model_path: str) -> BaseLLMAdaper: for adapter in llm_model_adapters: if adapter.match(model_path): return adapter - + raise ValueError(f"Invalid model adapter for {model_path}") # TODO support cpu? for practise we support gpt4all or chatglm-6b-int4? + class VicunaLLMAdapater(BaseLLMAdaper): - """Vicuna Adapter """ + """Vicuna Adapter""" + def match(self, model_path: str): - return "vicuna" in model_path + return "vicuna" in model_path def loader(self, model_path: str, from_pretrained_kwagrs: dict): tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False) model = AutoModelForCausalLM.from_pretrained( - model_path, - low_cpu_mem_usage=True, - **from_pretrained_kwagrs + model_path, low_cpu_mem_usage=True, **from_pretrained_kwagrs ) return model, tokenizer + class ChatGLMAdapater(BaseLLMAdaper): """LLM Adatpter for THUDM/chatglm-6b""" + def match(self, model_path: str): return "chatglm" in model_path @@ -73,37 +73,49 @@ class ChatGLMAdapater(BaseLLMAdaper): ).float() return model, tokenizer else: - model = AutoModel.from_pretrained( - model_path, trust_remote_code=True, **from_pretrained_kwargs - ).half().cuda() + model = ( + AutoModel.from_pretrained( + model_path, trust_remote_code=True, **from_pretrained_kwargs + ) + .half() + .cuda() + ) return model, tokenizer - + + class CodeGenAdapter(BaseLLMAdaper): pass + class StarCoderAdapter(BaseLLMAdaper): pass + class T5CodeAdapter(BaseLLMAdaper): pass + class KoalaLLMAdapter(BaseLLMAdaper): - """Koala LLM Adapter which Based LLaMA """ + """Koala LLM Adapter which Based LLaMA""" + def match(self, model_path: str): return "koala" in model_path - + class RWKV4LLMAdapter(BaseLLMAdaper): - """LLM Adapter for RwKv4 """ + """LLM Adapter for RwKv4""" + def match(self, model_path: str): return "RWKV-4" in model_path - + def loader(self, model_path: str, from_pretrained_kwargs: dict): # TODO pass + class GPT4AllAdapter(BaseLLMAdaper): """A light version for someone who want practise LLM use laptop.""" + def match(self, model_path: str): return "gpt4all" in model_path @@ -112,4 +124,4 @@ register_llm_model_adapters(VicunaLLMAdapater) register_llm_model_adapters(ChatGLMAdapater) # TODO Default support vicuna, other model need to tests and Evaluate -register_llm_model_adapters(BaseLLMAdaper) \ No newline at end of file +register_llm_model_adapters(BaseLLMAdaper) diff --git a/pilot/model/base.py b/pilot/model/base.py index 8199198eb..ba8190ea3 100644 --- a/pilot/model/base.py +++ b/pilot/model/base.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from typing import List, TypedDict +from typing import TypedDict + class Message(TypedDict): - """LLM Message object containing usually like (role: content) """ + """LLM Message object containing usually like (role: content)""" role: str content: str - diff --git a/pilot/model/chatglm_llm.py b/pilot/model/chatglm_llm.py index 0f8b74efa..4d72af072 100644 --- a/pilot/model/chatglm_llm.py +++ b/pilot/model/chatglm_llm.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 # -*- coding:utf-8 -*- -import torch +import torch + +from pilot.conversation import ROLE_ASSISTANT, ROLE_USER -from pilot.conversation import ROLE_USER, ROLE_ASSISTANT @torch.inference_mode() -def chatglm_generate_stream(model, tokenizer, params, device, context_len=2048, stream_interval=2): - - """Generate text using chatglm model's chat api """ +def chatglm_generate_stream( + model, tokenizer, params, device, context_len=2048, stream_interval=2 +): + """Generate text using chatglm model's chat api""" prompt = params["prompt"] temperature = float(params.get("temperature", 1.0)) top_p = float(params.get("top_p", 1.0)) @@ -19,31 +21,38 @@ def chatglm_generate_stream(model, tokenizer, params, device, context_len=2048, "do_sample": True if temperature > 1e-5 else False, "top_p": top_p, "repetition_penalty": 1.0, - "logits_processor": None + "logits_processor": None, } if temperature > 1e-5: generate_kwargs["temperature"] = temperature # TODO, Fix this - hist = [] + hist = [] messages = prompt.split(stop) - # Add history chat to hist for model. + # Add history chat to hist for model. for i in range(1, len(messages) - 2, 2): - hist.append((messages[i].split(ROLE_USER + ":")[1], messages[i+1].split(ROLE_ASSISTANT + ":")[1])) + hist.append( + ( + messages[i].split(ROLE_USER + ":")[1], + messages[i + 1].split(ROLE_ASSISTANT + ":")[1], + ) + ) query = messages[-2].split(ROLE_USER + ":")[1] print("Query Message: ", query) output = "" i = 0 - for i, (response, new_hist) in enumerate(model.stream_chat(tokenizer, query, hist, **generate_kwargs)): + for i, (response, new_hist) in enumerate( + model.stream_chat(tokenizer, query, hist, **generate_kwargs) + ): if echo: output = query + " " + response else: output = response - + yield output - yield output \ No newline at end of file + yield output diff --git a/pilot/model/compression.py b/pilot/model/compression.py index 9c8c25d08..83b681c6f 100644 --- a/pilot/model/compression.py +++ b/pilot/model/compression.py @@ -3,14 +3,15 @@ import dataclasses import torch -from torch import Tensor import torch.nn as nn +from torch import Tensor from torch.nn import functional as F @dataclasses.dataclass class CompressionConfig: """Group-wise quantization.""" + num_bits: int group_size: int group_dim: int @@ -19,7 +20,8 @@ class CompressionConfig: default_compression_config = CompressionConfig( - num_bits=8, group_size=256, group_dim=1, symmetric=True, enabled=True) + num_bits=8, group_size=256, group_dim=1, symmetric=True, enabled=True +) class CLinear(nn.Module): @@ -40,8 +42,11 @@ def compress_module(module, target_device): for attr_str in dir(module): target_attr = getattr(module, attr_str) if type(target_attr) == torch.nn.Linear: - setattr(module, attr_str, - CLinear(target_attr.weight, target_attr.bias, target_device)) + setattr( + module, + attr_str, + CLinear(target_attr.weight, target_attr.bias, target_device), + ) for name, child in module.named_children(): compress_module(child, target_device) @@ -52,22 +57,31 @@ def compress(tensor, config): return tensor group_size, num_bits, group_dim, symmetric = ( - config.group_size, config.num_bits, config.group_dim, config.symmetric) + config.group_size, + config.num_bits, + config.group_dim, + config.symmetric, + ) assert num_bits <= 8 original_shape = tensor.shape num_groups = (original_shape[group_dim] + group_size - 1) // group_size - new_shape = (original_shape[:group_dim] + (num_groups, group_size) + - original_shape[group_dim+1:]) + new_shape = ( + original_shape[:group_dim] + + (num_groups, group_size) + + original_shape[group_dim + 1 :] + ) # Pad pad_len = (group_size - original_shape[group_dim] % group_size) % group_size if pad_len != 0: - pad_shape = original_shape[:group_dim] + (pad_len,) + original_shape[group_dim+1:] - tensor = torch.cat([ - tensor, - torch.zeros(pad_shape, dtype=tensor.dtype, device=tensor.device)], - dim=group_dim) + pad_shape = ( + original_shape[:group_dim] + (pad_len,) + original_shape[group_dim + 1 :] + ) + tensor = torch.cat( + [tensor, torch.zeros(pad_shape, dtype=tensor.dtype, device=tensor.device)], + dim=group_dim, + ) data = tensor.view(new_shape) # Quantize @@ -78,7 +92,7 @@ def compress(tensor, config): data = data.clamp_(-B, B).round_().to(torch.int8) return data, scale, original_shape else: - B = 2 ** num_bits - 1 + B = 2**num_bits - 1 mn = torch.min(data, dim=group_dim + 1, keepdim=True)[0] mx = torch.max(data, dim=group_dim + 1, keepdim=True)[0] @@ -96,7 +110,11 @@ def decompress(packed_data, config): return packed_data group_size, num_bits, group_dim, symmetric = ( - config.group_size, config.num_bits, config.group_dim, config.symmetric) + config.group_size, + config.num_bits, + config.group_dim, + config.symmetric, + ) # Dequantize if symmetric: @@ -111,9 +129,10 @@ def decompress(packed_data, config): pad_len = (group_size - original_shape[group_dim] % group_size) % group_size if pad_len: padded_original_shape = ( - original_shape[:group_dim] + - (original_shape[group_dim] + pad_len,) + - original_shape[group_dim+1:]) + original_shape[:group_dim] + + (original_shape[group_dim] + pad_len,) + + original_shape[group_dim + 1 :] + ) data = data.reshape(padded_original_shape) indices = [slice(0, x) for x in original_shape] return data[indices].contiguous() diff --git a/pilot/model/inference.py b/pilot/model/inference.py index a677c0339..042f9954b 100644 --- a/pilot/model/inference.py +++ b/pilot/model/inference.py @@ -3,11 +3,12 @@ import torch -@torch.inference_mode() -def generate_stream(model, tokenizer, params, device, - context_len=4096, stream_interval=2): - """Fork from fastchat: https://github.com/lm-sys/FastChat/blob/main/fastchat/serve/inference.py """ +@torch.inference_mode() +def generate_stream( + model, tokenizer, params, device, context_len=4096, stream_interval=2 +): + """Fork from fastchat: https://github.com/lm-sys/FastChat/blob/main/fastchat/serve/inference.py""" prompt = params["prompt"] l_prompt = len(prompt) temperature = float(params.get("temperature", 1.0)) @@ -22,17 +23,19 @@ def generate_stream(model, tokenizer, params, device, for i in range(max_new_tokens): if i == 0: - out = model( - torch.as_tensor([input_ids], device=device), use_cache=True) + out = model(torch.as_tensor([input_ids], device=device), use_cache=True) logits = out.logits past_key_values = out.past_key_values else: attention_mask = torch.ones( - 1, past_key_values[0][0].shape[-2] + 1, device=device) - out = model(input_ids=torch.as_tensor([[token]], device=device), - use_cache=True, - attention_mask=attention_mask, - past_key_values=past_key_values) + 1, past_key_values[0][0].shape[-2] + 1, device=device + ) + out = model( + input_ids=torch.as_tensor([[token]], device=device), + use_cache=True, + attention_mask=attention_mask, + past_key_values=past_key_values, + ) logits = out.logits past_key_values = out.past_key_values @@ -68,9 +71,12 @@ def generate_stream(model, tokenizer, params, device, del past_key_values + @torch.inference_mode() -def generate_output(model, tokenizer, params, device, context_len=4096, stream_interval=2): - """Fork from fastchat: https://github.com/lm-sys/FastChat/blob/main/fastchat/serve/inference.py """ +def generate_output( + model, tokenizer, params, device, context_len=4096, stream_interval=2 +): + """Fork from fastchat: https://github.com/lm-sys/FastChat/blob/main/fastchat/serve/inference.py""" prompt = params["prompt"] l_prompt = len(prompt) @@ -78,7 +84,6 @@ def generate_output(model, tokenizer, params, device, context_len=4096, stream_i max_new_tokens = int(params.get("max_new_tokens", 2048)) stop_str = params.get("stop", None) - input_ids = tokenizer(prompt).input_ids output_ids = list(input_ids) @@ -87,17 +92,19 @@ def generate_output(model, tokenizer, params, device, context_len=4096, stream_i for i in range(max_new_tokens): if i == 0: - out = model( - torch.as_tensor([input_ids], device=device), use_cache=True) + out = model(torch.as_tensor([input_ids], device=device), use_cache=True) logits = out.logits past_key_values = out.past_key_values else: attention_mask = torch.ones( - 1, past_key_values[0][0].shape[-2] + 1, device=device) - out = model(input_ids=torch.as_tensor([[token]], device=device), - use_cache=True, - attention_mask=attention_mask, - past_key_values=past_key_values) + 1, past_key_values[0][0].shape[-2] + 1, device=device + ) + out = model( + input_ids=torch.as_tensor([[token]], device=device), + use_cache=True, + attention_mask=attention_mask, + past_key_values=past_key_values, + ) logits = out.logits past_key_values = out.past_key_values @@ -120,7 +127,6 @@ def generate_output(model, tokenizer, params, device, context_len=4096, stream_i else: stopped = False - if i % stream_interval == 0 or i == max_new_tokens - 1 or stopped: output = tokenizer.decode(output_ids, skip_special_tokens=True) pos = output.rfind(stop_str, l_prompt) @@ -133,8 +139,11 @@ def generate_output(model, tokenizer, params, device, context_len=4096, stream_i break del past_key_values + @torch.inference_mode() -def generate_output_ex(model, tokenizer, params, device, context_len=2048, stream_interval=2): +def generate_output_ex( + model, tokenizer, params, device, context_len=2048, stream_interval=2 +): prompt = params["prompt"] temperature = float(params.get("temperature", 1.0)) max_new_tokens = int(params.get("max_new_tokens", 2048)) @@ -161,20 +170,20 @@ def generate_output_ex(model, tokenizer, params, device, context_len=2048, strea for i in range(max_new_tokens): if i == 0: - out = model( - torch.as_tensor([input_ids], device=device), use_cache=True) + out = model(torch.as_tensor([input_ids], device=device), use_cache=True) logits = out.logits past_key_values = out.past_key_values else: - out = model(input_ids=torch.as_tensor([[token]], device=device), - use_cache=True, - past_key_values=past_key_values) + out = model( + input_ids=torch.as_tensor([[token]], device=device), + use_cache=True, + past_key_values=past_key_values, + ) logits = out.logits past_key_values = out.past_key_values last_token_logits = logits[0][-1] - if temperature < 1e-4: token = int(torch.argmax(last_token_logits)) else: @@ -188,7 +197,6 @@ def generate_output_ex(model, tokenizer, params, device, context_len=2048, strea else: stopped = False - output = tokenizer.decode(output_ids, skip_special_tokens=True) # print("Partial output:", output) for stop_str in stop_strings: @@ -211,7 +219,7 @@ def generate_output_ex(model, tokenizer, params, device, context_len=2048, strea del past_key_values if pos != -1: return output[:pos] - return output + return output @torch.inference_mode() diff --git a/pilot/model/llm/base.py b/pilot/model/llm/base.py index 435cc0d5f..581837347 100644 --- a/pilot/model/llm/base.py +++ b/pilot/model/llm/base.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from dataclasses import dataclass, field -from typing import List, TypedDict +from dataclasses import dataclass +from typing import TypedDict class Message(TypedDict): - """Vicuna Message object containing a role and the message content """ + """Vicuna Message object containing a role and the message content""" role: str content: str @@ -18,12 +18,15 @@ class ModelInfo: Would be lovely to eventually get this directly from APIs """ + name: str max_tokens: int + @dataclass class LLMResponse: """Standard response struct for a response from a LLM model.""" + model_info = ModelInfo @@ -31,4 +34,4 @@ class LLMResponse: class ChatModelResponse(LLMResponse): """Standard response struct for a response from an LLM model.""" - content: str = None \ No newline at end of file + content: str = None diff --git a/pilot/model/llm/llm_utils.py b/pilot/model/llm/llm_utils.py index a68860ee6..e2bf631cc 100644 --- a/pilot/model/llm/llm_utils.py +++ b/pilot/model/llm/llm_utils.py @@ -2,35 +2,39 @@ # -*- coding: utf-8 -*- import abc -import time import functools -from typing import List, Optional -from pilot.model.llm.base import Message -from pilot.conversation import conv_templates, Conversation, conv_one_shot, auto_dbgpt_one_shot +import time +from typing import Optional + from pilot.configs.config import Config +from pilot.conversation import ( + Conversation, + auto_dbgpt_one_shot, + conv_one_shot, + conv_templates, +) +from pilot.model.llm.base import Message # TODO Rewrite this def retry_stream_api( - num_retries: int = 10, - backoff_base: float = 2.0, - warn_user: bool = True -): + num_retries: int = 10, backoff_base: float = 2.0, warn_user: bool = True +): """Retry an Vicuna Server call. - Args: - num_retries int: Number of retries. Defaults to 10. - backoff_base float: Base for exponential backoff. Defaults to 2. - warn_user bool: Whether to warn the user. Defaults to True. + Args: + num_retries int: Number of retries. Defaults to 10. + backoff_base float: Base for exponential backoff. Defaults to 2. + warn_user bool: Whether to warn the user. Defaults to True. """ retry_limit_msg = f"Error: Reached rate limit, passing..." - backoff_msg = (f"Error: API Bad gateway. Waiting {{backoff}} seconds...") + backoff_msg = f"Error: API Bad gateway. Waiting {{backoff}} seconds..." def _wrapper(func): @functools.wraps(func) def _wrapped(*args, **kwargs): user_warned = not warn_user - num_attempts = num_retries + 1 # +1 for the first attempt + num_attempts = num_retries + 1 # +1 for the first attempt for attempt in range(1, num_attempts + 1): try: return func(*args, **kwargs) @@ -39,10 +43,13 @@ def retry_stream_api( raise backoff = backoff_base ** (attempt + 2) - time.sleep(backoff) + time.sleep(backoff) + return _wrapped + return _wrapper + # Overly simple abstraction util we create something better # simple retry mechanism when getting a rate error or a bad gateway def create_chat_competion( @@ -52,15 +59,15 @@ def create_chat_competion( max_new_tokens: Optional[int] = None, ) -> str: """Create a chat completion using the Vicuna-13b - - Args: - messages(List[Message]): The messages to send to the chat completion - model (str, optional): The model to use. Default to None. - temperature (float, optional): The temperature to use. Defaults to 0.7. - max_tokens (int, optional): The max tokens to use. Defaults to None. - Returns: - str: The response from the chat completion + Args: + messages(List[Message]): The messages to send to the chat completion + model (str, optional): The model to use. Default to None. + temperature (float, optional): The temperature to use. Defaults to 0.7. + max_tokens (int, optional): The max tokens to use. Defaults to None. + + Returns: + str: The response from the chat completion """ cfg = Config() if temperature is None: @@ -77,7 +84,7 @@ class ChatIO(abc.ABC): @abc.abstractmethod def prompt_for_input(self, role: str) -> str: """Prompt for input from a role.""" - + @abc.abstractmethod def prompt_for_output(self, role: str) -> str: """Prompt for output from a role.""" @@ -105,4 +112,3 @@ class SimpleChatIO(ChatIO): print(" ".join(outputs[pre:]), flush=True) return " ".join(outputs) - diff --git a/pilot/model/llm/monkey_patch.py b/pilot/model/llm/monkey_patch.py index a50481281..f3656a159 100644 --- a/pilot/model/llm/monkey_patch.py +++ b/pilot/model/llm/monkey_patch.py @@ -5,8 +5,8 @@ import math from typing import Optional, Tuple import torch -from torch import nn import transformers +from torch import nn def rotate_half(x): @@ -116,8 +116,8 @@ def replace_llama_attn_with_non_inplace_operations(): """Avoid bugs in mps backend by not using in-place operations.""" transformers.models.llama.modeling_llama.LlamaAttention.forward = forward -import transformers +import transformers def replace_llama_attn_with_non_inplace_operations(): diff --git a/pilot/model/llm_utils.py b/pilot/model/llm_utils.py index 196246118..118d45f97 100644 --- a/pilot/model/llm_utils.py +++ b/pilot/model/llm_utils.py @@ -2,31 +2,33 @@ # -*- coding:utf-8 -*- from typing import List, Optional -from pilot.model.base import Message + from pilot.configs.config import Config +from pilot.model.base import Message from pilot.server.llmserver import generate_output + def create_chat_completion( - messages: List[Message], # type: ignore + messages: List[Message], # type: ignore model: Optional[str] = None, temperature: float = None, max_tokens: Optional[int] = None, ) -> str: - """Create a chat completion using the vicuna local model + """Create a chat completion using the vicuna local model - Args: - messages(List[Message]): The messages to send to the chat completion - model (str, optional): The model to use. Defaults to None. - temperature (float, optional): The temperature to use. Defaults to 0.7. - max_tokens (int, optional): The max tokens to use. Defaults to None - - Returns: - str: The response from chat completion + Args: + messages(List[Message]): The messages to send to the chat completion + model (str, optional): The model to use. Defaults to None. + temperature (float, optional): The temperature to use. Defaults to 0.7. + max_tokens (int, optional): The max tokens to use. Defaults to None + + Returns: + str: The response from chat completion """ cfg = Config() if temperature is None: temperature = cfg.temperature - + for plugin in cfg.plugins: if plugin.can_handle_chat_completion( messages=messages, diff --git a/pilot/model/loader.py b/pilot/model/loader.py index bd31bae0a..a228acbf7 100644 --- a/pilot/model/loader.py +++ b/pilot/model/loader.py @@ -1,16 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import torch import sys import warnings -from pilot.singleton import Singleton from typing import Optional -from pilot.model.compression import compress_module -from pilot.model.adapter import get_llm_model_adapter -from pilot.utils import get_gpu_memory + +import torch + from pilot.configs.model_config import DEVICE +from pilot.model.adapter import get_llm_model_adapter +from pilot.model.compression import compress_module from pilot.model.llm.monkey_patch import replace_llama_attn_with_non_inplace_operations +from pilot.singleton import Singleton +from pilot.utils import get_gpu_memory + def raise_warning_for_incompatible_cpu_offloading_configuration( device: str, load_8bit: bool, cpu_offloading: bool @@ -40,27 +43,31 @@ def raise_warning_for_incompatible_cpu_offloading_configuration( class ModelLoader(metaclass=Singleton): """Model loader is a class for model load - + Args: model_path - TODO: multi model support. + TODO: multi model support. """ kwargs = {} - def __init__(self, - model_path) -> None: - + def __init__(self, model_path) -> None: self.device = DEVICE - self.model_path = model_path + self.model_path = model_path self.kwargs = { "torch_dtype": torch.float16, "device_map": "auto", } # TODO multi gpu support - def loader(self, num_gpus, load_8bit=False, debug=False, cpu_offloading=False, max_gpu_memory: Optional[str]=None): - + def loader( + self, + num_gpus, + load_8bit=False, + debug=False, + cpu_offloading=False, + max_gpu_memory: Optional[str] = None, + ): if self.device == "cpu": kwargs = {"torch_dtype": torch.float32} @@ -72,7 +79,7 @@ class ModelLoader(metaclass=Singleton): kwargs["device_map"] = "auto" if max_gpu_memory is None: kwargs["device_map"] = "sequential" - + available_gpu_memory = get_gpu_memory(num_gpus) kwargs["max_memory"] = { i: str(int(available_gpu_memory[i] * 0.85)) + "GiB" @@ -99,13 +106,14 @@ class ModelLoader(metaclass=Singleton): "8-bit quantization is not supported for multi-gpu inference" ) else: - compress_module(model, self.device) + compress_module(model, self.device) - if (self.device == "cuda" and num_gpus == 1 and not cpu_offloading) or self.device == "mps": + if ( + self.device == "cuda" and num_gpus == 1 and not cpu_offloading + ) or self.device == "mps": model.to(self.device) if debug: print(model) return model, tokenizer - diff --git a/pilot/model/vicuna_llm.py b/pilot/model/vicuna_llm.py index 63788a619..b38249a98 100644 --- a/pilot/model/vicuna_llm.py +++ b/pilot/model/vicuna_llm.py @@ -2,25 +2,34 @@ # -*- coding:utf-8 -*- import json -import requests +from typing import Any, List, Mapping, Optional from urllib.parse import urljoin + +import requests from langchain.embeddings.base import Embeddings -from pydantic import BaseModel -from typing import Any, Mapping, Optional, List from langchain.llms.base import LLM +from pydantic import BaseModel + from pilot.configs.config import Config CFG = Config() + + class VicunaLLM(LLM): - vicuna_generate_path = "generate_stream" - def _call(self, prompt: str, temperature: float, max_new_tokens: int, stop: Optional[List[str]] = None) -> str: + def _call( + self, + prompt: str, + temperature: float, + max_new_tokens: int, + stop: Optional[List[str]] = None, + ) -> str: params = { "prompt": prompt, "temperature": temperature, "max_new_tokens": max_new_tokens, - "stop": stop + "stop": stop, } response = requests.post( url=urljoin(CFG.MODEL_SERVER, self.vicuna_generate_path), @@ -41,10 +50,9 @@ class VicunaLLM(LLM): def _identifying_params(self) -> Mapping[str, Any]: return {} - + class VicunaEmbeddingLLM(BaseModel, Embeddings): - vicuna_embedding_path = "embedding" def _call(self, prompt: str) -> str: @@ -53,15 +61,13 @@ class VicunaEmbeddingLLM(BaseModel, Embeddings): response = requests.post( url=urljoin(CFG.MODEL_SERVER, self.vicuna_embedding_path), - json={ - "prompt": p - } + json={"prompt": p}, ) response.raise_for_status() return response.json()["response"] def embed_documents(self, texts: List[str]) -> List[List[float]]: - """ Call out to Vicuna's server embedding endpoint for embedding search docs. + """Call out to Vicuna's server embedding endpoint for embedding search docs. Args: texts: The list of text to embed @@ -73,17 +79,15 @@ class VicunaEmbeddingLLM(BaseModel, Embeddings): for text in texts: response = self.embed_query(text) results.append(response) - return results - + return results def embed_query(self, text: str) -> List[float]: - """ Call out to Vicuna's server embedding endpoint for embedding query text. - - Args: + """Call out to Vicuna's server embedding endpoint for embedding query text. + + Args: text: The text to embed. Returns: Embedding for the text """ embedding = self._call(text) return embedding - diff --git a/pilot/plugins.py b/pilot/plugins.py index 28f33a5a4..f1cd1c962 100644 --- a/pilot/plugins.py +++ b/pilot/plugins.py @@ -1,11 +1,10 @@ """加载组件""" -import importlib import json import os import zipfile from pathlib import Path -from typing import List, Optional, Tuple +from typing import List from urllib.parse import urlparse from zipimport import zipimporter @@ -15,6 +14,7 @@ from auto_gpt_plugin_template import AutoGPTPluginTemplate from pilot.configs.config import Config from pilot.logs import logger + def inspect_zip_for_modules(zip_path: str, debug: bool = False) -> list[str]: """ Loader zip plugin file. Native support Auto_gpt_plugin @@ -36,6 +36,7 @@ def inspect_zip_for_modules(zip_path: str, debug: bool = False) -> list[str]: logger.debug(f"Module '__init__.py' not found in the zipfile @ {zip_path}.") return result + def write_dict_to_json_file(data: dict, file_path: str) -> None: """ Write a dictionary to a JSON file. @@ -46,6 +47,7 @@ def write_dict_to_json_file(data: dict, file_path: str) -> None: with open(file_path, "w") as file: json.dump(data, file, indent=4) + def create_directory_if_not_exists(directory_path: str) -> bool: """ Create a directory if it does not exist. @@ -66,6 +68,7 @@ def create_directory_if_not_exists(directory_path: str) -> bool: logger.info(f"Directory {directory_path} already exists") return True + def scan_plugins(cfg: Config, debug: bool = False) -> List[AutoGPTPluginTemplate]: """Scan the plugins directory for plugins and loads them. diff --git a/pilot/prompts/auto_mode_prompt.py b/pilot/prompts/auto_mode_prompt.py index 86f707783..b47d24a76 100644 --- a/pilot/prompts/auto_mode_prompt.py +++ b/pilot/prompts/auto_mode_prompt.py @@ -1,19 +1,21 @@ -from pilot.prompts.generator import PromptGenerator -from typing import Any, Optional, Type -import os import platform -from pathlib import Path +from typing import Optional import distro import yaml + from pilot.configs.config import Config -from pilot.prompts.prompt import build_default_prompt_generator, DEFAULT_PROMPT_OHTER, DEFAULT_TRIGGERING_PROMPT +from pilot.prompts.generator import PromptGenerator +from pilot.prompts.prompt import ( + DEFAULT_PROMPT_OHTER, + DEFAULT_TRIGGERING_PROMPT, + build_default_prompt_generator, +) class AutoModePrompt: - """ + """ """ - """ def __init__( self, ai_goals: list | None = None, @@ -36,23 +38,21 @@ class AutoModePrompt: self.command_registry = None def construct_follow_up_prompt( - self, - user_input:[str], - last_auto_return: str = None, - prompt_generator: Optional[PromptGenerator] = None - )-> str: + self, + user_input: [str], + last_auto_return: str = None, + prompt_generator: Optional[PromptGenerator] = None, + ) -> str: """ - Build complete prompt information based on subsequent dialogue information entered by the user - Args: - self: - prompt_generator: + Build complete prompt information based on subsequent dialogue information entered by the user + Args: + self: + prompt_generator: - Returns: + Returns: - """ - prompt_start = ( - DEFAULT_PROMPT_OHTER - ) + """ + prompt_start = DEFAULT_PROMPT_OHTER if prompt_generator is None: prompt_generator = build_default_prompt_generator() prompt_generator.goals = user_input @@ -64,12 +64,13 @@ class AutoModePrompt: continue prompt_generator = plugin.post_prompt(prompt_generator) - full_prompt = f"{prompt_start}\n\nGOALS:\n\n" - if not self.ai_goals : + if not self.ai_goals: self.ai_goals = user_input for i, goal in enumerate(self.ai_goals): - full_prompt += f"{i+1}.According to the provided Schema information, {goal}\n" + full_prompt += ( + f"{i+1}.According to the provided Schema information, {goal}\n" + ) # if last_auto_return == None: # full_prompt += f"{cfg.last_plugin_return}\n\n" # else: @@ -82,10 +83,10 @@ class AutoModePrompt: return full_prompt def construct_first_prompt( - self, - fisrt_message: [str]=[], - db_schemes: str=None, - prompt_generator: Optional[PromptGenerator] = None + self, + fisrt_message: [str] = [], + db_schemes: str = None, + prompt_generator: Optional[PromptGenerator] = None, ) -> str: """ Build complete prompt information based on the initial dialogue information entered by the user @@ -125,16 +126,18 @@ class AutoModePrompt: # Construct full prompt full_prompt = f"{prompt_start}\n\nGOALS:\n\n" - if not self.ai_goals : + if not self.ai_goals: self.ai_goals = fisrt_message for i, goal in enumerate(self.ai_goals): - full_prompt += f"{i+1}.According to the provided Schema information,{goal}\n" - if db_schemes: - full_prompt += f"\nSchema:\n\n" + full_prompt += ( + f"{i+1}.According to the provided Schema information,{goal}\n" + ) + if db_schemes: + full_prompt += f"\nSchema:\n\n" full_prompt += f"{db_schemes}" # if self.api_budget > 0.0: # full_prompt += f"\nIt takes money to let you run. Your API budget is ${self.api_budget:.3f}" self.prompt_generator = prompt_generator full_prompt += f"\n\n{prompt_generator.generate_prompt_string()}" - return full_prompt \ No newline at end of file + return full_prompt diff --git a/pilot/prompts/generator.py b/pilot/prompts/generator.py index fc45f9512..c470ff5a5 100644 --- a/pilot/prompts/generator.py +++ b/pilot/prompts/generator.py @@ -149,7 +149,7 @@ class PromptGenerator: f"Resources:\n{self._generate_numbered_list(self.resources)}\n\n" "Performance Evaluation:\n" f"{self._generate_numbered_list(self.performance_evaluation)}\n\n" - "You should only respond in JSON format as described below and ensure the" + "You should only respond in JSON format as described below and ensure the" "response can be parsed by Python json.loads \nResponse" f" Format: \n{formatted_response_format}" ) diff --git a/pilot/prompts/prompt.py b/pilot/prompts/prompt.py index 8d050adf4..d46b69ad5 100644 --- a/pilot/prompts/prompt.py +++ b/pilot/prompts/prompt.py @@ -1,17 +1,14 @@ - from pilot.configs.config import Config from pilot.prompts.generator import PromptGenerator - CFG = Config() DEFAULT_TRIGGERING_PROMPT = ( "Determine which next command to use, and respond using the format specified above" ) -DEFAULT_PROMPT_OHTER = ( - "Previous response was excellent. Please response according to the requirements based on the new goal" -) +DEFAULT_PROMPT_OHTER = "Previous response was excellent. Please response according to the requirements based on the new goal" + def build_default_prompt_generator() -> PromptGenerator: """ @@ -36,17 +33,15 @@ def build_default_prompt_generator() -> PromptGenerator: ) # prompt_generator.add_constraint("No user assistance") - prompt_generator.add_constraint( - 'Only output one correct JSON response at a time' - ) + prompt_generator.add_constraint("Only output one correct JSON response at a time") prompt_generator.add_constraint( 'Exclusively use the commands listed in double quotes e.g. "command name"' ) prompt_generator.add_constraint( - 'If there is SQL in the args parameter, ensure to use the database and table definitions in Schema, and ensure that the fields and table names are in the definition' + "If there is SQL in the args parameter, ensure to use the database and table definitions in Schema, and ensure that the fields and table names are in the definition" ) prompt_generator.add_constraint( - 'The generated command args need to comply with the definition of the command' + "The generated command args need to comply with the definition of the command" ) # Add resources to the PromptGenerator object diff --git a/pilot/pturning/lora/finetune.py b/pilot/pturning/lora/finetune.py index 91ec07d0a..c661e4405 100644 --- a/pilot/pturning/lora/finetune.py +++ b/pilot/pturning/lora/finetune.py @@ -1,26 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import os import json -import transformers -from transformers import LlamaTokenizer, LlamaForCausalLM +import os -from typing import List +import pandas as pd +import torch +import transformers +from datasets import load_dataset from peft import ( LoraConfig, get_peft_model, get_peft_model_state_dict, prepare_model_for_int8_training, ) +from transformers import LlamaForCausalLM, LlamaTokenizer -import torch -from datasets import load_dataset -import pandas as pd from pilot.configs.config import Config - - from pilot.configs.model_config import DATA_DIR, LLM_MODEL_CONFIG + device = "cuda" if torch.cuda.is_available() else "cpu" CUTOFF_LEN = 50 @@ -28,6 +26,7 @@ df = pd.read_csv(os.path.join(DATA_DIR, "BTC_Tweets_Updated.csv")) CFG = Config() + def sentiment_score_to_name(score: float): if score > 0: return "Positive" @@ -40,16 +39,18 @@ dataset_data = [ { "instruction": "Detect the sentiment of the tweet.", "input": row_dict["Tweet"], - "output": sentiment_score_to_name(row_dict["New_Sentiment_State"]) - } + "output": sentiment_score_to_name(row_dict["New_Sentiment_State"]), + } for row_dict in df.to_dict(orient="records") ] with open(os.path.join(DATA_DIR, "alpaca-bitcoin-sentiment-dataset.json"), "w") as f: - json.dump(dataset_data, f) + json.dump(dataset_data, f) -data = load_dataset("json", data_files=os.path.join(DATA_DIR, "alpaca-bitcoin-sentiment-dataset.json")) +data = load_dataset( + "json", data_files=os.path.join(DATA_DIR, "alpaca-bitcoin-sentiment-dataset.json") +) print(data["train"]) BASE_MODEL = LLM_MODEL_CONFIG[CFG.LLM_MODEL] @@ -57,13 +58,14 @@ model = LlamaForCausalLM.from_pretrained( BASE_MODEL, torch_dtype=torch.float16, device_map="auto", - offload_folder=os.path.join(DATA_DIR, "vicuna-lora") -) + offload_folder=os.path.join(DATA_DIR, "vicuna-lora"), +) tokenizer = LlamaTokenizer.from_pretrained(BASE_MODEL) -tokenizer.pad_token_id = (0) +tokenizer.pad_token_id = 0 tokenizer.padding_side = "left" + def generate_prompt(data_point): return f"""Blow is an instruction that describes a task, paired with an input that provide future context. Write a response that appropriately completes the request. #noqa: @@ -76,6 +78,7 @@ def generate_prompt(data_point): {data_point["output"]} """ + def tokenize(prompt, add_eos_token=True): result = tokenizer( prompt, @@ -85,30 +88,29 @@ def tokenize(prompt, add_eos_token=True): return_tensors=None, ) - if (result["input_ids"][-1] != tokenizer.eos_token_id and len(result["input_ids"]) < CUTOFF_LEN and add_eos_token): + if ( + result["input_ids"][-1] != tokenizer.eos_token_id + and len(result["input_ids"]) < CUTOFF_LEN + and add_eos_token + ): result["input_ids"].append(tokenizer.eos_token_id) result["attention_mask"].append(1) result["labels"] = result["input_ids"].copy() return result + def generate_and_tokenize_prompt(data_point): full_prompt = generate_prompt(data_point) tokenized_full_prompt = tokenize(full_prompt) return tokenized_full_prompt -train_val = data["train"].train_test_split( - test_size=200, shuffle=True, seed=42 -) +train_val = data["train"].train_test_split(test_size=200, shuffle=True, seed=42) -train_data = ( - train_val["train"].map(generate_and_tokenize_prompt) -) +train_data = train_val["train"].map(generate_and_tokenize_prompt) -val_data = ( - train_val["test"].map(generate_and_tokenize_prompt) -) +val_data = train_val["test"].map(generate_and_tokenize_prompt) # Training LORA_R = 8 @@ -129,7 +131,7 @@ OUTPUT_DIR = "experiments" # We can now prepare model for training model = prepare_model_for_int8_training(model) config = LoraConfig( - r = LORA_R, + r=LORA_R, lora_alpha=LORA_ALPHA, target_modules=LORA_TARGET_MODULES, lora_dropout=LORA_DROPOUT, @@ -156,7 +158,7 @@ training_arguments = transformers.TrainingArguments( output_dir=OUTPUT_DIR, save_total_limit=3, load_best_model_at_end=True, - report_to="tensorboard" + report_to="tensorboard", ) data_collector = transformers.DataCollatorForSeq2Seq( @@ -168,15 +170,13 @@ trainer = transformers.Trainer( train_dataset=train_data, eval_dataset=val_data, args=training_arguments, - data_collector=data_collector + data_collector=data_collector, ) model.config.use_cache = False old_state_dict = model.state_dict model.state_dict = ( - lambda self, *_, **__: get_peft_model_state_dict( - self, old_state_dict() - ) + lambda self, *_, **__: get_peft_model_state_dict(self, old_state_dict()) ).__get__(model, type(model)) trainer.train() diff --git a/pilot/server/chat_adapter.py b/pilot/server/chat_adapter.py index 805cacb3d..b7e102be3 100644 --- a/pilot/server/chat_adapter.py +++ b/pilot/server/chat_adapter.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from typing import List from functools import cache +from typing import List + from pilot.model.inference import generate_stream + class BaseChatAdpter: """The Base class for chat with llm models. it will match the model, and fetch output from model""" @@ -15,7 +17,7 @@ class BaseChatAdpter: def get_generate_stream_func(self): """Return the generate stream handler func""" pass - + llm_model_chat_adapters: List[BaseChatAdpter] = [] @@ -31,13 +33,14 @@ def get_llm_chat_adapter(model_path: str) -> BaseChatAdpter: for adapter in llm_model_chat_adapters: if adapter.match(model_path): return adapter - + raise ValueError(f"Invalid model for chat adapter {model_path}") class VicunaChatAdapter(BaseChatAdpter): - """ Model chat Adapter for vicuna""" + """Model chat Adapter for vicuna""" + def match(self, model_path: str): return "vicuna" in model_path @@ -46,37 +49,42 @@ class VicunaChatAdapter(BaseChatAdpter): class ChatGLMChatAdapter(BaseChatAdpter): - """ Model chat Adapter for ChatGLM""" + """Model chat Adapter for ChatGLM""" + def match(self, model_path: str): return "chatglm" in model_path def get_generate_stream_func(self): from pilot.model.chatglm_llm import chatglm_generate_stream + return chatglm_generate_stream class CodeT5ChatAdapter(BaseChatAdpter): - """ Model chat adapter for CodeT5 """ + """Model chat adapter for CodeT5""" + def match(self, model_path: str): return "codet5" in model_path - + def get_generate_stream_func(self): # TODO pass + class CodeGenChatAdapter(BaseChatAdpter): - - """ Model chat adapter for CodeGen """ + + """Model chat adapter for CodeGen""" + def match(self, model_path: str): return "codegen" in model_path - + def get_generate_stream_func(self): - # TODO + # TODO pass register_llm_model_chat_adapter(VicunaChatAdapter) register_llm_model_chat_adapter(ChatGLMChatAdapter) -register_llm_model_chat_adapter(BaseChatAdpter) \ No newline at end of file +register_llm_model_chat_adapter(BaseChatAdpter) diff --git a/pilot/server/gradio_css.py b/pilot/server/gradio_css.py index 97706df3f..b0a3892ae 100644 --- a/pilot/server/gradio_css.py +++ b/pilot/server/gradio_css.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 # -*- coding:utf-8 -*- -code_highlight_css = ( -""" +code_highlight_css = """ #chatbot .hll { background-color: #ffffcc } #chatbot .c { color: #408080; font-style: italic } #chatbot .err { border: 1px solid #FF0000 } @@ -71,6 +70,5 @@ code_highlight_css = ( #chatbot .vi { color: #19177C } #chatbot .vm { color: #19177C } #chatbot .il { color: #666666 } -""") -#.highlight { background: #f8f8f8; } - +""" +# .highlight { background: #f8f8f8; } diff --git a/pilot/server/gradio_patch.py b/pilot/server/gradio_patch.py index a915760eb..ca3974cbd 100644 --- a/pilot/server/gradio_patch.py +++ b/pilot/server/gradio_patch.py @@ -49,7 +49,7 @@ class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable): warnings.warn( "The 'color_map' parameter has been deprecated.", ) - #self.md = utils.get_markdown_parser() + # self.md = utils.get_markdown_parser() self.md = Markdown(extras=["fenced-code-blocks", "tables", "break-on-newline"]) self.select: EventListenerMethod """ @@ -112,7 +112,7 @@ class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable): ): # This happens for previously processed messages return chat_message elif isinstance(chat_message, str): - #return self.md.render(chat_message) + # return self.md.render(chat_message) return str(self.md.convert(chat_message)) else: raise ValueError(f"Invalid message for Chatbot component: {chat_message}") @@ -141,9 +141,10 @@ class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable): ), f"Expected a list of lists of length 2 or list of tuples of length 2. Received: {message_pair}" processed_messages.append( ( - #self._process_chat_messages(message_pair[0]), - '

' +
-                    message_pair[0] + "
", + # self._process_chat_messages(message_pair[0]), + '
'
+                    + message_pair[0]
+                    + "
", self._process_chat_messages(message_pair[1]), ) ) @@ -163,5 +164,3 @@ class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable): **kwargs, ) return self - - diff --git a/pilot/server/llmserver.py b/pilot/server/llmserver.py index bc227d518..ac6b7cac9 100644 --- a/pilot/server/llmserver.py +++ b/pilot/server/llmserver.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import os -import uvicorn import asyncio import json +import os import sys -from typing import Optional, List -from fastapi import FastAPI, Request, BackgroundTasks + +import uvicorn +from fastapi import BackgroundTasks, FastAPI, Request from fastapi.responses import StreamingResponse from pydantic import BaseModel @@ -17,28 +17,26 @@ model_semaphore = None ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(ROOT_PATH) -from pilot.model.inference import generate_stream -from pilot.model.inference import generate_output, get_embeddings - -from pilot.model.loader import ModelLoader +from pilot.configs.config import Config from pilot.configs.model_config import * -from pilot.configs.config import Config +from pilot.model.inference import generate_output, generate_stream, get_embeddings +from pilot.model.loader import ModelLoader from pilot.server.chat_adapter import get_llm_chat_adapter - CFG = Config() -class ModelWorker: +class ModelWorker: def __init__(self, model_path, model_name, device, num_gpus=1): - if model_path.endswith("/"): model_path = model_path[:-1] self.model_name = model_name or model_path.split("/")[-1] self.device = device self.ml = ModelLoader(model_path=model_path) - self.model, self.tokenizer = self.ml.loader(num_gpus, load_8bit=ISLOAD_8BIT, debug=ISDEBUG) + self.model, self.tokenizer = self.ml.loader( + num_gpus, load_8bit=ISLOAD_8BIT, debug=ISDEBUG + ) if hasattr(self.model.config, "max_sequence_length"): self.context_len = self.model.config.max_sequence_length @@ -47,24 +45,28 @@ class ModelWorker: else: self.context_len = 2048 - + self.llm_chat_adapter = get_llm_chat_adapter(model_path) - self.generate_stream_func = self.llm_chat_adapter.get_generate_stream_func() + self.generate_stream_func = self.llm_chat_adapter.get_generate_stream_func() def get_queue_length(self): - if model_semaphore is None or model_semaphore._value is None or model_semaphore._waiters is None: + if ( + model_semaphore is None + or model_semaphore._value is None + or model_semaphore._waiters is None + ): return 0 else: - CFG.LIMIT_MODEL_CONCURRENCY - model_semaphore._value + len(model_semaphore._waiters) + ( + CFG.LIMIT_MODEL_CONCURRENCY + - model_semaphore._value + + len(model_semaphore._waiters) + ) def generate_stream_gate(self, params): try: for output in self.generate_stream_func( - self.model, - self.tokenizer, - params, - DEVICE, - CFG.MAX_POSITION_EMBEDDINGS + self.model, self.tokenizer, params, DEVICE, CFG.MAX_POSITION_EMBEDDINGS ): print("output: ", output) ret = { @@ -74,17 +76,16 @@ class ModelWorker: yield json.dumps(ret).encode() + b"\0" except torch.cuda.CudaError: - ret = { - "text": "**GPU OutOfMemory, Please Refresh.**", - "error_code": 0 - } + ret = {"text": "**GPU OutOfMemory, Please Refresh.**", "error_code": 0} yield json.dumps(ret).encode() + b"\0" def get_embeddings(self, prompt): return get_embeddings(self.model, self.tokenizer, prompt) + app = FastAPI() + class PromptRequest(BaseModel): prompt: str temperature: float @@ -92,6 +93,7 @@ class PromptRequest(BaseModel): model: str stop: str = None + class StreamRequest(BaseModel): model: str prompt: str @@ -99,9 +101,11 @@ class StreamRequest(BaseModel): max_new_tokens: int stop: str + class EmbeddingRequest(BaseModel): prompt: str + def release_model_semaphore(): model_semaphore.release() @@ -114,23 +118,24 @@ async def api_generate_stream(request: Request): if model_semaphore is None: model_semaphore = asyncio.Semaphore(CFG.LIMIT_MODEL_CONCURRENCY) - await model_semaphore.acquire() + await model_semaphore.acquire() generator = worker.generate_stream_gate(params) background_tasks = BackgroundTasks() background_tasks.add_task(release_model_semaphore) return StreamingResponse(generator, background=background_tasks) + @app.post("/generate") def generate(prompt_request: PromptRequest): params = { "prompt": prompt_request.prompt, "temperature": prompt_request.temperature, "max_new_tokens": prompt_request.max_new_tokens, - "stop": prompt_request.stop + "stop": prompt_request.stop, } - response = [] + response = [] rsp_str = "" output = worker.generate_stream_gate(params) for rsp in output: @@ -140,7 +145,7 @@ def generate(prompt_request: PromptRequest): response.append(rsp_str) return {"response": rsp_str} - + @app.post("/embedding") def embeddings(prompt_request: EmbeddingRequest): @@ -151,16 +156,11 @@ def embeddings(prompt_request: EmbeddingRequest): if __name__ == "__main__": - model_path = LLM_MODEL_CONFIG[CFG.LLM_MODEL] print(model_path, DEVICE) - - + worker = ModelWorker( - model_path=model_path, - model_name=CFG.LLM_MODEL, - device=DEVICE, - num_gpus=1 + model_path=model_path, model_name=CFG.LLM_MODEL, device=DEVICE, num_gpus=1 ) - uvicorn.run(app, host="0.0.0.0", port=CFG.MODEL_PORT, log_level="info") \ No newline at end of file + uvicorn.run(app, host="0.0.0.0", port=CFG.MODEL_PORT, log_level="info") diff --git a/pilot/server/vectordb_qa.py b/pilot/server/vectordb_qa.py index 71a9b881d..6bf0b4688 100644 --- a/pilot/server/vectordb_qa.py +++ b/pilot/server/vectordb_qa.py @@ -1,29 +1,30 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from pilot.vector_store.file_loader import KnownLedge2Vector from langchain.prompts import PromptTemplate -from pilot.conversation import conv_qa_prompt_template + from pilot.configs.model_config import VECTOR_SEARCH_TOP_K +from pilot.conversation import conv_qa_prompt_template from pilot.model.vicuna_llm import VicunaLLM +from pilot.vector_store.file_loader import KnownLedge2Vector + class KnownLedgeBaseQA: - def __init__(self) -> None: k2v = KnownLedge2Vector() self.vector_store = k2v.init_vector_store() self.llm = VicunaLLM() - + def get_similar_answer(self, query): - prompt = PromptTemplate( - template=conv_qa_prompt_template, - input_variables=["context", "question"] + template=conv_qa_prompt_template, input_variables=["context", "question"] ) - retriever = self.vector_store.as_retriever(search_kwargs={"k": VECTOR_SEARCH_TOP_K}) + retriever = self.vector_store.as_retriever( + search_kwargs={"k": VECTOR_SEARCH_TOP_K} + ) docs = retriever.get_relevant_documents(query=query) - context = [d.page_content for d in docs] + context = [d.page_content for d in docs] result = prompt.format(context="\n".join(context), question=query) return result diff --git a/pilot/server/webserver.py b/pilot/server/webserver.py index 1ac32ab26..15d360ec7 100644 --- a/pilot/server/webserver.py +++ b/pilot/server/webserver.py @@ -2,58 +2,55 @@ # -*- coding: utf-8 -*- import argparse +import datetime +import json import os import shutil -import uuid -import json import sys import time -import gradio as gr -import datetime -import requests +import uuid from urllib.parse import urljoin +import gradio as gr +import requests from langchain import PromptTemplate - ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(ROOT_PATH) -from pilot.configs.model_config import KNOWLEDGE_UPLOAD_ROOT_PATH, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K -from pilot.server.vectordb_qa import KnownLedgeBaseQA -from pilot.connections.mysql import MySQLOperator -from pilot.source_embedding.knowledge_embedding import KnowledgeEmbedding -from pilot.vector_store.extract_tovec import get_vector_storelist, load_knownledge_from_doc, knownledge_tovec_st - -from pilot.configs.model_config import LOGDIR, DATASETS_DIR - -from pilot.plugins import scan_plugins -from pilot.configs.config import Config +from pilot.commands.command import execute_ai_response_json from pilot.commands.command_mange import CommandRegistry +from pilot.commands.exception_not_commands import NotCommands +from pilot.configs.config import Config +from pilot.configs.model_config import ( + DATASETS_DIR, + KNOWLEDGE_UPLOAD_ROOT_PATH, + LLM_MODEL_CONFIG, + LOGDIR, + VECTOR_SEARCH_TOP_K, +) +from pilot.connections.mysql import MySQLOperator +from pilot.conversation import ( + SeparatorStyle, + conv_qa_prompt_template, + conv_templates, + conversation_sql_mode, + conversation_types, + default_conversation, +) +from pilot.plugins import scan_plugins from pilot.prompts.auto_mode_prompt import AutoModePrompt from pilot.prompts.generator import PromptGenerator - -from pilot.commands.exception_not_commands import NotCommands - - - -from pilot.conversation import ( - default_conversation, - conv_templates, - conversation_types, - conversation_sql_mode, - SeparatorStyle, conv_qa_prompt_template -) - -from pilot.utils import ( - build_logger, - server_error_msg, -) - from pilot.server.gradio_css import code_highlight_css from pilot.server.gradio_patch import Chatbot as grChatbot - -from pilot.commands.command import execute_ai_response_json +from pilot.server.vectordb_qa import KnownLedgeBaseQA +from pilot.source_embedding.knowledge_embedding import KnowledgeEmbedding +from pilot.utils import build_logger, server_error_msg +from pilot.vector_store.extract_tovec import ( + get_vector_storelist, + knownledge_tovec_st, + load_knownledge_from_doc, +) logger = build_logger("webserver", LOGDIR + "webserver.log") headers = {"User-Agent": "dbgpt Client"} @@ -70,19 +67,19 @@ autogpt = False vector_store_client = None vector_store_name = {"vs_name": ""} -priority = { - "vicuna-13b": "aaa" -} +priority = {"vicuna-13b": "aaa"} # 加载插件 -CFG= Config() +CFG = Config() DB_SETTINGS = { "user": CFG.LOCAL_DB_USER, - "password": CFG.LOCAL_DB_PASSWORD, + "password": CFG.LOCAL_DB_PASSWORD, "host": CFG.LOCAL_DB_HOST, - "port": CFG.LOCAL_DB_PORT + "port": CFG.LOCAL_DB_PORT, } + + def get_simlar(q): docsearch = knownledge_tovec_st(os.path.join(DATASETS_DIR, "plan.md")) docs = docsearch.similarity_search_with_score(q, k=1) @@ -92,9 +89,7 @@ def get_simlar(q): def gen_sqlgen_conversation(dbname): - mo = MySQLOperator( - **DB_SETTINGS - ) + mo = MySQLOperator(**DB_SETTINGS) message = "" @@ -132,13 +127,15 @@ def load_demo(url_params, request: gr.Request): gr.Dropdown.update(choices=dbs) state = default_conversation.copy() - return (state, - dropdown_update, - gr.Chatbot.update(visible=True), - gr.Textbox.update(visible=True), - gr.Button.update(visible=True), - gr.Row.update(visible=True), - gr.Accordion.update(visible=True)) + return ( + state, + dropdown_update, + gr.Chatbot.update(visible=True), + gr.Textbox.update(visible=True), + gr.Button.update(visible=True), + gr.Row.update(visible=True), + gr.Accordion.update(visible=True), + ) def get_conv_log_filename(): @@ -185,7 +182,9 @@ def post_process_code(code): return code -def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, request: gr.Request): +def http_bot( + state, mode, sql_mode, db_selector, temperature, max_new_tokens, request: gr.Request +): if sql_mode == conversation_sql_mode["auto_execute_ai_response"]: print("AUTO DB-GPT模式.") if sql_mode == conversation_sql_mode["dont_execute_ai_response"]: @@ -212,12 +211,13 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re # 第一轮对话需要加入提示Prompt if sql_mode == conversation_sql_mode["auto_execute_ai_response"]: # autogpt模式的第一轮对话需要 构建专属prompt - system_prompt = auto_prompt.construct_first_prompt(fisrt_message=[query], - db_schemes=gen_sqlgen_conversation(dbname)) + system_prompt = auto_prompt.construct_first_prompt( + fisrt_message=[query], db_schemes=gen_sqlgen_conversation(dbname) + ) logger.info("[TEST]:" + system_prompt) template_name = "auto_dbgpt_one_shot" new_state = conv_templates[template_name].copy() - new_state.append_message(role='USER', message=system_prompt) + new_state.append_message(role="USER", message=system_prompt) # new_state.append_message(new_state.roles[0], query) new_state.append_message(new_state.roles[1], None) else: @@ -226,7 +226,9 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re # prompt 中添加上下文提示, 根据已有知识对话, 上下文提示是否也应该放在第一轮, 还是每一轮都添加上下文? # 如果用户侧的问题跨度很大, 应该每一轮都加提示。 if db_selector: - new_state.append_message(new_state.roles[0], gen_sqlgen_conversation(dbname) + query) + new_state.append_message( + new_state.roles[0], gen_sqlgen_conversation(dbname) + query + ) new_state.append_message(new_state.roles[1], None) else: new_state.append_message(new_state.roles[0], query) @@ -244,7 +246,9 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re # prompt 中添加上下文提示, 根据已有知识对话, 上下文提示是否也应该放在第一轮, 还是每一轮都添加上下文? # 如果用户侧的问题跨度很大, 应该每一轮都加提示。 if db_selector: - new_state.append_message(new_state.roles[0], gen_sqlgen_conversation(dbname) + query) + new_state.append_message( + new_state.roles[0], gen_sqlgen_conversation(dbname) + query + ) new_state.append_message(new_state.roles[1], None) else: new_state.append_message(new_state.roles[0], query) @@ -268,17 +272,22 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re if mode == conversation_types["custome"] and not db_selector: print("vector store name: ", vector_store_name["vs_name"]) - vector_store_config = {"vector_store_name": vector_store_name["vs_name"], "text_field": "content", - "vector_store_path": KNOWLEDGE_UPLOAD_ROOT_PATH} - knowledge_embedding_client = KnowledgeEmbedding(file_path="", model_name=LLM_MODEL_CONFIG["text2vec"], - local_persist=False, - vector_store_config=vector_store_config) + vector_store_config = { + "vector_store_name": vector_store_name["vs_name"], + "text_field": "content", + "vector_store_path": KNOWLEDGE_UPLOAD_ROOT_PATH, + } + knowledge_embedding_client = KnowledgeEmbedding( + file_path="", + model_name=LLM_MODEL_CONFIG["text2vec"], + local_persist=False, + vector_store_config=vector_store_config, + ) query = state.messages[-2][1] docs = knowledge_embedding_client.similar_search(query, VECTOR_SEARCH_TOP_K) context = [d.page_content for d in docs] prompt_template = PromptTemplate( - template=conv_qa_prompt_template, - input_variables=["context", "question"] + template=conv_qa_prompt_template, input_variables=["context", "question"] ) result = prompt_template.format(context="\n".join(context), question=query) state.messages[-2][1] = result @@ -290,7 +299,7 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re context = context[:2000] prompt_template = PromptTemplate( template=conv_qa_prompt_template, - input_variables=["context", "question"] + input_variables=["context", "question"], ) result = prompt_template.format(context="\n".join(context), question=query) state.messages[-2][1] = result @@ -311,8 +320,12 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re logger.info(f"Requert: \n{payload}") if sql_mode == conversation_sql_mode["auto_execute_ai_response"]: - response = requests.post(urljoin(CFG.MODEL_SERVER, "generate"), - headers=headers, json=payload, timeout=120) + response = requests.post( + urljoin(CFG.MODEL_SERVER, "generate"), + headers=headers, + json=payload, + timeout=120, + ) print(response.json()) print(str(response)) @@ -321,17 +334,17 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re text = text.rstrip() respObj = json.loads(text) - xx = respObj['response'] - xx = xx.strip(b'\x00'.decode()) + xx = respObj["response"] + xx = xx.strip(b"\x00".decode()) respObj_ex = json.loads(xx) - if respObj_ex['error_code'] == 0: + if respObj_ex["error_code"] == 0: ai_response = None - all_text = respObj_ex['text'] + all_text = respObj_ex["text"] ### 解析返回文本,获取AI回复部分 tmpResp = all_text.split(state.sep) last_index = -1 for i in range(len(tmpResp)): - if tmpResp[i].find('ASSISTANT:') != -1: + if tmpResp[i].find("ASSISTANT:") != -1: last_index = i ai_response = tmpResp[last_index] ai_response = ai_response.replace("ASSISTANT:", "") @@ -343,14 +356,20 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re state.messages[-1][-1] = "ASSISTANT未能正确回复,回复结果为:\n" + all_text yield (state, state.to_gradio_chatbot()) + (no_change_btn,) * 5 else: - plugin_resp = execute_ai_response_json(auto_prompt.prompt_generator, ai_response) + plugin_resp = execute_ai_response_json( + auto_prompt.prompt_generator, ai_response + ) cfg.set_last_plugin_return(plugin_resp) print(plugin_resp) - state.messages[-1][-1] = "Model推理信息:\n" + ai_response + "\n\nDB-GPT执行结果:\n" + plugin_resp + state.messages[-1][-1] = ( + "Model推理信息:\n" + ai_response + "\n\nDB-GPT执行结果:\n" + plugin_resp + ) yield (state, state.to_gradio_chatbot()) + (no_change_btn,) * 5 except NotCommands as e: print("命令执行:" + e.message) - state.messages[-1][-1] = "命令执行:" + e.message + "\n模型输出:\n" + str(ai_response) + state.messages[-1][-1] = ( + "命令执行:" + e.message + "\n模型输出:\n" + str(ai_response) + ) yield (state, state.to_gradio_chatbot()) + (no_change_btn,) * 5 else: # 流式输出 @@ -359,8 +378,13 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re try: # Stream output - response = requests.post(urljoin(CFG.MODEL_SERVER, "generate_stream"), - headers=headers, json=payload, stream=True, timeout=20) + response = requests.post( + urljoin(CFG.MODEL_SERVER, "generate_stream"), + headers=headers, + json=payload, + stream=True, + timeout=20, + ) for chunk in response.iter_lines(decode_unicode=False, delimiter=b"\0"): if chunk: data = json.loads(chunk.decode()) @@ -368,7 +392,6 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re """ TODO Multi mode output handler, rewrite this for multi model, use adapter mode. """ if data["error_code"] == 0: - if "vicuna" in CFG.LLM_MODEL: output = data["text"][skip_echo_len:].strip() else: @@ -381,12 +404,23 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re output = data["text"] + f" (error_code: {data['error_code']})" state.messages[-1][-1] = output yield (state, state.to_gradio_chatbot()) + ( - disable_btn, disable_btn, disable_btn, enable_btn, enable_btn) + disable_btn, + disable_btn, + disable_btn, + enable_btn, + enable_btn, + ) return except requests.exceptions.RequestException as e: state.messages[-1][-1] = server_error_msg + f" (error_code: 4)" - yield (state, state.to_gradio_chatbot()) + (disable_btn, disable_btn, disable_btn, enable_btn, enable_btn) + yield (state, state.to_gradio_chatbot()) + ( + disable_btn, + disable_btn, + disable_btn, + enable_btn, + enable_btn, + ) return state.messages[-1][-1] = state.messages[-1][-1][:-1] @@ -410,8 +444,8 @@ def http_bot(state, mode, sql_mode, db_selector, temperature, max_new_tokens, re block_css = ( - code_highlight_css - + """ + code_highlight_css + + """ pre { white-space: pre-wrap; /* Since CSS 2.1 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ @@ -487,7 +521,8 @@ def build_single_model_ui(): choices=dbs, value=dbs[0] if len(models) > 0 else "", interactive=True, - show_label=True).style(container=False) + show_label=True, + ).style(container=False) sql_mode = gr.Radio(["直接执行结果", "不执行结果"], show_label=False, value="不执行结果") sql_vs_setting = gr.Markdown("自动执行模式下, DB-GPT可以具备执行SQL、从网络读取知识自动化存储学习的能力") @@ -495,7 +530,9 @@ def build_single_model_ui(): tab_qa = gr.TabItem("知识问答", elem_id="QA") with tab_qa: - mode = gr.Radio(["LLM原生对话", "默认知识库对话", "新增知识库对话"], show_label=False, value="LLM原生对话") + mode = gr.Radio( + ["LLM原生对话", "默认知识库对话", "新增知识库对话"], show_label=False, value="LLM原生对话" + ) vs_setting = gr.Accordion("配置知识库", open=False) mode.change(fn=change_mode, inputs=mode, outputs=vs_setting) with vs_setting: @@ -504,19 +541,22 @@ def build_single_model_ui(): with gr.Column() as doc2vec: gr.Markdown("向知识库中添加文件") with gr.Tab("上传文件"): - files = gr.File(label="添加文件", - file_types=[".txt", ".md", ".docx", ".pdf"], - file_count="multiple", - allow_flagged_uploads=True, - show_label=False - ) + files = gr.File( + label="添加文件", + file_types=[".txt", ".md", ".docx", ".pdf"], + file_count="multiple", + allow_flagged_uploads=True, + show_label=False, + ) load_file_button = gr.Button("上传并加载到知识库") with gr.Tab("上传文件夹"): - folder_files = gr.File(label="添加文件夹", - accept_multiple_files=True, - file_count="directory", - show_label=False) + folder_files = gr.File( + label="添加文件夹", + accept_multiple_files=True, + file_count="directory", + show_label=False, + ) load_folder_button = gr.Button("上传并加载到知识库") with gr.Blocks(): @@ -557,28 +597,32 @@ def build_single_model_ui(): ).then( http_bot, [state, mode, sql_mode, db_selector, temperature, max_output_tokens], - [state, chatbot] + btn_list + [state, chatbot] + btn_list, + ) + vs_add.click( + fn=save_vs_name, show_progress=True, inputs=[vs_name], outputs=[vs_name] + ) + load_file_button.click( + fn=knowledge_embedding_store, + show_progress=True, + inputs=[vs_name, files], + outputs=[vs_name], + ) + load_folder_button.click( + fn=knowledge_embedding_store, + show_progress=True, + inputs=[vs_name, folder_files], + outputs=[vs_name], ) - vs_add.click(fn=save_vs_name, show_progress=True, - inputs=[vs_name], - outputs=[vs_name]) - load_file_button.click(fn=knowledge_embedding_store, - show_progress=True, - inputs=[vs_name, files], - outputs=[vs_name]) - load_folder_button.click(fn=knowledge_embedding_store, - show_progress=True, - inputs=[vs_name, folder_files], - outputs=[vs_name]) return state, chatbot, textbox, send_btn, button_row, parameter_row def build_webdemo(): with gr.Blocks( - title="数据库智能助手", - # theme=gr.themes.Base(), - theme=gr.themes.Default(), - css=block_css, + title="数据库智能助手", + # theme=gr.themes.Base(), + theme=gr.themes.Default(), + css=block_css, ) as demo: url_params = gr.JSON(visible=False) ( @@ -613,26 +657,31 @@ def save_vs_name(vs_name): vector_store_name["vs_name"] = vs_name return vs_name + def knowledge_embedding_store(vs_id, files): # vs_path = os.path.join(VS_ROOT_PATH, vs_id) if not os.path.exists(os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, vs_id)): os.makedirs(os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, vs_id)) for file in files: filename = os.path.split(file.name)[-1] - shutil.move(file.name, os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, vs_id, filename)) + shutil.move( + file.name, os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, vs_id, filename) + ) knowledge_embedding_client = KnowledgeEmbedding( file_path=os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, vs_id, filename), model_name=LLM_MODEL_CONFIG["text2vec"], local_persist=False, vector_store_config={ "vector_store_name": vector_store_name["vs_name"], - "vector_store_path": KNOWLEDGE_UPLOAD_ROOT_PATH}) + "vector_store_path": KNOWLEDGE_UPLOAD_ROOT_PATH, + }, + ) knowledge_embedding_client.knowledge_embedding() - logger.info("knowledge embedding success") return os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, vs_id, vs_id + ".vectordb") + if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--host", type=str, default="0.0.0.0") @@ -671,5 +720,8 @@ if __name__ == "__main__": demo.queue( concurrency_count=args.concurrency_count, status_update_rate=10, api_open=False ).launch( - server_name=args.host, server_port=args.port, share=args.share, max_threads=200, + server_name=args.host, + server_port=args.port, + share=args.share, + max_threads=200, ) diff --git a/pilot/singleton.py b/pilot/singleton.py index 8a9d6e2fa..6fd65132e 100644 --- a/pilot/singleton.py +++ b/pilot/singleton.py @@ -5,10 +5,12 @@ import abc from typing import Any + class Singleton(abc.ABCMeta, type): - """ Singleton metaclass for ensuring only one instance of a class""" + """Singleton metaclass for ensuring only one instance of a class""" _instances = {} + def __call__(cls, *args: Any, **kwargs: Any) -> Any: """Call method for the singleton metaclass""" if cls not in cls._instances: @@ -18,4 +20,5 @@ class Singleton(abc.ABCMeta, type): class AbstractSingleton(abc.ABC, metaclass=Singleton): """Abstract singleton class for ensuring only one instance of a class""" - pass \ No newline at end of file + + pass diff --git a/pilot/source_embedding/__init__.py b/pilot/source_embedding/__init__.py index 9d1e74a31..464ff11b1 100644 --- a/pilot/source_embedding/__init__.py +++ b/pilot/source_embedding/__init__.py @@ -1,8 +1,3 @@ -from pilot.source_embedding.source_embedding import SourceEmbedding -from pilot.source_embedding.source_embedding import register +from pilot.source_embedding.source_embedding import SourceEmbedding, register - -__all__ = [ - "SourceEmbedding", - "register" -] \ No newline at end of file +__all__ = ["SourceEmbedding", "register"] diff --git a/pilot/source_embedding/chn_document_splitter.py b/pilot/source_embedding/chn_document_splitter.py index 10a77aeca..5bf06ea8c 100644 --- a/pilot/source_embedding/chn_document_splitter.py +++ b/pilot/source_embedding/chn_document_splitter.py @@ -1,5 +1,6 @@ import re from typing import List + from langchain.text_splitter import CharacterTextSplitter @@ -12,32 +13,43 @@ class CHNDocumentSplitter(CharacterTextSplitter): def split_text(self, text: str) -> List[str]: if self.pdf: text = re.sub(r"\n{3,}", r"\n", text) - text = re.sub('\s', " ", text) + text = re.sub("\s", " ", text) text = re.sub("\n\n", "", text) - text = re.sub(r'([;;.!?。!?\?])([^”’])', r"\1\n\2", text) + text = re.sub(r"([;;.!?。!?\?])([^”’])", r"\1\n\2", text) text = re.sub(r'(\.{6})([^"’”」』])', r"\1\n\2", text) text = re.sub(r'(\…{2})([^"’”」』])', r"\1\n\2", text) - text = re.sub(r'([;;!?。!?\?]["’”」』]{0,2})([^;;!?,。!?\?])', r'\1\n\2', text) + text = re.sub(r'([;;!?。!?\?]["’”」』]{0,2})([^;;!?,。!?\?])', r"\1\n\2", text) text = text.rstrip() ls = [i for i in text.split("\n") if i] for ele in ls: if len(ele) > self.sentence_size: - ele1 = re.sub(r'([,,.]["’”」』]{0,2})([^,,.])', r'\1\n\2', ele) + ele1 = re.sub(r'([,,.]["’”」』]{0,2})([^,,.])', r"\1\n\2", ele) ele1_ls = ele1.split("\n") for ele_ele1 in ele1_ls: if len(ele_ele1) > self.sentence_size: - ele_ele2 = re.sub(r'([\n]{1,}| {2,}["’”」』]{0,2})([^\s])', r'\1\n\2', ele_ele1) + ele_ele2 = re.sub( + r'([\n]{1,}| {2,}["’”」』]{0,2})([^\s])', r"\1\n\2", ele_ele1 + ) ele2_ls = ele_ele2.split("\n") for ele_ele2 in ele2_ls: if len(ele_ele2) > self.sentence_size: - ele_ele3 = re.sub('( ["’”」』]{0,2})([^ ])', r'\1\n\2', ele_ele2) + ele_ele3 = re.sub( + '( ["’”」』]{0,2})([^ ])', r"\1\n\2", ele_ele2 + ) ele2_id = ele2_ls.index(ele_ele2) - ele2_ls = ele2_ls[:ele2_id] + [i for i in ele_ele3.split("\n") if i] + ele2_ls[ - ele2_id + 1:] + ele2_ls = ( + ele2_ls[:ele2_id] + + [i for i in ele_ele3.split("\n") if i] + + ele2_ls[ele2_id + 1 :] + ) ele_id = ele1_ls.index(ele_ele1) - ele1_ls = ele1_ls[:ele_id] + [i for i in ele2_ls if i] + ele1_ls[ele_id + 1:] + ele1_ls = ( + ele1_ls[:ele_id] + + [i for i in ele2_ls if i] + + ele1_ls[ele_id + 1 :] + ) id = ls.index(ele) - ls = ls[:id] + [i for i in ele1_ls if i] + ls[id + 1:] + ls = ls[:id] + [i for i in ele1_ls if i] + ls[id + 1 :] return ls diff --git a/pilot/source_embedding/csv_embedding.py b/pilot/source_embedding/csv_embedding.py index 2f3b7ed06..8b2e25ff3 100644 --- a/pilot/source_embedding/csv_embedding.py +++ b/pilot/source_embedding/csv_embedding.py @@ -1,14 +1,21 @@ -from typing import List, Optional, Dict -from pilot.source_embedding import SourceEmbedding, register +from typing import Dict, List, Optional from langchain.document_loaders import CSVLoader from langchain.schema import Document +from pilot.source_embedding import SourceEmbedding, register + class CSVEmbedding(SourceEmbedding): """csv embedding for read csv document.""" - def __init__(self, file_path, model_name, vector_store_config, embedding_args: Optional[Dict] = None): + def __init__( + self, + file_path, + model_name, + vector_store_config, + embedding_args: Optional[Dict] = None, + ): """Initialize with csv path.""" super().__init__(file_path, model_name, vector_store_config) self.file_path = file_path @@ -29,6 +36,3 @@ class CSVEmbedding(SourceEmbedding): documents[i].page_content = d.page_content.replace("\n", "") i += 1 return documents - - - diff --git a/pilot/source_embedding/knowledge_embedding.py b/pilot/source_embedding/knowledge_embedding.py index 2f313a35a..33f35f826 100644 --- a/pilot/source_embedding/knowledge_embedding.py +++ b/pilot/source_embedding/knowledge_embedding.py @@ -1,7 +1,8 @@ import os +import markdown from bs4 import BeautifulSoup -from langchain.document_loaders import TextLoader, markdown, PyPDFLoader +from langchain.document_loaders import PyPDFLoader, TextLoader, markdown from langchain.embeddings import HuggingFaceEmbeddings from pilot.configs.config import Config @@ -10,12 +11,11 @@ from pilot.source_embedding.chn_document_splitter import CHNDocumentSplitter from pilot.source_embedding.csv_embedding import CSVEmbedding from pilot.source_embedding.markdown_embedding import MarkdownEmbedding from pilot.source_embedding.pdf_embedding import PDFEmbedding -import markdown - from pilot.vector_store.connector import VectorStoreConnector CFG = Config() + class KnowledgeEmbedding: def __init__(self, file_path, model_name, vector_store_config, local_persist=True): """Initialize with Loader url, model_name, vector_store_config""" @@ -37,16 +37,30 @@ class KnowledgeEmbedding: def init_knowledge_embedding(self): if self.file_path.endswith(".pdf"): - embedding = PDFEmbedding(file_path=self.file_path, model_name=self.model_name, - vector_store_config=self.vector_store_config) + embedding = PDFEmbedding( + file_path=self.file_path, + model_name=self.model_name, + vector_store_config=self.vector_store_config, + ) elif self.file_path.endswith(".md"): - embedding = MarkdownEmbedding(file_path=self.file_path, model_name=self.model_name, vector_store_config=self.vector_store_config) + embedding = MarkdownEmbedding( + file_path=self.file_path, + model_name=self.model_name, + vector_store_config=self.vector_store_config, + ) elif self.file_path.endswith(".csv"): - embedding = CSVEmbedding(file_path=self.file_path, model_name=self.model_name, - vector_store_config=self.vector_store_config) + embedding = CSVEmbedding( + file_path=self.file_path, + model_name=self.model_name, + vector_store_config=self.vector_store_config, + ) elif self.file_type == "default": - embedding = MarkdownEmbedding(file_path=self.file_path, model_name=self.model_name, vector_store_config=self.vector_store_config) + embedding = MarkdownEmbedding( + file_path=self.file_path, + model_name=self.model_name, + vector_store_config=self.vector_store_config, + ) return embedding @@ -55,7 +69,9 @@ class KnowledgeEmbedding: def knowledge_persist_initialization(self, append_mode): documents = self._load_knownlege(self.file_path) - self.vector_client = VectorStoreConnector(CFG.VECTOR_STORE_TYPE, self.vector_store_config) + self.vector_client = VectorStoreConnector( + CFG.VECTOR_STORE_TYPE, self.vector_store_config + ) self.vector_client.load_document(documents) return self.vector_client @@ -67,7 +83,9 @@ class KnowledgeEmbedding: docs = self._load_file(filename) new_docs = [] for doc in docs: - doc.metadata = {"source": doc.metadata["source"].replace(DATASETS_DIR, "")} + doc.metadata = { + "source": doc.metadata["source"].replace(DATASETS_DIR, "") + } print("doc is embedding...", doc.metadata) new_docs.append(doc) docments += new_docs @@ -76,27 +94,33 @@ class KnowledgeEmbedding: def _load_file(self, filename): if filename.lower().endswith(".md"): loader = TextLoader(filename) - text_splitter = CHNDocumentSplitter(pdf=True, sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE) + text_splitter = CHNDocumentSplitter( + pdf=True, sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE + ) docs = loader.load_and_split(text_splitter) i = 0 for d in docs: content = markdown.markdown(d.page_content) - soup = BeautifulSoup(content, 'html.parser') - for tag in soup(['!doctype', 'meta', 'i.fa']): + soup = BeautifulSoup(content, "html.parser") + for tag in soup(["!doctype", "meta", "i.fa"]): tag.extract() docs[i].page_content = soup.get_text() docs[i].page_content = docs[i].page_content.replace("\n", " ") i += 1 elif filename.lower().endswith(".pdf"): loader = PyPDFLoader(filename) - textsplitter = CHNDocumentSplitter(pdf=True, sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE) + textsplitter = CHNDocumentSplitter( + pdf=True, sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE + ) docs = loader.load_and_split(textsplitter) i = 0 for d in docs: - docs[i].page_content = d.page_content.replace("\n", " ").replace("�", "") + docs[i].page_content = d.page_content.replace("\n", " ").replace( + "�", "" + ) i += 1 else: loader = TextLoader(filename) text_splitor = CHNDocumentSplitter(sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE) docs = loader.load_and_split(text_splitor) - return docs \ No newline at end of file + return docs diff --git a/pilot/source_embedding/markdown_embedding.py b/pilot/source_embedding/markdown_embedding.py index 834226f75..3db6cdbf5 100644 --- a/pilot/source_embedding/markdown_embedding.py +++ b/pilot/source_embedding/markdown_embedding.py @@ -3,12 +3,12 @@ import os from typing import List +import markdown from bs4 import BeautifulSoup from langchain.document_loaders import TextLoader from langchain.schema import Document -import markdown -from pilot.configs.model_config import KNOWLEDGE_CHUNK_SPLIT_SIZE +from pilot.configs.model_config import KNOWLEDGE_CHUNK_SPLIT_SIZE from pilot.source_embedding import SourceEmbedding, register from pilot.source_embedding.chn_document_splitter import CHNDocumentSplitter @@ -27,7 +27,9 @@ class MarkdownEmbedding(SourceEmbedding): def read(self): """Load from markdown path.""" loader = TextLoader(self.file_path) - text_splitter = CHNDocumentSplitter(pdf=True, sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE) + text_splitter = CHNDocumentSplitter( + pdf=True, sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE + ) return loader.load_and_split(text_splitter) @register @@ -44,7 +46,9 @@ class MarkdownEmbedding(SourceEmbedding): # 更新metadata数据 new_docs = [] for doc in docs: - doc.metadata = {"source": doc.metadata["source"].replace(self.file_path, "")} + doc.metadata = { + "source": doc.metadata["source"].replace(self.file_path, "") + } print("doc is embedding ... ", doc.metadata) new_docs.append(doc) docments += new_docs @@ -55,13 +59,10 @@ class MarkdownEmbedding(SourceEmbedding): i = 0 for d in documents: content = markdown.markdown(d.page_content) - soup = BeautifulSoup(content, 'html.parser') - for tag in soup(['!doctype', 'meta', 'i.fa']): + soup = BeautifulSoup(content, "html.parser") + for tag in soup(["!doctype", "meta", "i.fa"]): tag.extract() documents[i].page_content = soup.get_text() documents[i].page_content = documents[i].page_content.replace("\n", " ") i += 1 return documents - - - diff --git a/pilot/source_embedding/pdf_embedding.py b/pilot/source_embedding/pdf_embedding.py index 75d17c4c6..c76cf65d2 100644 --- a/pilot/source_embedding/pdf_embedding.py +++ b/pilot/source_embedding/pdf_embedding.py @@ -4,8 +4,8 @@ from typing import List from langchain.document_loaders import PyPDFLoader from langchain.schema import Document -from pilot.configs.model_config import KNOWLEDGE_CHUNK_SPLIT_SIZE +from pilot.configs.model_config import KNOWLEDGE_CHUNK_SPLIT_SIZE from pilot.source_embedding import SourceEmbedding, register from pilot.source_embedding.chn_document_splitter import CHNDocumentSplitter @@ -25,7 +25,9 @@ class PDFEmbedding(SourceEmbedding): """Load from pdf path.""" # loader = UnstructuredPaddlePDFLoader(self.file_path) loader = PyPDFLoader(self.file_path) - textsplitter = CHNDocumentSplitter(pdf=True, sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE) + textsplitter = CHNDocumentSplitter( + pdf=True, sentence_size=KNOWLEDGE_CHUNK_SPLIT_SIZE + ) return loader.load_and_split(textsplitter) @register @@ -35,6 +37,3 @@ class PDFEmbedding(SourceEmbedding): documents[i].page_content = d.page_content.replace("\n", "") i += 1 return documents - - - diff --git a/pilot/source_embedding/pdf_loader.py b/pilot/source_embedding/pdf_loader.py index aa7cf4da5..80888631f 100644 --- a/pilot/source_embedding/pdf_loader.py +++ b/pilot/source_embedding/pdf_loader.py @@ -1,10 +1,10 @@ """Loader that loads image files.""" +import os from typing import List +import fitz from langchain.document_loaders.unstructured import UnstructuredFileLoader from paddleocr import PaddleOCR -import os -import fitz class UnstructuredPaddlePDFLoader(UnstructuredFileLoader): @@ -19,9 +19,8 @@ class UnstructuredPaddlePDFLoader(UnstructuredFileLoader): ocr = PaddleOCR(lang="ch", use_gpu=False, show_log=False) doc = fitz.open(filepath) txt_file_path = os.path.join(full_dir_path, "%s.txt" % (filename)) - img_name = os.path.join(full_dir_path, '.tmp.png') - with open(txt_file_path, 'w', encoding='utf-8') as fout: - + img_name = os.path.join(full_dir_path, ".tmp.png") + with open(txt_file_path, "w", encoding="utf-8") as fout: for i in range(doc.page_count): page = doc[i] text = page.get_text("") @@ -42,11 +41,14 @@ class UnstructuredPaddlePDFLoader(UnstructuredFileLoader): txt_file_path = pdf_ocr_txt(self.file_path) from unstructured.partition.text import partition_text + return partition_text(filename=txt_file_path, **self.unstructured_kwargs) if __name__ == "__main__": - filepath = os.path.join(os.path.dirname(os.path.dirname(__file__)), "content", "samples", "test.pdf") + filepath = os.path.join( + os.path.dirname(os.path.dirname(__file__)), "content", "samples", "test.pdf" + ) loader = UnstructuredPaddlePDFLoader(filepath, mode="elements") docs = loader.load() for doc in docs: diff --git a/pilot/source_embedding/search_milvus.py b/pilot/source_embedding/search_milvus.py index ec0aa6813..aa02c1f61 100644 --- a/pilot/source_embedding/search_milvus.py +++ b/pilot/source_embedding/search_milvus.py @@ -58,4 +58,4 @@ # # docs, # # embedding=embeddings, # # connection_args={"host": "127.0.0.1", "port": "19530", "alias": "default"} -# # ) \ No newline at end of file +# # ) diff --git a/pilot/source_embedding/source_embedding.py b/pilot/source_embedding/source_embedding.py index a84282009..acbf82a73 100644 --- a/pilot/source_embedding/source_embedding.py +++ b/pilot/source_embedding/source_embedding.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- from abc import ABC, abstractmethod +from typing import Dict, List, Optional from langchain.embeddings import HuggingFaceEmbeddings -from typing import List, Optional, Dict from pilot.configs.config import Config from pilot.vector_store.connector import VectorStoreConnector @@ -23,7 +23,13 @@ class SourceEmbedding(ABC): Implementations should implement the method """ - def __init__(self, file_path, model_name, vector_store_config, embedding_args: Optional[Dict] = None): + def __init__( + self, + file_path, + model_name, + vector_store_config, + embedding_args: Optional[Dict] = None, + ): """Initialize with Loader url, model_name, vector_store_config""" self.file_path = file_path self.model_name = model_name @@ -32,12 +38,15 @@ class SourceEmbedding(ABC): self.embeddings = HuggingFaceEmbeddings(model_name=self.model_name) vector_store_config["embeddings"] = self.embeddings - self.vector_client = VectorStoreConnector(CFG.VECTOR_STORE_TYPE, vector_store_config) + self.vector_client = VectorStoreConnector( + CFG.VECTOR_STORE_TYPE, vector_store_config + ) @abstractmethod @register def read(self) -> List[ABC]: """read datasource into document objects.""" + @register def data_process(self, text): """pre process data.""" @@ -63,25 +72,25 @@ class SourceEmbedding(ABC): return self.vector_client.similar_search(doc, topk) def source_embedding(self): - if 'read' in registered_methods: + if "read" in registered_methods: text = self.read() - if 'data_process' in registered_methods: + if "data_process" in registered_methods: text = self.data_process(text) - if 'text_split' in registered_methods: + if "text_split" in registered_methods: self.text_split(text) - if 'text_to_vector' in registered_methods: + if "text_to_vector" in registered_methods: self.text_to_vector(text) - if 'index_to_store' in registered_methods: + if "index_to_store" in registered_methods: self.index_to_store(text) def batch_embedding(self): - if 'read_batch' in registered_methods: + if "read_batch" in registered_methods: text = self.read_batch() - if 'data_process' in registered_methods: + if "data_process" in registered_methods: text = self.data_process(text) - if 'text_split' in registered_methods: + if "text_split" in registered_methods: self.text_split(text) - if 'text_to_vector' in registered_methods: + if "text_to_vector" in registered_methods: self.text_to_vector(text) - if 'index_to_store' in registered_methods: + if "index_to_store" in registered_methods: self.index_to_store(text) diff --git a/pilot/source_embedding/url_embedding.py b/pilot/source_embedding/url_embedding.py index 68fbdd5e4..59eef19e7 100644 --- a/pilot/source_embedding/url_embedding.py +++ b/pilot/source_embedding/url_embedding.py @@ -1,13 +1,11 @@ from typing import List -from langchain.text_splitter import CharacterTextSplitter - -from pilot.source_embedding import SourceEmbedding, register - from bs4 import BeautifulSoup from langchain.document_loaders import WebBaseLoader from langchain.schema import Document +from langchain.text_splitter import CharacterTextSplitter +from pilot.source_embedding import SourceEmbedding, register class URLEmbedding(SourceEmbedding): @@ -23,7 +21,9 @@ class URLEmbedding(SourceEmbedding): def read(self): """Load from url path.""" loader = WebBaseLoader(web_path=self.file_path) - text_splitor = CharacterTextSplitter(chunk_size=1000, chunk_overlap=20, length_function=len) + text_splitor = CharacterTextSplitter( + chunk_size=1000, chunk_overlap=20, length_function=len + ) return loader.load_and_split(text_splitor) @register @@ -31,12 +31,9 @@ class URLEmbedding(SourceEmbedding): i = 0 for d in documents: content = d.page_content.replace("\n", "") - soup = BeautifulSoup(content, 'html.parser') - for tag in soup(['!doctype', 'meta']): + soup = BeautifulSoup(content, "html.parser") + for tag in soup(["!doctype", "meta"]): tag.extract() documents[i].page_content = soup.get_text() i += 1 return documents - - - diff --git a/pilot/utils.py b/pilot/utils.py index 607b83251..41e42fd55 100644 --- a/pilot/utils.py +++ b/pilot/utils.py @@ -1,27 +1,28 @@ #!/usr/bin/env python3 # -*- coding:utf-8 -*- -import torch - -import datetime import logging import logging.handlers import os import sys import requests +import torch from pilot.configs.model_config import LOGDIR -server_error_msg = "**NETWORK ERROR DUE TO HIGH TRAFFIC. PLEASE REGENERATE OR REFRESH THIS PAGE.**" +server_error_msg = ( + "**NETWORK ERROR DUE TO HIGH TRAFFIC. PLEASE REGENERATE OR REFRESH THIS PAGE.**" +) handler = None + def get_gpu_memory(max_gpus=None): gpu_memory = [] num_gpus = ( torch.cuda.device_count() - if max_gpus is None + if max_gpus is None else min(max_gpus, torch.cuda.device_count()) ) @@ -29,14 +30,13 @@ def get_gpu_memory(max_gpus=None): with torch.cuda.device(gpu_id): device = torch.cuda.current_device() gpu_properties = torch.cuda.get_device_properties(device) - total_memory = gpu_properties.total_memory / (1024 ** 3) - allocated_memory = torch.cuda.memory_allocated() / (1024 ** 3) + total_memory = gpu_properties.total_memory / (1024**3) + allocated_memory = torch.cuda.memory_allocated() / (1024**3) available_memory = total_memory - allocated_memory gpu_memory.append(available_memory) return gpu_memory - def build_logger(logger_name, logger_filename): global handler @@ -47,7 +47,7 @@ def build_logger(logger_name, logger_filename): # Set the format of root handlers if not logging.getLogger().handlers: - logging.basicConfig(level=logging.INFO, encoding='utf-8') + logging.basicConfig(level=logging.INFO, encoding="utf-8") logging.getLogger().handlers[0].setFormatter(formatter) # Redirect stdout and stderr to loggers @@ -70,7 +70,8 @@ def build_logger(logger_name, logger_filename): os.makedirs(LOGDIR, exist_ok=True) filename = os.path.join(LOGDIR, logger_filename) handler = logging.handlers.TimedRotatingFileHandler( - filename, when='D', utc=True) + filename, when="D", utc=True + ) handler.setFormatter(formatter) for name, item in logging.root.manager.loggerDict.items(): @@ -84,35 +85,36 @@ class StreamToLogger(object): """ Fake file-like stream object that redirects writes to a logger instance. """ + def __init__(self, logger, log_level=logging.INFO): self.terminal = sys.stdout self.logger = logger self.log_level = log_level - self.linebuf = '' + self.linebuf = "" def __getattr__(self, attr): return getattr(self.terminal, attr) def write(self, buf): temp_linebuf = self.linebuf + buf - self.linebuf = '' + self.linebuf = "" for line in temp_linebuf.splitlines(True): # From the io.TextIOWrapper docs: # On output, if newline is None, any '\n' characters written # are translated to the system default line separator. # By default sys.stdout.write() expects '\n' newlines and then # translates them so this is still cross platform. - if line[-1] == '\n': - encoded_message = line.encode('utf-8', 'ignore').decode('utf-8') + if line[-1] == "\n": + encoded_message = line.encode("utf-8", "ignore").decode("utf-8") self.logger.log(self.log_level, encoded_message.rstrip()) else: self.linebuf += line def flush(self): - if self.linebuf != '': - encoded_message = self.linebuf.encode('utf-8', 'ignore').decode('utf-8') + if self.linebuf != "": + encoded_message = self.linebuf.encode("utf-8", "ignore").decode("utf-8") self.logger.log(self.log_level, encoded_message.rstrip()) - self.linebuf = '' + self.linebuf = "" def disable_torch_init(): @@ -120,6 +122,7 @@ def disable_torch_init(): Disable the redundant torch default initialization to accelerate model creation. """ import torch + setattr(torch.nn.Linear, "reset_parameters", lambda self: None) setattr(torch.nn.LayerNorm, "reset_parameters", lambda self: None) @@ -128,4 +131,3 @@ def pretty_print_semaphore(semaphore): if semaphore is None: return "None" return f"Semaphore(value={semaphore._value}, locked={semaphore.locked()})" - diff --git a/pilot/vector_store/chroma_store.py b/pilot/vector_store/chroma_store.py index 9a91659f1..1ec9e8b04 100644 --- a/pilot/vector_store/chroma_store.py +++ b/pilot/vector_store/chroma_store.py @@ -13,9 +13,12 @@ class ChromaStore(VectorStoreBase): def __init__(self, ctx: {}) -> None: self.ctx = ctx self.embeddings = ctx["embeddings"] - self.persist_dir = os.path.join(KNOWLEDGE_UPLOAD_ROOT_PATH, - ctx["vector_store_name"] + ".vectordb") - self.vector_store_client = Chroma(persist_directory=self.persist_dir, embedding_function=self.embeddings) + self.persist_dir = os.path.join( + KNOWLEDGE_UPLOAD_ROOT_PATH, ctx["vector_store_name"] + ".vectordb" + ) + self.vector_store_client = Chroma( + persist_directory=self.persist_dir, embedding_function=self.embeddings + ) def similar_search(self, text, topk) -> None: logger.info("ChromaStore similar search") @@ -27,4 +30,3 @@ class ChromaStore(VectorStoreBase): metadatas = [doc.metadata for doc in documents] self.vector_store_client.add_texts(texts=texts, metadatas=metadatas) self.vector_store_client.persist() - diff --git a/pilot/vector_store/connector.py b/pilot/vector_store/connector.py index 003415712..06fad00f2 100644 --- a/pilot/vector_store/connector.py +++ b/pilot/vector_store/connector.py @@ -1,15 +1,12 @@ from pilot.vector_store.chroma_store import ChromaStore from pilot.vector_store.milvus_store import MilvusStore -connector = { - "Chroma": ChromaStore, - "Milvus": MilvusStore - } +connector = {"Chroma": ChromaStore, "Milvus": MilvusStore} class VectorStoreConnector: - """ vector store connector, can connect different vector db provided load document api and similar search api - """ + """vector store connector, can connect different vector db provided load document api and similar search api""" + def __init__(self, vector_store_type, ctx: {}) -> None: self.ctx = ctx self.connector_class = connector[vector_store_type] diff --git a/pilot/vector_store/extract_tovec.py b/pilot/vector_store/extract_tovec.py index c6b83d467..1032876cf 100644 --- a/pilot/vector_store/extract_tovec.py +++ b/pilot/vector_store/extract_tovec.py @@ -3,14 +3,16 @@ import os +from langchain.embeddings import HuggingFaceEmbeddings from langchain.text_splitter import CharacterTextSplitter from langchain.vectorstores import Chroma + +from pilot.configs.model_config import DATASETS_DIR, VECTORE_PATH from pilot.model.vicuna_llm import VicunaEmbeddingLLM -from pilot.configs.model_config import VECTORE_PATH, DATASETS_DIR -from langchain.embeddings import HuggingFaceEmbeddings embeddings = VicunaEmbeddingLLM() + def knownledge_tovec(filename): with open(filename, "r") as f: knownledge = f.read() @@ -22,48 +24,64 @@ def knownledge_tovec(filename): ) return docsearch + def knownledge_tovec_st(filename): - """ Use sentence transformers to embedding the document. - https://github.com/UKPLab/sentence-transformers + """Use sentence transformers to embedding the document. + https://github.com/UKPLab/sentence-transformers """ from pilot.configs.model_config import LLM_MODEL_CONFIG - embeddings = HuggingFaceEmbeddings(model_name=LLM_MODEL_CONFIG["sentence-transforms"]) + + embeddings = HuggingFaceEmbeddings( + model_name=LLM_MODEL_CONFIG["sentence-transforms"] + ) with open(filename, "r") as f: knownledge = f.read() - + text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0) texts = text_splitter.split_text(knownledge) - docsearch = Chroma.from_texts(texts, embeddings, metadatas=[{"source": str(i)} for i in range(len(texts))]) + docsearch = Chroma.from_texts( + texts, embeddings, metadatas=[{"source": str(i)} for i in range(len(texts))] + ) return docsearch def load_knownledge_from_doc(): """Loader Knownledge from current datasets - # TODO if the vector store is exists, just use it. + # TODO if the vector store is exists, just use it. """ if not os.path.exists(DATASETS_DIR): - print("Not Exists Local DataSets, We will answers the Question use model default.") + print( + "Not Exists Local DataSets, We will answers the Question use model default." + ) from pilot.configs.model_config import LLM_MODEL_CONFIG - embeddings = HuggingFaceEmbeddings(model_name=LLM_MODEL_CONFIG["sentence-transforms"]) + + embeddings = HuggingFaceEmbeddings( + model_name=LLM_MODEL_CONFIG["sentence-transforms"] + ) files = os.listdir(DATASETS_DIR) for file in files: - if not os.path.isdir(file): + if not os.path.isdir(file): filename = os.path.join(DATASETS_DIR, file) with open(filename, "r") as f: - knownledge = f.read() + knownledge = f.read() text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_owerlap=0) texts = text_splitter.split_text(knownledge) - docsearch = Chroma.from_texts(texts, embeddings, metadatas=[{"source": str(i)} for i in range(len(texts))], - persist_directory=os.path.join(VECTORE_PATH, ".vectore")) + docsearch = Chroma.from_texts( + texts, + embeddings, + metadatas=[{"source": str(i)} for i in range(len(texts))], + persist_directory=os.path.join(VECTORE_PATH, ".vectore"), + ) return docsearch + def get_vector_storelist(): if not os.path.exists(VECTORE_PATH): return [] - return os.listdir(VECTORE_PATH) \ No newline at end of file + return os.listdir(VECTORE_PATH) diff --git a/pilot/vector_store/file_loader.py b/pilot/vector_store/file_loader.py index 279d5343c..c42eda7a6 100644 --- a/pilot/vector_store/file_loader.py +++ b/pilot/vector_store/file_loader.py @@ -2,56 +2,70 @@ # -*- coding: utf-8 -*- import os -import copy -from typing import Optional, List, Dict -from langchain.prompts import PromptTemplate -from langchain.vectorstores import Chroma -from langchain.text_splitter import CharacterTextSplitter -from langchain.document_loaders import UnstructuredFileLoader, UnstructuredPDFLoader, TextLoader + from langchain.chains import VectorDBQA +from langchain.document_loaders import ( + TextLoader, + UnstructuredFileLoader, + UnstructuredPDFLoader, +) from langchain.embeddings import HuggingFaceEmbeddings -from pilot.configs.model_config import VECTORE_PATH, DATASETS_DIR, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K +from langchain.prompts import PromptTemplate +from langchain.text_splitter import CharacterTextSplitter +from langchain.vectorstores import Chroma + +from pilot.configs.model_config import ( + DATASETS_DIR, + LLM_MODEL_CONFIG, + VECTOR_SEARCH_TOP_K, + VECTORE_PATH, +) class KnownLedge2Vector: - """KnownLedge2Vector class is order to load document to vector + """KnownLedge2Vector class is order to load document to vector and persist to vector store. - - Args: + + Args: - model_name Usage: k2v = KnownLedge2Vector() - persist_dir = os.path.join(VECTORE_PATH, ".vectordb") + persist_dir = os.path.join(VECTORE_PATH, ".vectordb") print(persist_dir) for s, dc in k2v.query("what is oceanbase?"): print(s, dc.page_content, dc.metadata) """ - embeddings: object = None + + embeddings: object = None model_name = LLM_MODEL_CONFIG["sentence-transforms"] top_k: int = VECTOR_SEARCH_TOP_K def __init__(self, model_name=None) -> None: if not model_name: # use default embedding model - self.embeddings = HuggingFaceEmbeddings(model_name=self.model_name) - + self.embeddings = HuggingFaceEmbeddings(model_name=self.model_name) + def init_vector_store(self): persist_dir = os.path.join(VECTORE_PATH, ".vectordb") print("Vector store Persist address is: ", persist_dir) if os.path.exists(persist_dir): # Loader from local file. print("Loader data from local persist vector file...") - vector_store = Chroma(persist_directory=persist_dir, embedding_function=self.embeddings) + vector_store = Chroma( + persist_directory=persist_dir, embedding_function=self.embeddings + ) # vector_store.add_documents(documents=documents) else: documents = self.load_knownlege() - # reinit - vector_store = Chroma.from_documents(documents=documents, - embedding=self.embeddings, - persist_directory=persist_dir) + # reinit + vector_store = Chroma.from_documents( + documents=documents, + embedding=self.embeddings, + persist_directory=persist_dir, + ) vector_store.persist() return vector_store @@ -62,9 +76,11 @@ class KnownLedge2Vector: filename = os.path.join(root, file) docs = self._load_file(filename) # update metadata. - new_docs = [] + new_docs = [] for doc in docs: - doc.metadata = {"source": doc.metadata["source"].replace(DATASETS_DIR, "")} + doc.metadata = { + "source": doc.metadata["source"].replace(DATASETS_DIR, "") + } print("Documents to vector running, please wait...", doc.metadata) new_docs.append(doc) docments += new_docs @@ -73,7 +89,7 @@ class KnownLedge2Vector: def _load_file(self, filename): # Loader file if filename.lower().endswith(".pdf"): - loader = UnstructuredFileLoader(filename) + loader = UnstructuredFileLoader(filename) text_splitor = CharacterTextSplitter() docs = loader.load_and_split(text_splitor) else: @@ -86,13 +102,10 @@ class KnownLedge2Vector: """Load data from url address""" pass - def query(self, q): - """Query similar doc from Vector """ + """Query similar doc from Vector""" vector_store = self.init_vector_store() docs = vector_store.similarity_search_with_score(q, k=self.top_k) for doc in docs: dc, s = doc yield s, dc - - \ No newline at end of file diff --git a/pilot/vector_store/milvus_store.py b/pilot/vector_store/milvus_store.py index a61027850..3ae265f7b 100644 --- a/pilot/vector_store/milvus_store.py +++ b/pilot/vector_store/milvus_store.py @@ -1,15 +1,17 @@ -from typing import List, Optional, Iterable, Tuple, Any - -from pymilvus import connections, Collection, DataType +from typing import Any, Iterable, List, Optional, Tuple from langchain.docstore.document import Document +from pymilvus import Collection, DataType, connections from pilot.configs.config import Config from pilot.vector_store.vector_store_base import VectorStoreBase CFG = Config() + + class MilvusStore(VectorStoreBase): """Milvus database""" + def __init__(self, ctx: {}) -> None: """init a milvus storage connection. @@ -66,12 +68,12 @@ class MilvusStore(VectorStoreBase): def init_schema_and_load(self, vector_name, documents): """Create a Milvus collection, indexes it with HNSW, load document. - Args: - vector_name (Embeddings): your collection name. - documents (List[str]): Text to insert. - Returns: - VectorStore: The MilvusStore vector store. - """ + Args: + vector_name (Embeddings): your collection name. + documents (List[str]): Text to insert. + Returns: + VectorStore: The MilvusStore vector store. + """ try: from pymilvus import ( Collection, @@ -237,13 +239,10 @@ class MilvusStore(VectorStoreBase): partition_name: Optional[str] = None, timeout: Optional[int] = None, ) -> List[str]: - """add text data into Milvus. - """ + """add text data into Milvus.""" insert_dict: Any = {self.text_field: list(texts)} try: - insert_dict[self.vector_field] = self.embedding.embed_documents( - list(texts) - ) + insert_dict[self.vector_field] = self.embedding.embed_documents(list(texts)) except NotImplementedError: insert_dict[self.vector_field] = [ self.embedding.embed_query(x) for x in texts diff --git a/pilot/vector_store/vector_store_base.py b/pilot/vector_store/vector_store_base.py index b483b3116..70888f5aa 100644 --- a/pilot/vector_store/vector_store_base.py +++ b/pilot/vector_store/vector_store_base.py @@ -12,4 +12,4 @@ class VectorStoreBase(ABC): @abstractmethod def similar_search(self, text, topk) -> None: """Initialize schema in vector database.""" - pass \ No newline at end of file + pass diff --git a/tests/unit/test_plugins.py b/tests/unit/test_plugins.py index 21dbaaf27..a2a3d2506 100644 --- a/tests/unit/test_plugins.py +++ b/tests/unit/test_plugins.py @@ -1,6 +1,6 @@ -import pytest import os +import pytest from pilot.configs.config import Config from pilot.plugins import ( @@ -15,10 +15,13 @@ PLUGIN_TEST_ZIP_FILE = "Auto-GPT-Plugin-Test-master.zip" PLUGIN_TEST_INIT_PY = "Auto-GPT-Plugin-Test-master/src/auto_gpt_vicuna/__init__.py" PLUGIN_TEST_OPENAI = "https://weathergpt.vercel.app/" + def test_inspect_zip_for_modules(): current_dir = os.getcwd() print(current_dir) - result = inspect_zip_for_modules(str(f"{current_dir}/{PLUGINS_TEST_DIR_TEMP}/{PLUGIN_TEST_ZIP_FILE}")) + result = inspect_zip_for_modules( + str(f"{current_dir}/{PLUGINS_TEST_DIR_TEMP}/{PLUGIN_TEST_ZIP_FILE}") + ) assert result == [PLUGIN_TEST_INIT_PY] @@ -99,6 +102,7 @@ def mock_config_openai_plugin(): class MockConfig: """Mock config object for testing the scan_plugins function""" + current_dir = os.getcwd() plugins_dir = f"{current_dir}/{PLUGINS_TEST_DIR_TEMP}/" plugins_openai = [PLUGIN_TEST_OPENAI] diff --git a/tools/knowlege_init.py b/tools/knowlege_init.py index 23ca33a80..df8697273 100644 --- a/tools/knowlege_init.py +++ b/tools/knowlege_init.py @@ -2,8 +2,13 @@ # -*- coding: utf-8 -*- import argparse -from pilot.configs.model_config import DATASETS_DIR, LLM_MODEL_CONFIG, VECTOR_SEARCH_TOP_K, VECTOR_STORE_CONFIG, \ - VECTOR_STORE_TYPE +from pilot.configs.model_config import ( + DATASETS_DIR, + LLM_MODEL_CONFIG, + VECTOR_SEARCH_TOP_K, + VECTOR_STORE_CONFIG, + VECTOR_STORE_TYPE, +) from pilot.source_embedding.knowledge_embedding import KnowledgeEmbedding @@ -16,22 +21,24 @@ class LocalKnowledgeInit: self.vector_store_config = vector_store_config def knowledge_persist(self, file_path, append_mode): - """ knowledge persist """ + """knowledge persist""" kv = KnowledgeEmbedding( file_path=file_path, model_name=LLM_MODEL_CONFIG["text2vec"], - vector_store_config= self.vector_store_config) + vector_store_config=self.vector_store_config, + ) vector_store = kv.knowledge_persist_initialization(append_mode) return vector_store def query(self, q): - """Query similar doc from Vector """ + """Query similar doc from Vector""" vector_store = self.init_vector_store() docs = vector_store.similarity_search_with_score(q, k=self.top_k) for doc in docs: dc, s = doc yield s, dc + if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--vector_name", type=str, default="default") @@ -41,8 +48,12 @@ if __name__ == "__main__": vector_name = args.vector_name append_mode = args.append store_type = VECTOR_STORE_TYPE - vector_store_config = {"url": VECTOR_STORE_CONFIG["url"], "port": VECTOR_STORE_CONFIG["port"], "vector_store_name":vector_name} + vector_store_config = { + "url": VECTOR_STORE_CONFIG["url"], + "port": VECTOR_STORE_CONFIG["port"], + "vector_store_name": vector_name, + } print(vector_store_config) - kv = LocalKnowledgeInit(vector_store_config=vector_store_config) + kv = LocalKnowledgeInit(vector_store_config=vector_store_config) vector_store = kv.knowledge_persist(file_path=DATASETS_DIR, append_mode=append_mode) - print("your knowledge embedding success...") \ No newline at end of file + print("your knowledge embedding success...") From 2ee4a25af4d009a53a7da76d4d176d3e7f08bb6b Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Wed, 24 May 2023 18:53:16 +0800 Subject: [PATCH 65/66] doc: add Contribution guide --- README.md | 4 ++++ README.zh.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index 18dd30f80..8e36d5103 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,10 @@ The achievements of this project are thanks to the technical community, especial - [ChatGLM](https://github.com/THUDM/ChatGLM-6B) as the base model - [llama_index](https://github.com/jerryjliu/llama_index) for enhancing database-related knowledge using [in-context learning](https://arxiv.org/abs/2301.00234) based on existing knowledge bases. +## Contribution + +- Please run `black .` before submitting the code. + ## Contributors diff --git a/README.zh.md b/README.zh.md index b84671da2..1d80b747d 100644 --- a/README.zh.md +++ b/README.zh.md @@ -218,6 +218,10 @@ python tools/knowledge_init.py - [ChatGLM](https://github.com/THUDM/ChatGLM-6B) 基础模型 - [llama-index](https://github.com/jerryjliu/llama_index) 基于现有知识库进行[In-Context Learning](https://arxiv.org/abs/2301.00234)来对其进行数据库相关知识的增强。 +# 贡献 + +- 提交代码前请先执行 `black .` + ## 贡献者 From 2e1d4c8a1aa8864e94b06a20072eb3589ab16187 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Wed, 24 May 2023 18:53:58 +0800 Subject: [PATCH 66/66] ci: ci branch name --- .github/workflows/pylint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index e82f1b9d4..0fedfc130 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -2,7 +2,7 @@ name: Pylint on: push: - branches: [ make_ci_happy ] + branches: [ main ] pull_request: branches: [ main ] workflow_dispatch: