mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 09:17:06 +08:00
perf: Chat and model
This commit is contained in:
@@ -3,6 +3,7 @@ from fastapi import APIRouter
|
||||
from apps.system.api import login, user, aimodel
|
||||
from apps.settings.api import terminology
|
||||
from apps.datasource.api import datasource
|
||||
from apps.chat.api import chat
|
||||
|
||||
|
||||
api_router = APIRouter()
|
||||
@@ -11,5 +12,6 @@ api_router.include_router(user.router)
|
||||
api_router.include_router(aimodel.router)
|
||||
api_router.include_router(terminology.router)
|
||||
api_router.include_router(datasource.router)
|
||||
api_router.include_router(chat.router)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from sqlmodel import select
|
||||
from apps.chat.schemas.chat_base_schema import LLMConfig
|
||||
from apps.chat.schemas.chat_schema import ChatQuestion
|
||||
from apps.chat.schemas.llm import LLMService
|
||||
from apps.system.models.system_modle import AiModelDetail
|
||||
from common.core.deps import SessionDep
|
||||
# from sse_starlette.sse import EventSourceResponse
|
||||
router = APIRouter(tags=["Data Q&A"], prefix="/chat")
|
||||
|
||||
|
||||
@router.post("/question")
|
||||
async def stream_sql(session: SessionDep, requestQuestion: ChatQuestion):
|
||||
question = requestQuestion.question
|
||||
|
||||
# Use OpenAI model
|
||||
""" openai_config = LLMConfig(
|
||||
model_type="openai",
|
||||
model_name="gpt-4",
|
||||
api_key="your-api-key",
|
||||
additional_params={"temperature": 0.7}
|
||||
)
|
||||
openai_service = LLMService(openai_config) """
|
||||
|
||||
aimodel = session.exec(select(AiModelDetail).where(AiModelDetail.status == True, AiModelDetail.api_key.is_not(None))).first()
|
||||
|
||||
if not aimodel:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No available AI model configuration found"
|
||||
)
|
||||
|
||||
# Use Tongyi Qianwen
|
||||
tongyi_config = LLMConfig(
|
||||
model_type="tongyi",
|
||||
model_name=aimodel.name,
|
||||
api_key=aimodel.api_key,
|
||||
additional_params={"temperature": aimodel.temperature}
|
||||
)
|
||||
llm_service = LLMService(tongyi_config)
|
||||
|
||||
# Use Custom VLLM model
|
||||
""" vllm_config = LLMConfig(
|
||||
model_type="vllm",
|
||||
model_name="your-model-path",
|
||||
additional_params={
|
||||
"max_new_tokens": 200,
|
||||
"temperature": 0.3
|
||||
}
|
||||
)
|
||||
vllm_service = LLMService(vllm_config) """
|
||||
result = llm_service.generate_sql(question)
|
||||
return result
|
||||
@@ -0,0 +1,86 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, Dict, Any, Type
|
||||
from abc import ABC, abstractmethod
|
||||
from langchain_core.language_models import BaseLLM as LangchainBaseLLM
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_community.llms import Tongyi, VLLM
|
||||
|
||||
class LLMConfig(BaseModel):
|
||||
"""Base configuration class for large language models"""
|
||||
model_type: str # Model type: openai/tongyi/vllm etc.
|
||||
model_name: str # Specific model name
|
||||
api_key: Optional[str] = None
|
||||
api_base_url: Optional[str] = None
|
||||
additional_params: Dict[str, Any] = {}
|
||||
|
||||
|
||||
class BaseLLM(ABC):
|
||||
"""Abstract base class for large language models"""
|
||||
|
||||
def __init__(self, config: LLMConfig):
|
||||
self.config = config
|
||||
self._llm = self._init_llm()
|
||||
|
||||
@abstractmethod
|
||||
def _init_llm(self) -> LangchainBaseLLM:
|
||||
"""Initialize specific large language model instance"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def llm(self) -> LangchainBaseLLM:
|
||||
"""Return the langchain LLM instance"""
|
||||
return self._llm
|
||||
|
||||
class OpenAILLM(BaseLLM):
|
||||
def _init_llm(self) -> LangchainBaseLLM:
|
||||
return ChatOpenAI(
|
||||
model=self.config.model_name,
|
||||
api_key=self.config.api_key,
|
||||
**self.config.additional_params
|
||||
)
|
||||
|
||||
def generate(self, prompt: str) -> str:
|
||||
return self.llm.invoke(prompt)
|
||||
|
||||
class TongyiLLM(BaseLLM):
|
||||
def _init_llm(self) -> LangchainBaseLLM:
|
||||
return Tongyi(
|
||||
model_name=self.config.model_name,
|
||||
dashscope_api_key=self.config.api_key,
|
||||
**self.config.additional_params
|
||||
)
|
||||
|
||||
def generate(self, prompt: str) -> str:
|
||||
return self.llm.invoke(prompt)
|
||||
|
||||
class VLLMLLM(BaseLLM):
|
||||
def _init_llm(self) -> LangchainBaseLLM:
|
||||
return VLLM(
|
||||
model=self.config.model_name,
|
||||
**self.config.additional_params
|
||||
)
|
||||
|
||||
def generate(self, prompt: str) -> str:
|
||||
return self.llm.invoke(prompt)
|
||||
|
||||
|
||||
class LLMFactory:
|
||||
"""Large Language Model Factory Class"""
|
||||
|
||||
_llm_types: Dict[str, Type[BaseLLM]] = {
|
||||
"openai": OpenAILLM,
|
||||
"tongyi": TongyiLLM,
|
||||
"vllm": VLLMLLM
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_llm(cls, config: LLMConfig) -> BaseLLM:
|
||||
llm_class = cls._llm_types.get(config.model_type)
|
||||
if not llm_class:
|
||||
raise ValueError(f"Unsupported LLM type: {config.model_type}")
|
||||
return llm_class(config)
|
||||
|
||||
@classmethod
|
||||
def register_llm(cls, model_type: str, llm_class: Type[BaseLLM]):
|
||||
"""Register new model type"""
|
||||
cls._llm_types[model_type] = llm_class
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ChatQuestion(BaseModel):
|
||||
question: str
|
||||
@@ -0,0 +1,33 @@
|
||||
from langchain_community.utilities import SQLDatabase
|
||||
# from langchain_community.agent_toolkits import create_sql_agent
|
||||
from langchain_community.llms import Tongyi
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from apps.chat.schemas.chat_base_schema import LLMConfig, LLMFactory
|
||||
from common.core.config import settings
|
||||
import warnings
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
class LLMService:
|
||||
def __init__(self, config: LLMConfig):
|
||||
# Initialize database connection
|
||||
self.db = SQLDatabase.from_uri(str(settings.SQLALCHEMY_DATABASE_URI))
|
||||
|
||||
# Create LLM instance through factory
|
||||
llm_instance = LLMFactory.create_llm(config)
|
||||
self.llm = llm_instance.llm
|
||||
|
||||
# Define prompt template
|
||||
self.prompt = ChatPromptTemplate.from_messages([
|
||||
("system", """You are a professional SQL engineer. Generate PostgreSQL SELECT queries based on the database schema and user questions.
|
||||
Data modification or deletion is prohibited. Table structure is as follows:
|
||||
{schema}
|
||||
"""),
|
||||
("human", "{question}")
|
||||
])
|
||||
|
||||
def generate_sql(self, question: str) -> str:
|
||||
chain = self.prompt | self.llm
|
||||
schema = self.db.get_table_info()
|
||||
return chain.invoke({"schema": schema, "question": question})
|
||||
|
||||
@@ -49,7 +49,14 @@ class ResponseMiddleware(BaseHTTPMiddleware):
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Response processing error: {str(e)}", exc_info=True)
|
||||
return response
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"code": 500,
|
||||
"data": None,
|
||||
"msg": str(e)
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
+6
-2
@@ -3,13 +3,14 @@ from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import os
|
||||
import sentry_sdk
|
||||
from fastapi import FastAPI, Path
|
||||
from fastapi import FastAPI, Path, HTTPException
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from apps.api import api_router
|
||||
from apps.system.middleware.auth import TokenMiddleware
|
||||
from common.core.config import settings
|
||||
from common.core.response_middleware import ResponseMiddleware
|
||||
from common.core.response_middleware import ResponseMiddleware, exception_handler
|
||||
|
||||
def custom_generate_unique_id(route: APIRoute) -> str:
|
||||
tag = route.tags[0] if route.tags and len(route.tags) > 0 else ""
|
||||
@@ -39,6 +40,9 @@ app.add_middleware(TokenMiddleware)
|
||||
app.add_middleware(ResponseMiddleware)
|
||||
app.include_router(api_router, prefix=settings.API_V1_STR)
|
||||
|
||||
# Register exception handlers
|
||||
app.add_exception_handler(StarletteHTTPException, exception_handler.http_exception_handler)
|
||||
app.add_exception_handler(Exception, exception_handler.global_exception_handler)
|
||||
|
||||
frontend_dist = os.path.abspath("../frontend/dist")
|
||||
if not os.path.exists(frontend_dist):
|
||||
|
||||
@@ -19,6 +19,13 @@ dependencies = [
|
||||
"sentry-sdk[fastapi]<2.0.0,>=1.40.6",
|
||||
"pyjwt<3.0.0,>=2.8.0",
|
||||
"pycryptodome (>=3.22.0,<4.0.0)",
|
||||
"langchain>=0.1.0,<0.2.0",
|
||||
"langchain-core>=0.1.10,<0.2.0",
|
||||
"langchain-openai>=0.1.0,<0.2.0",
|
||||
"langchain-community>=0.0.19,<0.1.0",
|
||||
"vllm>=0.8.5",
|
||||
"dashscope>=1.14.0,<2.0.0",
|
||||
"sse-starlette>=1.8.0,<2.0.0"
|
||||
"pymysql (>=1.1.1,<2.0.0)",
|
||||
]
|
||||
[[tool.uv.index]]
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
export const questionApi = {
|
||||
pager: (pageNumber: number, pageSize: number) => request.get(`/chat/question/pager/${pageNumber}/${pageSize}`),
|
||||
// add: (data: any, progress: any) => request.post('/chat/question', data, { responseType: 'stream', onDownloadProgress: progress }),
|
||||
add: (data: any) => request.post('/chat/question', data),
|
||||
edit: (data: any) => request.put('/chat/question', data),
|
||||
delete: (id: number) => request.delete(`/chat/question/${id}`),
|
||||
query: (id: number) => request.get(`/chat/question/${id}`)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
export interface SelectOption {
|
||||
label: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
export const modelTypeOptions: SelectOption[] = [
|
||||
{ label: 'OpenAI', value: 0 },
|
||||
{ label: 'Anthropic', value: 1 },
|
||||
{ label: 'Baidu', value: 2 },
|
||||
{ label: 'iFLYTEK', value: 3 },
|
||||
{ label: 'Zhipu AI', value: 4 },
|
||||
{ label: 'MiniMax', value: 5 },
|
||||
{ label: 'Tencent', value: 6 },
|
||||
{ label: 'Other', value: 7 },
|
||||
]
|
||||
|
||||
export const getModelTypeName = (value: any) => {
|
||||
const tv = parseInt(value)
|
||||
const item = modelTypeOptions.find(item => item.value === tv)
|
||||
return item?.label || ''
|
||||
}
|
||||
@@ -49,7 +49,7 @@ class HttpService {
|
||||
this.cancelTokenSource = axios.CancelToken.source()
|
||||
this.instance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 15000,
|
||||
timeout: 50000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...config?.headers
|
||||
@@ -155,6 +155,10 @@ class HttpService {
|
||||
default:
|
||||
errorMessage = `Server responded with error: ${error.response.status}`
|
||||
}
|
||||
if (error?.response?.data) {
|
||||
const msgData: any = error.response.data
|
||||
msgData.msg && (errorMessage = msgData.msg)
|
||||
}
|
||||
} else if (error.request) {
|
||||
errorMessage = 'No response from server'
|
||||
} else if (axios.isCancel(error)) {
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
class="message"
|
||||
:class="msg.role"
|
||||
>
|
||||
<div class="message-content">{{ msg.content }}</div>
|
||||
<!-- <div class="message-content">{{ msg.content }}</div> -->
|
||||
<div class="message-content" v-html="formatMessage(msg.content)"></div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="empty-state">
|
||||
@@ -87,8 +88,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import { ref, computed, nextTick, watch } from 'vue'
|
||||
import { Plus, Search, More, Position } from '@element-plus/icons-vue'
|
||||
import { questionApi } from '@/api/chat'
|
||||
|
||||
|
||||
interface ChatMessage {
|
||||
role: 'user' | 'assistant'
|
||||
@@ -105,6 +108,7 @@ const chatHistory = ref<Chat[]>([])
|
||||
const currentChatId = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const inputMessage = ref('')
|
||||
const messagesRef = ref<HTMLDivElement>()
|
||||
|
||||
const filteredHistory = computed(() => {
|
||||
return chatHistory.value.filter(chat =>
|
||||
@@ -130,19 +134,72 @@ const switchChat = (chatId: string) => {
|
||||
currentChatId.value = chatId
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
if (messagesRef.value) {
|
||||
const scrollHeight = messagesRef.value.scrollHeight
|
||||
const animateScroll = () => {
|
||||
const currentScroll = messagesRef.value!.scrollTop
|
||||
const distance = scrollHeight - currentScroll
|
||||
if (distance > 0) {
|
||||
window.requestAnimationFrame(animateScroll)
|
||||
messagesRef.value!.scrollTop = currentScroll + distance / 8
|
||||
}
|
||||
}
|
||||
animateScroll()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const sendMessage = () => {
|
||||
if (!inputMessage.value.trim()) return
|
||||
|
||||
const chat = currentChat.value
|
||||
if (!chat) return
|
||||
let chat = currentChat.value
|
||||
// if (!chat) return
|
||||
if (!chat) {
|
||||
chat = {
|
||||
id: null,
|
||||
title: 'test',
|
||||
messages: []
|
||||
} as unknown as Chat
|
||||
}
|
||||
|
||||
chat.messages.push({
|
||||
role: 'user',
|
||||
content: inputMessage.value
|
||||
})
|
||||
|
||||
|
||||
const param = {
|
||||
question: inputMessage.value
|
||||
}
|
||||
inputMessage.value = ''
|
||||
chatHistory.value.unshift(chat)
|
||||
currentChatId.value = chat.id
|
||||
|
||||
|
||||
// const index = chat.messages.length - 1
|
||||
questionApi.add(param).then((result: string) => {
|
||||
// chat.messages[index]['content'] += result
|
||||
chat.messages.push({
|
||||
role: 'assistant',
|
||||
content: result
|
||||
})
|
||||
scrollToBottom()
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => currentChat.value?.messages, () => {
|
||||
scrollToBottom()
|
||||
}, { deep: true })
|
||||
|
||||
const formatMessage = (content: string) => {
|
||||
if (!content) return ''
|
||||
return content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/ {2}/g, ' ')
|
||||
}
|
||||
|
||||
const handleCommand = (command: string, chatId: string) => {
|
||||
@@ -253,6 +310,28 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: #999;
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&:not(:hover)::-webkit-scrollbar-thumb {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 24px;
|
||||
@@ -266,6 +345,7 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
|
||||
|
||||
&.assistant .message-content {
|
||||
background-color: #f5f5f5;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
|
||||
@@ -32,7 +32,11 @@
|
||||
<el-table :data="state.tableData" style="width: 100%" row-key="id" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="name" label="Model Name" width="280" />
|
||||
<el-table-column prop="type" label="Model Type" width="280" />
|
||||
<el-table-column prop="type" label="Model Type" width="280">
|
||||
<template #default="scope">
|
||||
<span>{{ getModelTypeName(scope.row.type) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="api_key" label="API KEY" width="280">
|
||||
<template #default="scope">
|
||||
<div class="user-status-container" :class="[ scope.row.api_key ? 'active' : 'disabled' ]">
|
||||
@@ -100,9 +104,10 @@
|
||||
|
||||
<el-form-item label="Model Type">
|
||||
<el-select v-model="state.form.type" placeholder="Please select a type">
|
||||
<el-option label="domain1" value="0" />
|
||||
<el-option v-for="item in modelTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
<!-- <el-option label="domain1" value="0" />
|
||||
<el-option label="domain2" value="1" />
|
||||
<el-option label="domain3" value="2" />
|
||||
<el-option label="domain3" value="2" /> -->
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
@@ -152,7 +157,7 @@ import IconOpeEdit from '@/assets/svg/operate/ope-edit.svg';
|
||||
import IconOpeDelete from '@/assets/svg/operate/ope-delete.svg';
|
||||
import { modelApi } from '@/api/system'
|
||||
import { formatTimestamp } from '@/utils/date'
|
||||
|
||||
import { modelTypeOptions, getModelTypeName } from '@/entity/CommonEntity.ts'
|
||||
const keyword = ref('')
|
||||
const dialogFormVisible = ref(false)
|
||||
const termFormRef = ref()
|
||||
|
||||
Reference in New Issue
Block a user