From d36d801a5836239ffdbc2c70f88cb8afe93a5abe Mon Sep 17 00:00:00 2001 From: liunux4odoo Date: Thu, 3 Aug 2023 09:33:24 +0800 Subject: [PATCH 1/5] add api request methods for all chat apis --- webui_pages/utils.py | 60 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/webui_pages/utils.py b/webui_pages/utils.py index 12dab3d..89e99d4 100644 --- a/webui_pages/utils.py +++ b/webui_pages/utils.py @@ -231,6 +231,66 @@ class ApiRequest: response = self.post("/chat/chat", json=f"{query}", stream=True) return response + def knowledge_base_chat( + self, + query: str, + knowledge_base_name: str, + no_remote_api: bool = False, + ): + ''' + 对应/chat/knowledge_base_chat接口 + ''' + if no_remote_api: + from server.chat.knowledge_base_chat import knowledge_base_chat + response = knowledge_base_chat(query, knowledge_base_name) + return self._stream2generator(response) + else: + response = self.post( + "/chat/knowledge_base_chat", + json={"query": query, "knowledge_base_name": knowledge_base_name}, + stream=True, + ) + return response + + def duckduckgo_search_chat( + self, + query: str, + no_remote_api: bool = False, + ): + ''' + 对应api.py/chat/duckduckgo_search_chat接口 + ''' + if no_remote_api: + from server.chat.duckduckgo_search_chat import duckduckgo_search_chat + response = duckduckgo_search_chat(query) + return self._stream2generator(response) + else: + response = self.post( + "/chat/duckduckgo_search_chat", + json=f"{query}", + stream=True, + ) + return response + + def bing_search_chat( + self, + query: str, + no_remote_api: bool = False, + ): + ''' + 对应api.py/chat/bing_search_chat接口 + ''' + if no_remote_api: + from server.chat.bing_search_chat import bing_search_chat + response = bing_search_chat(query) + return self._stream2generator(response) + else: + response = self.post( + "/chat/bing_search_chat", + json=f"{query}", + stream=True, + ) + return response if __name__ == "__main__": api = ApiRequest() From 3c2b8a85d68bf5a8c5369984adc54cb056724084 Mon Sep 17 00:00:00 2001 From: liunux4odoo Date: Thu, 3 Aug 2023 10:49:57 +0800 Subject: [PATCH 2/5] add knowledge base api methods --- webui_pages/utils.py | 200 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 186 insertions(+), 14 deletions(-) diff --git a/webui_pages/utils.py b/webui_pages/utils.py index 89e99d4..72dd251 100644 --- a/webui_pages/utils.py +++ b/webui_pages/utils.py @@ -1,5 +1,6 @@ # 该文件包含webui通用工具,可以被不同的webui使用 +import tempfile from typing import * from pathlib import Path import os @@ -87,9 +88,11 @@ class ApiRequest: self, base_url: str = "http://127.0.0.1:7861", timeout: float = 60.0, + no_remote_api: bool = False, # call api view function directly ): self.base_url = base_url self.timeout = timeout + self.no_remote_api = no_remote_api def _parse_url(self, url: str) -> str: if (not url.startswith("http") @@ -112,7 +115,7 @@ class ApiRequest: kwargs.setdefault("timeout", self.timeout) while retry > 0: try: - return httpx.get(url, params, **kwargs) + return httpx.get(url, params=params, **kwargs) except: retry -= 1 @@ -128,7 +131,7 @@ class ApiRequest: async with httpx.AsyncClient() as client: while retry > 0: try: - return await client.get(url, params, **kwargs) + return await client.get(url, params=params, **kwargs) except: retry -= 1 @@ -180,6 +183,8 @@ class ApiRequest: loop = asyncio.new_event_loop() return iter_over_async(response.body_iterator, loop) + # 对话相关操作 + def chat_fastchat( self, messages: List[Dict], @@ -187,12 +192,14 @@ class ApiRequest: model: str = LLM_MODEL, temperature: float = 0.7, max_tokens: int = 1024, # todo:根据message内容自动计算max_tokens - no_remote_api=False, # all api view function directly + no_remote_api: bool = None, **kwargs: Any, ): ''' 对应api.py/chat/fastchat接口 ''' + if no_remote_api is None: + no_remote_api = self.no_remote_api msg = OpenAiChatMsgIn(**{ "messages": messages, "stream": stream, @@ -218,11 +225,14 @@ class ApiRequest: def chat_chat( self, query: str, - no_remote_api: bool = False, + no_remote_api: bool = None, ): ''' 对应api.py/chat/chat接口 ''' + if no_remote_api is None: + no_remote_api = self.no_remote_api + if no_remote_api: from server.chat.chat import chat response = chat(query) @@ -235,11 +245,14 @@ class ApiRequest: self, query: str, knowledge_base_name: str, - no_remote_api: bool = False, + no_remote_api: bool = None, ): ''' - 对应/chat/knowledge_base_chat接口 + 对应api.py/chat/knowledge_base_chat接口 ''' + if no_remote_api is None: + no_remote_api = self.no_remote_api + if no_remote_api: from server.chat.knowledge_base_chat import knowledge_base_chat response = knowledge_base_chat(query, knowledge_base_name) @@ -255,11 +268,14 @@ class ApiRequest: def duckduckgo_search_chat( self, query: str, - no_remote_api: bool = False, + no_remote_api: bool = None, ): ''' 对应api.py/chat/duckduckgo_search_chat接口 ''' + if no_remote_api is None: + no_remote_api = self.no_remote_api + if no_remote_api: from server.chat.duckduckgo_search_chat import duckduckgo_search_chat response = duckduckgo_search_chat(query) @@ -275,11 +291,14 @@ class ApiRequest: def bing_search_chat( self, query: str, - no_remote_api: bool = False, + no_remote_api: bool = None, ): ''' 对应api.py/chat/bing_search_chat接口 ''' + if no_remote_api is None: + no_remote_api = self.no_remote_api + if no_remote_api: from server.chat.bing_search_chat import bing_search_chat response = bing_search_chat(query) @@ -292,16 +311,169 @@ class ApiRequest: ) return response + # 知识库相关操作 + + def list_knowledge_bases( + self, + no_remote_api: bool = None, + ): + ''' + 对应api.py/knowledge_base/list_knowledge_bases接口 + ''' + if no_remote_api is None: + no_remote_api = self.no_remote_api + + if no_remote_api: + from server.knowledge_base.kb_api import list_kbs + response = run_async(list_kbs()) + return response.data + else: + response = self.get("/knowledge_base/list_knowledge_bases") + return response.json().get("data") + + def create_knowledge_base( + self, + knowledge_base_name: str, + no_remote_api: bool = None, + ): + ''' + 对应api.py/knowledge_base/create_knowledge_base接口 + ''' + if no_remote_api is None: + no_remote_api = self.no_remote_api + + if no_remote_api: + from server.knowledge_base.kb_api import create_kb + response = run_async(create_kb(knowledge_base_name)) + return response.dict() + else: + response = self.post( + "/knowledge_base/create_knowledge_base", + json={"knowledge_base_name": knowledge_base_name}, + ) + return response.json() + + def delete_knowledge_base( + self, + knowledge_base_name: str, + no_remote_api: bool = None, + ): + ''' + 对应api.py/knowledge_base/delete_knowledge_base接口 + ''' + if no_remote_api is None: + no_remote_api = self.no_remote_api + + if no_remote_api: + from server.knowledge_base.kb_api import delete_kb + response = run_async(delete_kb(knowledge_base_name)) + return response.dict() + else: + response = self.delete( + "/knowledge_base/delete_knowledge_base", + json={"knowledge_base_name": knowledge_base_name}, + ) + return response.json() + + def list_kb_docs( + self, + knowledge_base_name: str, + no_remote_api: bool = None, + ): + ''' + 对应api.py/knowledge_base/list_docs接口 + ''' + if no_remote_api is None: + no_remote_api = self.no_remote_api + + if no_remote_api: + from server.knowledge_base.kb_doc_api import list_docs + response = run_async(list_docs(knowledge_base_name)) + return response.data + else: + response = self.get( + "/knowledge_base/list_docs", + params={"knowledge_base_name": knowledge_base_name} + ) + return response.json().get("data") + + def upload_kb_doc( + self, + file: Union[str, Path], + knowledge_base_name: str, + no_remote_api: bool = None, + ): + ''' + 对应api.py/knowledge_base/upload_docs接口 + ''' + if no_remote_api is None: + no_remote_api = self.no_remote_api + + file = Path(file).absolute() + filename = file.name + + if no_remote_api: + from server.knowledge_base.kb_doc_api import upload_doc + from fastapi import UploadFile + from tempfile import SpooledTemporaryFile + + temp_file = SpooledTemporaryFile(max_size=10 * 1024 * 1024) + with file.open("rb") as fp: + temp_file.write(fp.read()) + response = run_async(upload_doc( + UploadFile(temp_file, filename=filename), + knowledge_base_name, + )) + return response.dict() + else: + response = self.post( + "/knowledge_base/upload_doc", + data={"knowledge_base_name": knowledge_base_name}, + files={"file": (filename, file.open("rb"))}, + ) + return response.json() + + def delete_kb_doc( + self, + knowledge_base_name: str, + doc_name: str, + no_remote_api: bool = None, + ): + ''' + 对应api.py/knowledge_base/delete_doc接口 + ''' + if no_remote_api is None: + no_remote_api = self.no_remote_api + + if no_remote_api: + from server.knowledge_base.kb_doc_api import delete_doc + response = run_async(delete_doc(knowledge_base_name, doc_name)) + return response.dict() + else: + response = self.delete( + "/knowledge_base/delete_doc", + json={"knowledge_base_name": knowledge_base_name, "doc_name": doc_name}, + ) + return response.json() + + if __name__ == "__main__": api = ApiRequest() + # print(api.chat_fastchat( # messages=[{"role": "user", "content": "hello"}] # )) - with api.chat_chat("你好") as r: - for t in r.iter_text(None): - print(t) + # with api.chat_chat("你好") as r: + # for t in r.iter_text(None): + # print(t) - r = api.chat_chat("你好", no_remote_api=True) - for t in r: - print(t) \ No newline at end of file + # r = api.chat_chat("你好", no_remote_api=True) + # for t in r: + # print(t) + + # r = api.duckduckgo_search_chat("室温超导最新研究进展", no_remote_api=True) + # for t in r: + # print(t) + + print(api.list_knowledge_bases()) From f7d465b7d44b9db0e1553d642d99510b205a6297 Mon Sep 17 00:00:00 2001 From: liunux4odoo Date: Thu, 3 Aug 2023 12:11:18 +0800 Subject: [PATCH 3/5] bug fix --- server/llm_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server/llm_api.py b/server/llm_api.py index 8f5c401..a844838 100644 --- a/server/llm_api.py +++ b/server/llm_api.py @@ -13,7 +13,6 @@ openai_api_port = 8888 base_url = "http://127.0.0.1:{}" queue = Queue() sys.modules['fastchat.constants.LOGDIR'] = LOG_PATH -import parser def set_httpx_timeout(timeout=60.0): From dc74bdab4149a4210c6afa37a91d06590c1e5e7d Mon Sep 17 00:00:00 2001 From: liunux4odoo Date: Thu, 3 Aug 2023 12:52:49 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E5=B0=86httpx.stream=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E8=BD=AC=E5=8C=96=E4=B8=BA=E6=99=AE=E9=80=9A?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=99=A8=EF=BC=8C=E4=BD=BF=E5=BE=97httpx=20a?= =?UTF-8?q?pi=E8=AF=B7=E6=B1=82=E4=B8=8E=E7=9B=B4=E6=8E=A5=E8=B0=83?= =?UTF-8?q?=E7=94=A8server.chat.xx=E8=BF=94=E5=9B=9E=E7=9B=B8=E5=90=8C?= =?UTF-8?q?=E7=9A=84=E7=BB=93=E6=9E=9C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- webui_pages/utils.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/webui_pages/utils.py b/webui_pages/utils.py index 72dd251..747b685 100644 --- a/webui_pages/utils.py +++ b/webui_pages/utils.py @@ -13,6 +13,7 @@ import httpx import asyncio from server.chat.openai_chat import OpenAiChatMsgIn from fastapi.responses import StreamingResponse +import contextlib def set_httpx_timeout(timeout=60.0): @@ -173,7 +174,7 @@ class ApiRequest: except: retry -= 1 - def _stream2generator(self, response: StreamingResponse): + def _fastapi_stream2generator(self, response: StreamingResponse): ''' 将api.py中视图函数返回的StreamingResponse转化为同步生成器 ''' @@ -183,6 +184,14 @@ class ApiRequest: loop = asyncio.new_event_loop() return iter_over_async(response.body_iterator, loop) + def _httpx_stream2generator(self,response: contextlib._GeneratorContextManager): + ''' + 将httpx.stream返回的GeneratorContextManager转化为普通生成器 + ''' + with response as r: + for chunk in r.iter_text(None): + yield chunk + # 对话相关操作 def chat_fastchat( @@ -212,7 +221,7 @@ class ApiRequest: if no_remote_api: from server.chat.openai_chat import openai_chat response = openai_chat(msg) - return self._stream2generator(response) + return self._fastapi_stream2generator(response) else: data = msg.dict(exclude_unset=True, exclude_none=True) response = self.post( @@ -220,7 +229,7 @@ class ApiRequest: json=data, stream=stream, ) - return response + return self._httpx_stream2generator(response) def chat_chat( self, @@ -236,10 +245,10 @@ class ApiRequest: if no_remote_api: from server.chat.chat import chat response = chat(query) - return self._stream2generator(response) + return self._fastapi_stream2generator(response) else: response = self.post("/chat/chat", json=f"{query}", stream=True) - return response + return self._httpx_stream2generator(response) def knowledge_base_chat( self, @@ -256,14 +265,14 @@ class ApiRequest: if no_remote_api: from server.chat.knowledge_base_chat import knowledge_base_chat response = knowledge_base_chat(query, knowledge_base_name) - return self._stream2generator(response) + return self._fastapi_stream2generator(response) else: response = self.post( "/chat/knowledge_base_chat", json={"query": query, "knowledge_base_name": knowledge_base_name}, stream=True, ) - return response + return self._httpx_stream2generator(response) def duckduckgo_search_chat( self, @@ -279,14 +288,14 @@ class ApiRequest: if no_remote_api: from server.chat.duckduckgo_search_chat import duckduckgo_search_chat response = duckduckgo_search_chat(query) - return self._stream2generator(response) + return self._fastapi_stream2generator(response) else: response = self.post( "/chat/duckduckgo_search_chat", json=f"{query}", stream=True, ) - return response + return self._httpx_stream2generator(response) def bing_search_chat( self, @@ -302,14 +311,14 @@ class ApiRequest: if no_remote_api: from server.chat.bing_search_chat import bing_search_chat response = bing_search_chat(query) - return self._stream2generator(response) + return self._fastapi_stream2generator(response) else: response = self.post( "/chat/bing_search_chat", json=f"{query}", stream=True, ) - return response + return self._httpx_stream2generator(response) # 知识库相关操作 From 4651b50176a111ab2acbb82a0c4d2d67f789b301 Mon Sep 17 00:00:00 2001 From: liunux4odoo Date: Thu, 3 Aug 2023 13:41:31 +0800 Subject: [PATCH 5/5] =?UTF-8?q?WEB=20UI=E4=B8=AD=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=B7=B2=E5=AE=8C=E6=88=90=E3=80=82=E7=9B=AE?= =?UTF-8?q?=E5=89=8D=E5=BE=88=E5=A4=9A=E5=8F=82=E6=95=B0=E8=BF=98=E4=B8=8D?= =?UTF-8?q?=E6=94=AF=E6=8C=81=EF=BC=8C=E5=BE=85API=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=90=8E=E5=86=8D=E5=8A=A0=E5=85=A5=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- webui.py | 6 +++ webui_pages/dialogue/dialogue.py | 90 ++++++++++++++++++++++++-------- 2 files changed, 75 insertions(+), 21 deletions(-) diff --git a/webui.py b/webui.py index bd02735..fe58240 100644 --- a/webui.py +++ b/webui.py @@ -1,3 +1,9 @@ +# 运行方式: +# 1. 安装必要的包:pip install streamlit-option-menu streamlit-chatbox>=1.1.3 +# 2. 运行本机fastchat服务:python server\llm_api.py 或者 运行对应的sh文件 +# 3. 运行API服务器:python server/api.py。如果使用api = ApiRequest(no_remote_api=True),该步可以跳过。 +# 4. 运行WEB UI:streamlit run webui.py --server.port 7860 + import streamlit as st from webui_pages.utils import * from streamlit_option_menu import option_menu diff --git a/webui_pages/dialogue/dialogue.py b/webui_pages/dialogue/dialogue.py index f01c0ac..5f7faf9 100644 --- a/webui_pages/dialogue/dialogue.py +++ b/webui_pages/dialogue/dialogue.py @@ -2,37 +2,85 @@ import streamlit as st from webui_pages.utils import * from streamlit_chatbox import * -chat_box = ChatBox() - def dialogue_page(api: ApiRequest): + chat_box = ChatBox( + greetings=[ + f"欢迎使用 [`Langchain-Chatglm`](https://github.com/chatchat-space/langchain-chatglm) ! 当前使用模型`{LLM_MODEL}`, 您可以开始提问了.", + ] + ) + with st.sidebar: + def on_mode_change(): + mode = st.session_state.dialogue_mode + text = f"已切换到 {mode} 模式。" + if mode == "知识库问答": + cur_kb = st.session_state.get("selected_kb") + if cur_kb: + text = f"{text} 当前知识库: `{cur_kb}`。" + chat_box.ai_say(text, not_render=True) + dialogue_mode = st.radio("请选择对话模式", ["LLM 对话", "知识库问答", - "Bing 搜索问答"]) - history_len = st.slider("历史对话轮数:", 1, 10, 1) + "Bing 搜索问答", + "Duck 搜索问答", + ], + on_change=on_mode_change, + key="dialogue_mode", + ) + history_len = st.slider("历史对话轮数:", 1, 10, 1, disabled=True) + # todo: support history len + if st.button("清除历史对话"): + chat_box.reset_history() + + def on_kb_change(): + chat_box.ai_say(f"已加载知识库: {st.session_state.selected_kb}", not_render=True) + if dialogue_mode == "知识库问答": - selected_kb = st.selectbox("请选择知识库:", get_kb_list()) - with st.expander(f"{selected_kb} 中已存储文件"): - st.write(get_kb_files(selected_kb)) + kb_list = api.list_knowledge_bases() + selected_kb = st.selectbox( + "请选择知识库:", + kb_list, + on_change=on_kb_change, + key="selected_kb", + ) + top_k = st.slider("匹配知识条数:", 1, 20, 3, disabled=True) + score_threshold = st.slider("知识匹配分数阈值:", 0, 1000, 0, disabled=True) + chunk_content = st.checkbox("关联上下文", False, disabled=True) + chunk_size = st.slider("关联长度:", 0, 500, 250, disabled=True) # Display chat messages from history on app rerun chat_box.output_messages() if prompt := st.chat_input("请输入对话内容,换行请使用Ctrl+Enter"): chat_box.user_say(prompt) - chat_box.ai_say("正在思考...") - # with api.chat_fastchat([{"role": "user", "content": "prompt"}], stream=streaming) as r: - # todo: support history len - text = "" - r = api.chat_chat(prompt, no_remote_api=True) - for t in r: - text += t - chat_box.update_msg(text) - chat_box.update_msg(text, streaming=False) - # with api.chat_chat(prompt) as r: - # for t in r.iter_text(None): - # text += t - # chat_box.update_msg(text) - # chat_box.update_msg(text, streaming=False) + if dialogue_mode == "LLM 对话": + chat_box.ai_say("正在思考...") + text = "" + r = api.chat_chat(prompt, no_remote_api=True) + for t in r: + text += t + chat_box.update_msg(text) + chat_box.update_msg(text, streaming=False) # 更新最终的字符串,去除光标 + elif dialogue_mode == "知识库问答": + chat_box.ai_say(f"正在查询知识库: `{selected_kb}` ...") + text = "" + for t in api.knowledge_base_chat(prompt, selected_kb): + text += t + chat_box.update_msg(text) + chat_box.update_msg(text, streaming=False) + elif dialogue_mode == "Bing 搜索问答": + chat_box.ai_say("正在执行Bing搜索...") + text = "" + for t in api.bing_search_chat(prompt): + text += t + chat_box.update_msg(text) + chat_box.update_msg(text, streaming=False) + elif dialogue_mode == "Duck 搜索问答": + chat_box.ai_say("正在执行Duckduck搜索...") + text = "" + for t in api.duckduckgo_search_chat(prompt): + text += t + chat_box.update_msg(text) + chat_box.update_msg(text, streaming=False)