Skip to content

Commit ded35a7

Browse files
committed
add network search feature.
1 parent 99f5345 commit ded35a7

File tree

25 files changed

+2860
-138
lines changed

25 files changed

+2860
-138
lines changed

WebUI/Server/api.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@
1414
from WebUI.Server.chat.feedback import chat_feedback
1515
from WebUI.Server.embeddings_api import embed_texts_endpoint
1616
from WebUI.Server.chat.openai_chat import openai_chat
17+
from WebUI.Server.chat.search_engine_chat import search_engine_chat
1718
from WebUI.Server.llm_api import (list_running_models, get_running_models, list_config_models,
1819
change_llm_model, stop_llm_model, chat_llm_model, download_llm_model,
1920
get_model_config, save_chat_config, save_model_config, get_webui_configs,
2021
get_vtot_model, get_vtot_data, stop_vtot_model, change_vtot_model, save_voice_model_config,
2122
get_speech_model, get_speech_data, save_speech_model_config, stop_speech_model, change_speech_model,
2223
get_image_recognition_model, save_image_recognition_model_config, eject_image_recognition_model, change_image_recognition_model, get_image_recognition_data,
2324
get_image_generation_model, save_image_generation_model_config, eject_image_generation_model, change_image_generation_model, get_image_generation_data,
24-
llm_knowledge_base_chat, list_search_engines)
25+
save_search_engine_config,
26+
llm_knowledge_base_chat, llm_search_engine_chat, list_search_engines)
2527
from WebUI.Server.utils import(BaseResponse, ListResponse, FastAPI, MakeFastAPIOffline,
2628
get_server_configs, get_prompt_template)
2729
from typing import List, Literal
@@ -68,11 +70,6 @@ def mount_app_routes(app: FastAPI, run_mode: str = None):
6870
summary="Save chat configration information",
6971
)(save_chat_config)
7072

71-
#app.post("/chat/search_engine_chat",
72-
# tags=["Chat"],
73-
# summary="Chat with search engine.",
74-
# )(search_engine_chat)
75-
7673
app.post("/chat/feedback",
7774
tags=["Chat"],
7875
summary="Return dialogue scores.",
@@ -127,6 +124,22 @@ def mount_app_routes(app: FastAPI, run_mode: str = None):
127124
summary="Download LLM Model (Model Worker)",
128125
)(download_llm_model)
129126

127+
# Search Engine interface
128+
app.post("/search_engine/save_search_engine_config",
129+
tags=["Search Engine Management"],
130+
summary="Save config for search engine",
131+
)(save_search_engine_config)
132+
133+
app.post("/search_engine/search_engine_chat",
134+
tags=["Search Engine Management"],
135+
summary="Chat with search engine.",
136+
)(search_engine_chat)
137+
138+
app.post("/llm_model/search_engine_chat",
139+
tags=["Search Engine Management"],
140+
summary="Chat with search engine.",
141+
)(llm_search_engine_chat)
142+
130143
# Voice Model interface
131144
app.post("/voice_model/get_vtot_model",
132145
tags=["Voice Model Management"],
@@ -236,15 +249,6 @@ def mount_app_routes(app: FastAPI, run_mode: str = None):
236249
tags=["Server State"],
237250
summary="get webui config",
238251
)(get_webui_configs)
239-
#app.post("/server/configs",
240-
# tags=["Server State"],
241-
# summary="Get server configration info.",
242-
# )(get_server_configs)
243-
244-
#app.post("/server/list_search_engines",
245-
# tags=["Server State"],
246-
# summary="Get all search engine info.",
247-
# )(list_search_engines)
248252

249253
@app.post("/server/get_prompt_template",
250254
tags=["Server State"],
@@ -328,7 +332,6 @@ def mount_knowledge_routes(app: FastAPI):
328332
summary="update doc for knowledge base"
329333
)(update_docs_by_id)
330334

331-
332335
app.post("/knowledge_base/upload_docs",
333336
tags=["Knowledge Base Management"],
334337
response_model=BaseResponse,
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
from fastapi import Body, Request
2+
from WebUI.configs import GetProviderByName
3+
from fastapi.responses import StreamingResponse
4+
from WebUI.Server.chat.utils import History
5+
from langchain.docstore.document import Document
6+
from langchain.chains import LLMChain
7+
import asyncio
8+
import json
9+
import os
10+
from WebUI.Server.utils import wrap_done, get_ChatOpenAI
11+
from fastapi.concurrency import run_in_threadpool
12+
from WebUI.Server.utils import get_prompt_template
13+
from langchain.callbacks import AsyncIteratorCallbackHandler
14+
from langchain.utilities.bing_search import BingSearchAPIWrapper
15+
from langchain.utilities.duckduckgo_search import DuckDuckGoSearchAPIWrapper
16+
from langchain.text_splitter import RecursiveCharacterTextSplitter
17+
from WebUI.configs.webuiconfig import InnerJsonConfigWebUIParse
18+
from langchain.prompts.chat import ChatPromptTemplate
19+
from typing import AsyncIterable, Dict, List, Optional
20+
21+
def bing_search(text, search_url, api_key, result_len, **kwargs):
22+
search = BingSearchAPIWrapper(bing_subscription_key=api_key,
23+
bing_search_url=search_url)
24+
return search.results(text, result_len)
25+
26+
27+
def duckduckgo_search(text, search_url, api_key, result_len, **kwargs):
28+
search = DuckDuckGoSearchAPIWrapper()
29+
return search.results(text, result_len)
30+
31+
def metaphor_search(
32+
text: str,
33+
search_url: str,
34+
api_key: str,
35+
result_len: int,
36+
chunk_size: int = 500,
37+
chunk_overlap: int = 50,
38+
) -> List[Dict]:
39+
from exa_py import Exa
40+
from markdownify import markdownify
41+
from strsimpy.normalized_levenshtein import NormalizedLevenshtein
42+
43+
highlights_options = {
44+
"num_sentences": 7, # how long our highlights should be
45+
"highlights_per_url": 1, # just get the best highlight for each URL
46+
}
47+
48+
info_for_llm = []
49+
exa = Exa(api_key=api_key)
50+
search_response = exa.search_and_contents(text, highlights=highlights_options, num_results=result_len, use_autoprompt=True)
51+
info = [sr for sr in search_response.results]
52+
for x in info:
53+
x.highlights[0] = markdownify(x.highlights[0])
54+
info_for_llm = info
55+
56+
docs = [{"snippet": x.highlights[0],
57+
"link": x.url,
58+
"title": x.title}
59+
for x in info_for_llm]
60+
return docs
61+
62+
SEARCH_ENGINES = {
63+
"bing": bing_search,
64+
"duckduckgo": duckduckgo_search,
65+
"metaphor": metaphor_search,
66+
}
67+
68+
def search_result2docs(search_results):
69+
docs = []
70+
for result in search_results:
71+
doc = Document(page_content=result["snippet"] if "snippet" in result.keys() else "",
72+
metadata={"source": result["link"] if "link" in result.keys() else "",
73+
"filename": result["title"] if "title" in result.keys() else ""})
74+
docs.append(doc)
75+
return docs
76+
77+
78+
async def lookup_search_engine(
79+
query: str,
80+
search_engine_name: str,
81+
top_k: int = 3,
82+
):
83+
search_engine = SEARCH_ENGINES[search_engine_name]
84+
85+
configinst = InnerJsonConfigWebUIParse()
86+
webui_config = configinst.dump()
87+
config = webui_config.get("SearchEngine").get(search_engine_name)
88+
api_key = config.get("api_key", "")
89+
search_url = config.get("search_url", "")
90+
if search_engine_name == "bing":
91+
if api_key == "" or api_key == "YOUR_API_KEY":
92+
api_key = os.environ.get("BING_SUBSCRIPTION_KEY", "")
93+
if search_url == "":
94+
search_url = os.environ.get("BING_SEARCH_URL", "")
95+
elif search_engine_name == "metaphor":
96+
if api_key == "" or api_key == "YOUR_API_KEY":
97+
api_key = os.environ.get("METAPHOR_API_KEY", "")
98+
results = await run_in_threadpool(search_engine, query, search_url=search_url, api_key=api_key, result_len=top_k)
99+
docs = search_result2docs(results)
100+
return docs
101+
102+
async def search_engine_chat(
103+
query: str = Body(..., description="User input: ", examples=["chat"]),
104+
search_engine_name: str = Body(..., description="search engine name", examples=["duckduckgo"]),
105+
history: List[dict] = Body([],
106+
description="History chat",
107+
examples=[[
108+
{"role": "user", "content": "Who are you?"},
109+
{"role": "assistant", "content": "I am AI."}]]
110+
),
111+
stream: bool = Body(False, description="stream output"),
112+
model_name: str = Body("", description="model name"),
113+
temperature: float = Body(0.7, description="LLM Temperature", ge=0.0, le=1.0),
114+
max_tokens: Optional[int] = Body(None, description="max tokens."),
115+
prompt_name: str = Body("default", description=""),
116+
) -> StreamingResponse:
117+
configinst = InnerJsonConfigWebUIParse()
118+
webui_config = configinst.dump()
119+
searchengine = webui_config.get("SearchEngine")
120+
top_k = searchengine.get("top_k", 3)
121+
122+
history = [History.from_data(h) for h in history]
123+
124+
async def search_engine_chat_iterator(query: str,
125+
search_engine_name: str,
126+
top_k: int,
127+
history: Optional[List[History]],
128+
stream: bool,
129+
model_name: str = "",
130+
temperature: float = 0.7,
131+
max_tokens: Optional[int] = None,
132+
prompt_name: str = prompt_name,
133+
) -> AsyncIterable[str]:
134+
nonlocal webui_config
135+
callback = AsyncIteratorCallbackHandler()
136+
if isinstance(max_tokens, int) and max_tokens <= 0:
137+
max_tokens = None
138+
provider = GetProviderByName(webui_config, model_name)
139+
140+
model = get_ChatOpenAI(
141+
provider=provider,
142+
model_name=model_name,
143+
temperature=temperature,
144+
max_tokens=max_tokens,
145+
callbacks=[callback],
146+
)
147+
148+
docs = await lookup_search_engine(query, search_engine_name, top_k)
149+
context = "\n".join([doc.page_content for doc in docs])
150+
151+
prompt_template = get_prompt_template("search_engine_chat", prompt_name)
152+
input_msg = History(role="user", content=prompt_template).to_msg_template(False)
153+
chat_prompt = ChatPromptTemplate.from_messages(
154+
[i.to_msg_template() for i in history] + [input_msg])
155+
156+
chain = LLMChain(prompt=chat_prompt, llm=model)
157+
158+
task = asyncio.create_task(wrap_done(
159+
chain.acall({"context": context, "question": query}),
160+
callback.done),
161+
)
162+
163+
source_documents = [
164+
f"""from [{inum + 1}] [{doc.metadata["source"]}]({doc.metadata["source"]}) \n\n{doc.page_content}\n\n"""
165+
for inum, doc in enumerate(docs)
166+
]
167+
168+
if len(source_documents) == 0:
169+
source_documents.append(f"""<span style='color:red'>No relevant information were found. This response is generated based on the LLM Model '{model_name}' itself!</span>""")
170+
171+
if stream:
172+
async for token in callback.aiter():
173+
# Use server-sent-events to stream the response
174+
yield json.dumps({"answer": token}, ensure_ascii=False)
175+
yield json.dumps({"docs": source_documents}, ensure_ascii=False)
176+
else:
177+
answer = ""
178+
async for token in callback.aiter():
179+
answer += token
180+
yield json.dumps({"answer": answer,
181+
"docs": source_documents},
182+
ensure_ascii=False)
183+
await task
184+
185+
return StreamingResponse(search_engine_chat_iterator(query=query,
186+
search_engine_name=search_engine_name,
187+
top_k=top_k,
188+
history=history,
189+
stream=stream,
190+
model_name=model_name,
191+
temperature=temperature,
192+
max_tokens=max_tokens,
193+
prompt_name=prompt_name),
194+
)

WebUI/Server/llm_api.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,69 @@ def save_chat_config(
818818
return BaseResponse(
819819
code=500,
820820
msg=f"failed to save chat configration, error: {e}")
821-
821+
822+
def save_search_engine_config(
823+
config: dict = Body(..., description="Search Engine configration information"),
824+
controller_address: str = Body(None, description="Fastchat controller address", examples=[fschat_controller_address()])
825+
) -> BaseResponse:
826+
try:
827+
with open("WebUI/configs/webuiconfig.json", 'r+') as file:
828+
jsondata = json.load(file)
829+
jsondata["SearchEngine"].update(config)
830+
file.seek(0)
831+
json.dump(jsondata, file, indent=4)
832+
file.truncate()
833+
return BaseResponse(
834+
code=200,
835+
msg=f"success save chat configration!")
836+
837+
except Exception as e:
838+
print(f'{e.__class__.__name__}: {e}')
839+
return BaseResponse(
840+
code=500,
841+
msg=f"failed to save chat configration, error: {e}")
842+
843+
def llm_search_engine_chat(
844+
query: str = Body(..., description="User input: ", examples=["chat"]),
845+
search_engine_name: str = Body(..., description="Search engine name"),
846+
history: List[dict] = Body([],
847+
description="History chat",
848+
examples=[[
849+
{"role": "user", "content": "Who are you?"},
850+
{"role": "assistant", "content": "I am AI."}]]
851+
),
852+
stream: bool = Body(False, description="stream output"),
853+
model_name: str = Body("", description="model name"),
854+
temperature: float = Body(0.7, description="LLM Temperature", ge=0.0, le=1.0),
855+
max_tokens: Optional[int] = Body(None, description="max tokens."),
856+
prompt_name: str = Body("default", description=""),
857+
controller_address: str = Body(None, description="Fastchat controller address", examples=[fschat_controller_address()])
858+
):
859+
controller_address = controller_address or fschat_controller_address()
860+
async def fake_json_streamer() -> AsyncIterable[str]:
861+
import asyncio
862+
with get_httpx_client() as client:
863+
response = client.stream(
864+
"POST",
865+
url=controller_address + "/llm_search_engine_chat",
866+
json={
867+
"query": query,
868+
"search_engine_name": search_engine_name,
869+
"history": history,
870+
"stream": stream,
871+
"model_name": model_name,
872+
"temperature": temperature,
873+
"max_tokens": max_tokens,
874+
"prompt_name": prompt_name,
875+
},
876+
)
877+
with response as r:
878+
for chunk in r.iter_text(None):
879+
if not chunk:
880+
continue
881+
yield chunk
882+
await asyncio.sleep(0.1)
883+
return StreamingResponse(fake_json_streamer(), media_type="text/event-stream")
822884

823885
def list_search_engines() -> BaseResponse:
824886
pass

WebUI/configs/basicconfig.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,4 +451,11 @@ def generate_prompt_for_imagegen(model_name : str = "", prompt : str = "", image
451451
new_prompt += f". Contents of this image is '{imagesprompt}'"
452452
print("new_prompt: ", new_prompt)
453453
return new_prompt
454-
return prompt
454+
return prompt
455+
456+
def generate_prompt_for_smart_search(prompt : str = ""):
457+
new_prompt = "You are an AI assistant, answering questions based on user inquiries. If you are absolutely certain of the answer to the question, please answer it to the best of your ability and refrain from returning the 'search_engine' command. If you don't know how to answer the question or you require real-time information or need to search the internet before answering questions, then please only return the command: 'search_engine'. \n\n User's question: " + prompt
458+
return new_prompt
459+
460+
def use_search_engine(text : str = ""):
461+
return "search_engine" in text

WebUI/configs/prompttemplates.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
"""
1212
You are a Python expert, please write code using Python. \n
1313
{{ input }}
14-
"""
15-
,
14+
""",
1615
},
1716

1817
"knowledge_base_chat": {
@@ -42,7 +41,16 @@
4241
},
4342

4443
"search_engine_chat": {
45-
"default": "{{ input }}",
44+
"default":
45+
'<Instruction> This is the internet information I found. Please extract and organize it to provide concise answers to the questions.'
46+
'If you cannot find an answer from it, please say, "Unable to find content that answers the question."</Instruction>\n'
47+
'<Known Information>{{ context }}</Known Information>\n'
48+
'<Question>{{ question }}</Question>\n',
49+
50+
"search":
51+
'<Instruction>Based on the known information, please provide concise and professional answers to the questions. If unable to find an answer from it, please say, "Unable to answer the question based on known information," and the response should be in the language of the query.</Instruction>\n'
52+
'<Known Information>{{ context }}</Known Information>\n'
53+
'<Question>{{ question }}</Question>\n',
4654
},
4755

4856
"agent_chat": {

0 commit comments

Comments
 (0)