diff --git a/backend/apps/api.py b/backend/apps/api.py index 478d86b3..e11cd90b 100644 --- a/backend/apps/api.py +++ b/backend/apps/api.py @@ -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) diff --git a/backend/apps/chat/__init__.py b/backend/apps/chat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/chat/api/__init__.py b/backend/apps/chat/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/chat/api/chat.py b/backend/apps/chat/api/chat.py new file mode 100644 index 00000000..3a6d723c --- /dev/null +++ b/backend/apps/chat/api/chat.py @@ -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 \ No newline at end of file diff --git a/backend/apps/chat/schemas/__init__.py b/backend/apps/chat/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/chat/schemas/chat_base_schema.py b/backend/apps/chat/schemas/chat_base_schema.py new file mode 100644 index 00000000..b5cb540e --- /dev/null +++ b/backend/apps/chat/schemas/chat_base_schema.py @@ -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 \ No newline at end of file diff --git a/backend/apps/chat/schemas/chat_schema.py b/backend/apps/chat/schemas/chat_schema.py new file mode 100644 index 00000000..607e5f54 --- /dev/null +++ b/backend/apps/chat/schemas/chat_schema.py @@ -0,0 +1,6 @@ + +from pydantic import BaseModel + + +class ChatQuestion(BaseModel): + question: str \ No newline at end of file diff --git a/backend/apps/chat/schemas/llm.py b/backend/apps/chat/schemas/llm.py new file mode 100644 index 00000000..fff9a2a2 --- /dev/null +++ b/backend/apps/chat/schemas/llm.py @@ -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}) + diff --git a/backend/common/core/response_middleware.py b/backend/common/core/response_middleware.py index 85c1cd85..f0979411 100644 --- a/backend/common/core/response_middleware.py +++ b/backend/common/core/response_middleware.py @@ -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 diff --git a/backend/main.py b/backend/main.py index f062e0f2..f8858542 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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): diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 41ccb44e..4ac10e32 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -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]] diff --git a/frontend/src/api/chat.ts b/frontend/src/api/chat.ts new file mode 100644 index 00000000..c96d9d64 --- /dev/null +++ b/frontend/src/api/chat.ts @@ -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}`) +} \ No newline at end of file diff --git a/frontend/src/entity/CommonEntity.ts b/frontend/src/entity/CommonEntity.ts new file mode 100644 index 00000000..d926132d --- /dev/null +++ b/frontend/src/entity/CommonEntity.ts @@ -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 || '' +} \ No newline at end of file diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 4e20c920..c314c492 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -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)) { diff --git a/frontend/src/views/chat/index.vue b/frontend/src/views/chat/index.vue index db827254..b97139c9 100644 --- a/frontend/src/views/chat/index.vue +++ b/frontend/src/views/chat/index.vue @@ -50,7 +50,8 @@ class="message" :class="msg.role" > -
+ +