This commit is contained in:
Wenjie Zhang 2024-10-02 20:11:28 +08:00
parent 4f3ba3ad5f
commit 02686db89f
18 changed files with 550 additions and 85 deletions

View File

@ -0,0 +1,143 @@
#!/usr/bin/env bash
# Licensed to the LF AI & Data foundation under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
run_embed() {
cat << EOF > embedEtcd.yaml
listen-client-urls: http://0.0.0.0:2379
advertise-client-urls: http://0.0.0.0:2379
quota-backend-bytes: 4294967296
auto-compaction-mode: revision
auto-compaction-retention: '1000'
EOF
cat << EOF > user.yaml
# Extra config to override default milvus.yaml
EOF
sudo docker run -d \
--name milvus-standalone \
--security-opt seccomp:unconfined \
-e ETCD_USE_EMBED=true \
-e ETCD_DATA_DIR=/var/lib/milvus/etcd \
-e ETCD_CONFIG_PATH=/milvus/configs/embedEtcd.yaml \
-e COMMON_STORAGETYPE=local \
-v $(pwd)/volumes/milvus:/var/lib/milvus \
-v $(pwd)/embedEtcd.yaml:/milvus/configs/embedEtcd.yaml \
-v $(pwd)/user.yaml:/milvus/configs/user.yaml \
-p 19530:19530 \
-p 9091:9091 \
-p 2379:2379 \
--health-cmd="curl -f http://localhost:9091/healthz" \
--health-interval=30s \
--health-start-period=90s \
--health-timeout=20s \
--health-retries=3 \
milvusdb/milvus:v2.4.5 \
milvus run standalone 1> /dev/null
}
wait_for_milvus_running() {
echo "Wait for Milvus Starting..."
while true
do
res=`sudo docker ps|grep milvus-standalone|grep healthy|wc -l`
if [ $res -eq 1 ]
then
echo "Start successfully."
echo "To change the default Milvus configuration, add your settings to the user.yaml file and then restart the service."
break
fi
sleep 1
done
}
start() {
res=`sudo docker ps|grep milvus-standalone|grep healthy|wc -l`
if [ $res -eq 1 ]
then
echo "Milvus is running."
exit 0
fi
res=`sudo docker ps -a|grep milvus-standalone|wc -l`
if [ $res -eq 1 ]
then
sudo docker start milvus-standalone 1> /dev/null
else
run_embed
fi
if [ $? -ne 0 ]
then
echo "Start failed."
exit 1
fi
wait_for_milvus_running
}
stop() {
sudo docker stop milvus-standalone 1> /dev/null
if [ $? -ne 0 ]
then
echo "Stop failed."
exit 1
fi
echo "Stop successfully."
}
delete() {
res=`sudo docker ps|grep milvus-standalone|wc -l`
if [ $res -eq 1 ]
then
echo "Please stop Milvus service before delete."
exit 1
fi
sudo docker rm milvus-standalone 1> /dev/null
if [ $? -ne 0 ]
then
echo "Delete failed."
exit 1
fi
sudo rm -rf $(pwd)/volumes
sudo rm -rf $(pwd)/embedEtcd.yaml
sudo rm -rf $(pwd)/user.yaml
echo "Delete successfully."
}
case $1 in
restart)
stop
start
;;
start)
start
;;
stop)
stop
;;
delete)
delete
;;
*)
echo "please use bash standalone_embed.sh restart|start|stop|delete"
;;
esac

View File

@ -64,6 +64,7 @@ class DataBaseManager:
def get_databases(self):
self._update_database()
assert self.config.enable_knowledge_base, "知识库未启用"
knowledge_base_collections = self.knowledge_base.get_collection_names()
if len(self.data["databases"]) != len(knowledge_base_collections):
logger.warning(f"Database number not match, {knowledge_base_collections}")

32
src/main.py Normal file
View File

@ -0,0 +1,32 @@
import uvicorn
from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from src.routers import router
from src.utils.logging_config import setup_logger
load_dotenv()
import os
os.environ["ZHIPUAI_API_KEY"] = "270ea71e9560c0ff406acbcdd48bfd97.e3XOMdWKuZb7Q1Sk"
app = FastAPI()
app.include_router(router)
# CORS 设置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
logger = setup_logger("server:main")
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=5000)

View File

@ -58,7 +58,7 @@ class DeepSeek(OpenAIBase):
class Zhipu(OpenAIBase):
def __init__(self, model_name=None):
model_name = model_name or "glm-4-flash"
api_key = os.getenv("ZHIPUAI_API_KEY")
api_key = os.getenv("ZHIPUAI_API_KEY", "270ea71e9560c0ff406acbcdd48bfd97.e3XOMdWKuZb7Q1Sk")
base_url = "https://open.bigmodel.cn/api/paas/v4/"
super().__init__(api_key=api_key, base_url=base_url, model_name=model_name)

11
src/routers/__init__.py Normal file
View File

@ -0,0 +1,11 @@
from fastapi import APIRouter
from src.routers.chat_router import chat
from src.routers.data_router import data
from src.routers.base_router import base
from src.routers.tool_router import tool
router = APIRouter()
router.include_router(base)
router.include_router(chat)
router.include_router(data)
router.include_router(tool)

View File

@ -0,0 +1,45 @@
from fastapi import APIRouter
base = APIRouter()
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from fastapi import Request
from src.core import HistoryManager
from src.utils.logging_config import setup_logger
from src.core.startup import startup
logger = setup_logger("server-base")
@base.get("/")
async def route_index():
return {"message": "You Got It!"}
@base.get("/config")
async def get_config():
return startup.config
@base.post("/config")
async def update_config(request: Request):
request_data = await request.json()
startup.config.update(request_data)
startup.config.save()
return startup.config
@base.post("/restart")
async def restart():
startup.restart()
return {"message": "Restarted!"}
@base.get("/log")
async def get_log():
from src.utils.logging_config import LOG_FILE
from collections import deque
with open(LOG_FILE, 'r') as f:
last_lines = deque(f, maxlen=1000)
log = ''.join(last_lines)
return {"log": log}

View File

@ -0,0 +1,55 @@
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import StreamingResponse
import json
from src.core import HistoryManager
from src.core.startup import startup
from src.utils.logging_config import setup_logger
chat = APIRouter(prefix="/chat")
logger = setup_logger("server-chat")
@chat.get("/")
async def chat_get():
return "Chat Get!"
@chat.post("/")
async def chat_post(request: Request):
request_data = await request.json()
query = request_data['query']
meta = request_data.get('meta')
history_manager = HistoryManager(request_data['history'])
new_query, refs = startup.retriever(query, history_manager.messages, meta)
messages = history_manager.get_history_with_msg(new_query, max_rounds=meta.get('history_round'))
history_manager.add_user(query)
logger.debug(f"Web history: {history_manager.messages}")
async def generate_response():
content = ""
for delta in startup.model.predict(messages, stream=True):
if not delta.content:
continue
if hasattr(delta, 'is_full') and delta.is_full:
content = delta.content
else:
content += delta.content
response_chunk = json.dumps({
"history": history_manager.update_ai(content),
"response": content,
"refs": refs # TODO: 优化 refs不需要每次都返回
}, ensure_ascii=False).encode('utf8') + b'\n'
yield response_chunk
return StreamingResponse(generate_response(), media_type='application/json')
@chat.post("/call")
async def call(request: Request):
request_data = await request.json()
query = request_data['query']
response = startup.model.predict(query)
logger.debug({"query": query, "response": response.content})
return {"response": response.content}

120
src/routers/data_router.py Normal file
View File

@ -0,0 +1,120 @@
import os
from typing import List, Optional
from fastapi import APIRouter, File, UploadFile, HTTPException, Depends, Body
from pydantic import BaseModel
from src.utils import setup_logger, hashstr
from src.core.startup import startup
data = APIRouter(prefix="/data")
logger = setup_logger("server-database")
@data.get("/")
async def get_databases():
try:
database = startup.dbm.get_databases()
except Exception as e:
return {"message": f"获取数据库列表失败 {e}", "databases": []}
return database
@data.post("/")
async def create_database(
database_name: str = Body(...),
description: str = Body(...),
db_type: str = Body(...),
dimension: int = Body(None)
):
logger.debug(f"Create database {database_name}")
database_info = startup.dbm.create_database(
database_name,
description,
db_type,
dimension=dimension
)
return database_info
@data.delete("/")
async def delete_database(db_id: str = Body(...)):
logger.debug(f"Delete database {db_id}")
startup.dbm.delete_database(db_id)
return {"message": "删除成功"}
@data.post("/query-test")
async def query_test(query: str = Body(...), meta: dict = Body(...)):
logger.debug(f"Query test in {meta}: {query}")
result = startup.retriever.query_knowledgebase(query, history=None, refs={"meta": meta})
return result
@data.post("/add-by-file")
async def create_document_by_file(db_id: str = Body(...), files: List[str] = Body(...)):
logger.debug(f"Add document in {db_id} by file: {files}")
msg = startup.dbm.add_files(db_id, files)
return msg
@data.get("/database-info")
async def get_database_info(db_id: str):
logger.debug(f"Get database {db_id} info")
database = startup.dbm.get_database_info(db_id)
if database is None:
raise HTTPException(status_code=404, detail="Database not found")
return database
@data.delete("/document")
async def delete_document(db_id: str = Body(...), file_id: str = Body(...)):
logger.debug(f"DELETE document {file_id} info in {db_id}")
startup.dbm.delete_file(db_id, file_id)
return {"message": "删除成功"}
@data.get("/document")
async def get_document_info(db_id: str, file_id: str):
logger.debug(f"GET document {file_id} info in {db_id}")
info = startup.dbm.get_file_info(db_id, file_id)
return info
@data.post("/upload")
async def upload_file(file: UploadFile = File(...)):
if not file.filename:
raise HTTPException(status_code=400, detail="No selected file")
upload_dir = os.path.join(startup.config.save_dir, "data/uploads")
os.makedirs(upload_dir, exist_ok=True)
filename = f"{hashstr(file.filename, 4, with_salt=True)}_{file.filename}".lower()
file_path = os.path.join(upload_dir, filename)
with open(file_path, "wb") as buffer:
buffer.write(await file.read())
return {"message": "File successfully uploaded", "file_path": file_path}
@data.get("/graph")
async def get_graph_info():
graph_info = startup.dbm.get_graph()
return graph_info
@data.get("/graph/node")
async def get_graph_node(entity_name: str):
logger.debug(f"Get graph node {entity_name}")
result = startup.dbm.graph_base.query_node(entity_name=entity_name)
return {"result": startup.retriever.format_query_results(result), "message": "success"}
@data.get("/graph/nodes")
async def get_graph_nodes(kgdb_name: str, num: int):
if not startup.config.enable_knowledge_graph:
raise HTTPException(status_code=400, detail="Knowledge graph is not enabled")
logger.debug(f"Get graph nodes in {kgdb_name} with {num} nodes")
result = startup.dbm.graph_base.get_sample_nodes(kgdb_name, num)
return {"result": startup.retriever.format_general_results(result), "message": "success"}
@data.post("/graph/add")
async def add_graph_entity(kgdb_name: str = Body(...), file_path: str = Body(...)):
if not startup.config.enable_knowledge_graph:
raise HTTPException(status_code=400, detail="Knowledge graph is not enabled")
if not file_path.endswith('.jsonl'):
raise HTTPException(status_code=400, detail="file_path must be a jsonl file")
startup.dbm.graph_base.jsonl_file_add_entity(file_path, kgdb_name)
return {"message": "Entity successfully added"}

View File

@ -0,0 +1,50 @@
import os
from fastapi import APIRouter, Body
from pydantic import BaseModel
from typing import List, Dict, Any, Optional
from src.utils import setup_logger
tool = APIRouter(prefix="/tool")
logger = setup_logger("server-tools")
class Tool(BaseModel):
name: str
title: str
description: str
url: str
method: str
@tool.get("/", response_model=List[Tool])
async def route_index():
tools = [
Tool(
name="text-chunking",
title="文本分块",
description="将文本分块以更好地理解。可以输入文本或者上传文件。",
url="/tools/text-chunking",
method="POST",
),
Tool(
name="pdf2txt",
title="PDF转文本",
description="将PDF文件转换为文本文件。",
url="/tools/pdf2txt",
method="POST",
)
]
return tools
@tool.post("/text-chunking")
async def text_chunking(text: str = Body(...), params: Dict[str, Any] = Body(...)):
from src.core.indexing import chunk
nodes = chunk(text, params=params)
return {"nodes": [node.to_dict() for node in nodes]}
@tool.post("/pdf2txt")
async def handle_pdf2txt(file: str = Body(...)):
from src.plugins import pdf2txt
text = pdf2txt(file, return_text=True)
return {"text": text}

View File

@ -351,7 +351,7 @@ const updateStatus = (id, status) => {
const simpleCall = (message) => {
return new Promise((resolve, reject) => {
fetch('/api/call', {
fetch('/api/chat/call', {
method: 'POST',
body: JSON.stringify({ query: message, }),
headers: { 'Content-Type': 'application/json' }
@ -363,7 +363,7 @@ const simpleCall = (message) => {
}
const loadDatabases = () => {
fetch('/api/database/', { method: "GET", })
fetch('/api/data/', { method: "GET", })
.then(response => response.json())
.then(data => {
console.log(data)
@ -371,63 +371,74 @@ const loadDatabases = () => {
})
}
const sendMessage = () => {
const user_input = conv.value.inputText.trim()
if (user_input) {
isStreaming.value = true
appendUserMessage(user_input)
appendAiMessage("", null)
const cur_res_id = conv.value.messages[conv.value.messages.length - 1].id
conv.value.inputText = ''
meta.db_name = opts.databases[meta.selectedKB]?.metaname
fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({
query: user_input,
history: conv.value.history,
meta: meta
}),
headers: {
'Content-Type': 'application/json'
}
}).then((response) => {const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
//
const readChunk = () => {
return reader.read().then(({ done, value }) => {
if (done) {
console.log(conv.value)
console.log('Finished')
updateStatus(cur_res_id, "finished")
isStreaming.value = false
if (conv.value.messages.length === 2) { renameTitle() }
return
}
// fetch
const fetchChatResponse = (user_input, cur_res_id) => {
fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({
query: user_input,
history: conv.value.history,
meta: meta
}),
headers: {
'Content-Type': 'application/json'
}
}).then((response) => {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
buffer += decoder.decode(value, { stream: true })
const message = buffer.trim().split('\n').pop()
//
const readChunk = () => {
return reader.read().then(({ done, value }) => {
if (done) {
//
updateStatus(cur_res_id, "finished");
isStreaming.value = false;
if (conv.value.messages.length === 2) { renameTitle(); }
return; //
}
buffer += decoder.decode(value, { stream: true });
const messages = buffer.trim().split('\n');
messages.forEach((message) => {
try {
const data = JSON.parse(message)
updateMessage(data.response, cur_res_id, data.refs, "loading")
conv.value.history = data.history
buffer = ''
const data = JSON.parse(message);
updateMessage(data.response, cur_res_id, data.refs, "loading");
conv.value.history = data.history;
} catch (e) {
// console.log(e)
console.error('JSON 解析错误:', e);
}
return readChunk()
})
}
return readChunk()
})
.catch((error) => {
console.error(error)
updateStatus(cur_res_id, "error")
isStreaming.value = false
})
});
buffer = ''; //
return readChunk(); //
});
};
return readChunk();
})
.catch((error) => {
console.error(error);
updateStatus(cur_res_id, "error");
isStreaming.value = false;
});
}
// sendMessage
const sendMessage = () => {
const user_input = conv.value.inputText.trim();
const dbName = opts.databases.length > 0 ? opts.databases[meta.selectedKB]?.metaname : null;
if (user_input) {
isStreaming.value = true;
appendUserMessage(user_input);
appendAiMessage("", null);
const cur_res_id = conv.value.messages[conv.value.messages.length - 1].id;
conv.value.inputText = '';
meta.db_name = dbName;
fetchChatResponse(user_input, cur_res_id)
} else {
console.log('请输入消息')
console.log('请输入消息');
}
}
@ -436,11 +447,6 @@ const autoSend = (message) => {
sendMessage()
}
// const clearChat = () => {
// conv.value.messages = []
// conv.value.history = []
// }
//
onMounted(() => {
scrollToBottom()

View File

@ -15,7 +15,7 @@
name="file"
:max-count="1"
:disabled="state.uploading"
action="/api/database/upload"
action="/api/data/upload"
@change="handleFileUpload"
@drop="handleDrop"
>
@ -92,7 +92,7 @@ const convertPdfToText = async () => {
try {
state.loading = true;
const response = await fetch('/api/tools/pdf2txt', {
const response = await fetch('/api/tool/pdf2txt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file: file })

View File

@ -40,7 +40,7 @@
name="file"
:max-count="1"
:disabled="state.uploading"
action="/api/database/upload"
action="/api/data/upload"
@change="handleFileUpload"
@drop="handleDrop"
>
@ -125,14 +125,16 @@ const chunkText = async () => {
try {
state.loading = true
const response = await fetch('/api/tools/text_chunking', {
const response = await fetch('/api/tool/text-chunking', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: text_or_file,
chunk_size: params.chunkSize,
chunk_overlap: params.chunkOverlap,
use_parser: params.useParser
params: {
chunk_size: params.chunkSize,
chunk_overlap: params.chunkOverlap,
use_parser: params.useParser
}
})
});

View File

@ -43,7 +43,7 @@ const getRemoteDatabase = () => {
if (!configStore.config.enable_knowledge_base) {
return
}
fetch('/api/database').then(res => res.json()).then(data => {
fetch('/api/data').then(res => res.json()).then(data => {
console.log("database", data)
databaseStore.setDatabase(data.databases)
})

View File

@ -87,7 +87,7 @@ const router = createRouter({
meta: { keepAlive: true }
},
{
path: 'text_chunking',
path: 'text-chunking',
name: 'TextChunking',
component: () => import('../components/TextChunkingComponent.vue'),
},

View File

@ -32,7 +32,7 @@
name="file"
:multiple="true"
:disabled="state.loading"
action="/api/database/upload"
action="/api/data/upload"
@change="handleFileUpload"
@drop="handleDrop"
>
@ -300,7 +300,7 @@ const onQuery = () => {
return
}
meta.db_name = database.value.metaname
fetch('/api/database/query-test', {
fetch('/api/data/query-test', {
method: "POST",
body: JSON.stringify({
query: queryText.value.trim(),
@ -359,7 +359,7 @@ const deleteDatabse = () => {
cancelText: '取消',
onOk: () => {
state.lock = true
fetch('/api/database/', {
fetch('/api/data/', {
method: "DELETE",
body: JSON.stringify({
db_id: databaseId.value
@ -387,7 +387,7 @@ const deleteDatabse = () => {
const openFileDetail = (record) => {
state.lock = true
fetch(`/api/database/document?db_id=${databaseId.value}&file_id=${record.file_id}`, {
fetch(`/api/data/document?db_id=${databaseId.value}&file_id=${record.file_id}`, {
method: "GET",
})
.then(response => response.json())
@ -427,7 +427,7 @@ const getDatabaseInfo = () => {
const db_id = databaseId.value
state.lock = true
return new Promise((resolve, reject) => {
fetch(`/api/database/info?db_id=${db_id}`, {
fetch(`/api/data/info?db_id=${db_id}`, {
method: "GET",
})
.then(response => response.json())
@ -449,7 +449,7 @@ const getDatabaseInfo = () => {
const deleteFile = (fileId) => {
console.log(fileId)
state.lock = true
fetch('/api/database/document', {
fetch('/api/data/document', {
method: "DELETE",
body: JSON.stringify({
db_id: databaseId.value,
@ -478,7 +478,7 @@ const addDocumentByFile = () => {
state.refreshInterval = setInterval(() => {
getDatabaseInfo();
}, 1000);
fetch('/api/database/add_by_file', {
fetch('/api/data/add_by_file', {
method: "POST",
body: JSON.stringify({
db_id: databaseId.value,

View File

@ -111,7 +111,7 @@ const newDatabase = reactive({
const loadDatabases = () => {
// loadGraph()
fetch('/api/database/', {
fetch('/api/data/', {
method: "GET",
})
.then(response => response.json())
@ -130,7 +130,7 @@ const createDatabase = () => {
newDatabase.loading = false
return
}
fetch('/api/database/', {
fetch('/api/data/', {
method: "POST",
body: JSON.stringify({
database_name: newDatabase.name,
@ -163,7 +163,7 @@ const navigateToGraph = () => {
// const loadGraph = () => {
// graphloading.value = true
// fetch('/api/database/graph', {
// fetch('/api/data/graph', {
// method: "GET",
// })
// .then(response => response.json())

View File

@ -59,7 +59,7 @@
:fileList="fileList"
:max-count="1"
:disabled="state.precessing"
action="/api/database/upload"
action="/api/data/upload"
@change="handleFileUpload"
@drop="handleDrop"
>
@ -106,7 +106,7 @@ const state = reactive({
const loadGraphInfo = () => {
state.loadingGraphInfo = true
fetch('/api/database/graph', {
fetch('/api/data/graph', {
method: "GET",
})
.then(response => response.json())
@ -147,7 +147,7 @@ const getGraphData = () => {
const addDocumentByFile = () => {
state.precessing = true
const files = fileList.value.filter(file => file.status === 'done').map(file => file.response.file_path)
fetch('/api/database/graph/add', {
fetch('/api/data/graph/add', {
method: 'POST',
body: JSON.stringify({
file_path: files[0]
@ -166,7 +166,7 @@ const addDocumentByFile = () => {
const loadSampleNodes = () => {
state.fetching = true
fetch(`/api/database/graph/nodes?kgdb_name=neo4j&num=${sampleNodeCount.value}`)
fetch(`/api/data/graph/nodes?kgdb_name=neo4j&num=${sampleNodeCount.value}`)
.then((res) => {
if (res.ok) {
return res.json();
@ -199,7 +199,7 @@ const onSearch = () => {
}
state.searchLoading = true
fetch(`/api/database/graph/node?entity_name=${state.searchInput}`)
fetch(`/api/data/graph/node?entity_name=${state.searchInput}`)
.then((res) => {
if (!res.ok) {
return res.json().then(errorData => {

View File

@ -31,7 +31,7 @@ import HeaderComponent from '@/components/HeaderComponent.vue';
const router = useRouter();
const tools = ref([]);
const iconMap = ref({
"text_chunking": FileSearchOutlined
"text-chunking": FileSearchOutlined
})
const state = reactive({
@ -40,7 +40,7 @@ const state = reactive({
const getTools = () => {
state.loadingTools = true
fetch('/api/tools/')
fetch('/api/tool/')
.then(response => response.json())
.then(data => {
tools.value = data;