mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"""016_modify_chat
|
||||
|
||||
Revision ID: 031148da1d81
|
||||
Revises: 02d84523a979
|
||||
Create Date: 2025-06-26 17:00:07.054531
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel.sql.sqltypes
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '031148da1d81'
|
||||
down_revision = '02d84523a979'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('chat', 'datasource',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=True)
|
||||
op.add_column('chat_record', sa.Column('ai_modal_id', sa.Integer(), nullable=True))
|
||||
op.add_column('chat_record', sa.Column('first_chat', sa.Boolean(), nullable=True))
|
||||
op.add_column('chat_record', sa.Column('recommended_question_answer', sa.Text(), nullable=True))
|
||||
op.add_column('chat_record', sa.Column('recommended_question', sa.Text(), nullable=True))
|
||||
op.add_column('chat_record', sa.Column('datasource_select_answer', sa.Text(), nullable=True))
|
||||
op.add_column('chat_record', sa.Column('token_sql', sa.Integer(), nullable=True))
|
||||
op.add_column('chat_record', sa.Column('token_chart', sa.Integer(), nullable=True))
|
||||
op.add_column('chat_record', sa.Column('token_analysis', sa.Integer(), nullable=True))
|
||||
op.add_column('chat_record', sa.Column('token_predict', sa.Integer(), nullable=True))
|
||||
op.add_column('chat_record', sa.Column('full_recommended_question_message', sa.Text(), nullable=True))
|
||||
op.add_column('chat_record', sa.Column('token_recommended_question', sa.Integer(), nullable=True))
|
||||
op.add_column('chat_record', sa.Column('full_select_datasource_message', sa.Text(), nullable=True))
|
||||
op.add_column('chat_record', sa.Column('token_select_datasource_question', sa.Integer(), nullable=True))
|
||||
op.alter_column('chat_record', 'chat_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=False)
|
||||
op.alter_column('chat_record', 'datasource',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('chat_record', 'datasource',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=False)
|
||||
op.alter_column('chat_record', 'chat_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=True)
|
||||
op.drop_column('chat_record', 'token_select_datasource_question')
|
||||
op.drop_column('chat_record', 'full_select_datasource_message')
|
||||
op.drop_column('chat_record', 'token_recommended_question')
|
||||
op.drop_column('chat_record', 'full_recommended_question_message')
|
||||
op.drop_column('chat_record', 'token_predict')
|
||||
op.drop_column('chat_record', 'token_analysis')
|
||||
op.drop_column('chat_record', 'token_chart')
|
||||
op.drop_column('chat_record', 'token_sql')
|
||||
op.drop_column('chat_record', 'datasource_select_answer')
|
||||
op.drop_column('chat_record', 'recommended_question')
|
||||
op.drop_column('chat_record', 'recommended_question_answer')
|
||||
op.drop_column('chat_record', 'first_chat')
|
||||
op.drop_column('chat_record', 'ai_modal_id')
|
||||
op.alter_column('chat', 'datasource',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,35 @@
|
||||
"""017_rsa_ddl
|
||||
|
||||
Revision ID: a0ba8268868d
|
||||
Revises: 031148da1d81
|
||||
Create Date: 2025-06-27 15:05:38.676825
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel.sql.sqltypes
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a0ba8268868d'
|
||||
down_revision = '031148da1d81'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'rsa',
|
||||
sa.Column('id', sa.BigInteger(), primary_key=True, nullable=False),
|
||||
sa.Column('private_key', sa.Text(), default="", nullable=False),
|
||||
sa.Column('public_key', sa.Text(), default="", nullable=False),
|
||||
sa.Column('salt', sa.Text(), default="", nullable=False),
|
||||
sa.Column('create_time', sa.BigInteger(), default=0, nullable=False),
|
||||
sa.Column('update_time', sa.BigInteger(), default=0, nullable=False)
|
||||
)
|
||||
op.create_index(op.f('ix_rsa_id'), 'rsa', ['id'], unique=False)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('rsa')
|
||||
op.drop_index(op.f('ix_rsa_id'), table_name='rsa')
|
||||
@@ -8,7 +8,7 @@ from sqlmodel import select
|
||||
|
||||
from apps.chat.curd.chat import list_chats, get_chat_with_records, create_chat, rename_chat, \
|
||||
delete_chat, list_records
|
||||
from apps.chat.models.chat_model import CreateChat, ChatRecord, RenameChat, Chat, ChatQuestion
|
||||
from apps.chat.models.chat_model import CreateChat, ChatRecord, RenameChat, Chat, ChatQuestion, ChatMcp
|
||||
from apps.chat.task.llm import LLMService
|
||||
from apps.datasource.crud.datasource import get_table_schema
|
||||
from apps.datasource.models.datasource import CoreDatasource
|
||||
@@ -57,6 +57,18 @@ async def delete(session: SessionDep, chart_id: int):
|
||||
)
|
||||
|
||||
|
||||
@router.post("/mcp_start", operation_id="mcp_start")
|
||||
async def mcp_start(session: SessionDep, chat: ChatMcp):
|
||||
user = await get_current_user(session, chat.token)
|
||||
return create_chat(session, user, CreateChat(), False)
|
||||
|
||||
|
||||
@router.post("/mcp_question", operation_id="mcp_question")
|
||||
async def mcp_question(session: SessionDep, chat: ChatMcp):
|
||||
user = await get_current_user(session, chat.token)
|
||||
return await stream_sql(session, user, chat)
|
||||
|
||||
|
||||
@router.post("/start")
|
||||
async def start_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj: CreateChat):
|
||||
try:
|
||||
@@ -68,25 +80,6 @@ async def start_chat(session: SessionDep, current_user: CurrentUser, create_chat
|
||||
)
|
||||
|
||||
|
||||
@router.post("/mcp_question", operation_id="mcp_question")
|
||||
async def mcp_question(session: SessionDep, token: str, request_question: ChatQuestion):
|
||||
user = await get_current_user(session, token)
|
||||
# return await stream_sql(session, user, request_question)
|
||||
return {"content":"""步骤1: 确定需要查询的字段。
|
||||
我们需要统计上海的订单总数,因此需要从"城市"字段中筛选出值为"上海"的记录,并使用COUNT函数计算这些记录的数量。
|
||||
|
||||
步骤2: 确定筛选条件。
|
||||
问题要求统计上海的订单总数,所以我们需要在SQL语句中添加WHERE "城市" = '上海'来筛选出符合条件的记录。
|
||||
|
||||
步骤3: 避免关键字冲突。
|
||||
因为这个Excel/CSV数据库是 PostgreSQL 类型,所以在schema、表名、字段名和别名外层加双引号。
|
||||
|
||||
最终答案:
|
||||
```json
|
||||
{"success":true,"sql":"SELECT COUNT(*) AS \"TotalOrders\" FROM \"public\".\"Sheet1_c27345b66e\" WHERE \"城市\" = '上海';"}
|
||||
```"""}
|
||||
|
||||
|
||||
@router.post("/question", operation_id="question")
|
||||
async def stream_sql(session: SessionDep, current_user: CurrentUser, request_question: ChatQuestion):
|
||||
"""Stream SQL analysis results
|
||||
@@ -106,16 +99,17 @@ async def stream_sql(session: SessionDep, current_user: CurrentUser, request_que
|
||||
status_code=400,
|
||||
detail=f"Chat with id {request_question.chat_id} not found"
|
||||
)
|
||||
ds: CoreDatasource | None = None
|
||||
if chat.datasource:
|
||||
# Get available datasource
|
||||
ds = session.query(CoreDatasource).filter(CoreDatasource.id == chat.datasource).first()
|
||||
if not ds:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="No available datasource configuration found"
|
||||
)
|
||||
|
||||
# Get available datasource
|
||||
ds = session.query(CoreDatasource).filter(CoreDatasource.id == chat.datasource).first()
|
||||
if not ds:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="No available datasource configuration found"
|
||||
)
|
||||
|
||||
request_question.engine = ds.type_name if ds.type != 'excel' else 'PostgreSQL'
|
||||
request_question.engine = ds.type_name if ds.type != 'excel' else 'PostgreSQL'
|
||||
|
||||
# Get available AI model
|
||||
aimodel = session.exec(select(AiModelDetail).where(
|
||||
@@ -128,14 +122,18 @@ async def stream_sql(session: SessionDep, current_user: CurrentUser, request_que
|
||||
detail="No available AI model configuration found"
|
||||
)
|
||||
|
||||
history_records: List[ChatRecord] = list_records(session=session, current_user=current_user,
|
||||
chart_id=request_question.chat_id)
|
||||
history_records: List[ChatRecord] = list(filter(lambda r: True if r.first_chat != True else False,
|
||||
list_records(session=session, current_user=current_user,
|
||||
chart_id=request_question.chat_id)))
|
||||
# get schema
|
||||
request_question.db_schema = get_table_schema(session=session, ds=ds)
|
||||
if ds:
|
||||
request_question.db_schema = get_table_schema(session=session, ds=ds)
|
||||
|
||||
db_user = get_user_info(session=session, user_id=current_user.id)
|
||||
request_question.lang = db_user.language
|
||||
|
||||
llm_service = LLMService(request_question, aimodel, history_records, CoreDatasource(**ds.model_dump()))
|
||||
llm_service = LLMService(request_question, aimodel, history_records,
|
||||
CoreDatasource(**ds.model_dump()) if ds else None)
|
||||
|
||||
llm_service.init_record(session=session, current_user=current_user)
|
||||
|
||||
@@ -144,6 +142,16 @@ async def stream_sql(session: SessionDep, current_user: CurrentUser, request_que
|
||||
# return id
|
||||
yield orjson.dumps({'type': 'id', 'id': llm_service.get_record().id}).decode() + '\n\n'
|
||||
|
||||
# select datasource if datasource is none
|
||||
if not ds:
|
||||
ds_res = llm_service.select_datasource(session=session)
|
||||
for chunk in ds_res:
|
||||
yield orjson.dumps({'content': chunk, 'type': 'datasource-result'}).decode() + '\n\n'
|
||||
yield orjson.dumps({'id': llm_service.ds.id, 'datasource_name': llm_service.ds.name,
|
||||
'engine_type': llm_service.ds.type_name, 'type': 'datasource'}).decode() + '\n\n'
|
||||
|
||||
llm_service.chat_question.db_schema = get_table_schema(session=session, ds=llm_service.ds)
|
||||
|
||||
# generate sql
|
||||
sql_res = llm_service.generate_sql(session=session)
|
||||
full_sql_text = ''
|
||||
|
||||
@@ -60,6 +60,8 @@ def get_chat_with_records(session: SessionDep, chart_id: int, current_user: Curr
|
||||
load_only(ChatRecord.id, ChatRecord.chat_id, ChatRecord.create_time, ChatRecord.finish_time,
|
||||
ChatRecord.question, ChatRecord.sql_answer, ChatRecord.sql, ChatRecord.data,
|
||||
ChatRecord.chart_answer, ChatRecord.chart, ChatRecord.analysis, ChatRecord.predict,
|
||||
ChatRecord.datasource_select_answer, ChatRecord.recommended_question_answer,
|
||||
ChatRecord.recommended_question,
|
||||
ChatRecord.predict_data, ChatRecord.finish, ChatRecord.error, ChatRecord.run_time)).filter(
|
||||
and_(Chat.create_by == current_user.id, ChatRecord.chat_id == chart_id)).order_by(ChatRecord.create_time).all()
|
||||
|
||||
@@ -74,7 +76,8 @@ def list_records(session: SessionDep, chart_id: int, current_user: CurrentUser)
|
||||
return record_list
|
||||
|
||||
|
||||
def create_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj: CreateChat, require_datasource: bool = True) -> ChatInfo:
|
||||
def create_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj: CreateChat,
|
||||
require_datasource: bool = True) -> ChatInfo:
|
||||
if not create_chat_obj.datasource and require_datasource:
|
||||
raise Exception("Datasource cannot be None")
|
||||
|
||||
@@ -84,7 +87,7 @@ def create_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj:
|
||||
chat = Chat(create_time=datetime.datetime.now(),
|
||||
create_by=current_user.id,
|
||||
brief=create_chat_obj.question.strip()[:20])
|
||||
ds: CoreDatasource = None
|
||||
ds: CoreDatasource | None = None
|
||||
if create_chat_obj.datasource:
|
||||
chat.datasource = create_chat_obj.datasource
|
||||
ds = session.query(CoreDatasource).filter(CoreDatasource.id == create_chat_obj.datasource).first()
|
||||
@@ -93,6 +96,8 @@ def create_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj:
|
||||
raise Exception(f"Datasource with id {create_chat_obj.datasource} not found")
|
||||
|
||||
chat.engine_type = ds.type_name
|
||||
else:
|
||||
chat.engine_type = ''
|
||||
|
||||
chat_info = ChatInfo(**chat.model_dump())
|
||||
|
||||
@@ -102,15 +107,9 @@ def create_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj:
|
||||
chat_info.id = chat.id
|
||||
session.commit()
|
||||
|
||||
if not create_chat_obj.datasource:
|
||||
# use AI to get ds
|
||||
|
||||
if not ds:
|
||||
raise Exception(f"Datasource with id {create_chat_obj.datasource} not found")
|
||||
|
||||
|
||||
chat_info.datasource_exists = True
|
||||
chat_info.datasource_name = ds.name
|
||||
if ds:
|
||||
chat_info.datasource_exists = True
|
||||
chat_info.datasource_name = ds.name
|
||||
|
||||
return chat_info
|
||||
|
||||
@@ -205,6 +204,30 @@ def save_full_predict_message_and_answer(session: SessionDep, record_id: int, an
|
||||
return result
|
||||
|
||||
|
||||
def save_full_select_datasource_message_and_answer(session: SessionDep, record_id: int, answer: str,
|
||||
full_message: str, datasource: int = None,
|
||||
engine_type: str = None) -> ChatRecord:
|
||||
if not record_id:
|
||||
raise Exception("Record id cannot be None")
|
||||
record = session.query(ChatRecord).filter(ChatRecord.id == record_id).first()
|
||||
record.full_select_datasource_message = full_message
|
||||
record.datasource_select_answer = answer
|
||||
|
||||
if datasource:
|
||||
record.datasource = datasource
|
||||
record.engine_type = engine_type
|
||||
|
||||
result = ChatRecord(**record.model_dump())
|
||||
|
||||
session.add(record)
|
||||
session.flush()
|
||||
session.refresh(record)
|
||||
|
||||
session.commit()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def save_sql(session: SessionDep, record_id: int, sql: str) -> ChatRecord:
|
||||
if not record_id:
|
||||
raise Exception("Record id cannot be None")
|
||||
|
||||
@@ -7,8 +7,10 @@ from sqlmodel import SQLModel, Field
|
||||
|
||||
from apps.template.generate_analysis.generator import get_analysis_template
|
||||
from apps.template.generate_chart.generator import get_chart_template
|
||||
from apps.template.generate_guess_question.generator import get_guess_question_template
|
||||
from apps.template.generate_predict.generator import get_predict_template
|
||||
from apps.template.generate_sql.generator import get_sql_template
|
||||
from apps.template.select_datasource.generator import get_datasource_template
|
||||
|
||||
|
||||
class Chat(SQLModel, table=True):
|
||||
@@ -25,11 +27,13 @@ class Chat(SQLModel, table=True):
|
||||
class ChatRecord(SQLModel, table=True):
|
||||
__tablename__ = "chat_record"
|
||||
id: Optional[int] = Field(sa_column=Column(Integer, Identity(always=True), primary_key=True))
|
||||
chat_id: int = Field(sa_column=Column(Integer))
|
||||
chat_id: int = Field(sa_column=Column(Integer, nullable=False))
|
||||
ai_modal_id: Optional[int] = Field(sa_column=Column(Integer))
|
||||
first_chat: bool = Field(sa_column=Column(Boolean, nullable=True, default=False))
|
||||
create_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True))
|
||||
finish_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True))
|
||||
create_by: int = Field(sa_column=Column(BigInteger, nullable=True))
|
||||
datasource: int = Field(sa_column=Column(Integer, nullable=False))
|
||||
datasource: int = Field(sa_column=Column(Integer, nullable=True))
|
||||
engine_type: str = Field(max_length=64)
|
||||
question: str = Field(sa_column=Column(Text, nullable=True))
|
||||
sql_answer: str = Field(sa_column=Column(Text, nullable=True))
|
||||
@@ -41,10 +45,21 @@ class ChatRecord(SQLModel, table=True):
|
||||
analysis: str = Field(sa_column=Column(Text, nullable=True))
|
||||
predict: str = Field(sa_column=Column(Text, nullable=True))
|
||||
predict_data: str = Field(sa_column=Column(Text, nullable=True))
|
||||
recommended_question_answer: str = Field(sa_column=Column(Text, nullable=True))
|
||||
recommended_question: str = Field(sa_column=Column(Text, nullable=True))
|
||||
datasource_select_answer: str = Field(sa_column=Column(Text, nullable=True))
|
||||
full_sql_message: str = Field(sa_column=Column(Text, nullable=True))
|
||||
token_sql: int = Field(default=0, nullable=True)
|
||||
full_chart_message: str = Field(sa_column=Column(Text, nullable=True))
|
||||
token_chart: int = Field(default=0, nullable=True)
|
||||
full_analysis_message: str = Field(sa_column=Column(Text, nullable=True))
|
||||
token_analysis: int = Field(default=0, nullable=True)
|
||||
full_predict_message: str = Field(sa_column=Column(Text, nullable=True))
|
||||
token_predict: int = Field(default=0, nullable=True)
|
||||
full_recommended_question_message: str = Field(sa_column=Column(Text, nullable=True))
|
||||
token_recommended_question: int = Field(default=0, nullable=True)
|
||||
full_select_datasource_message: str = Field(sa_column=Column(Text, nullable=True))
|
||||
token_select_datasource_question: int = Field(default=0, nullable=True)
|
||||
finish: bool = Field(sa_column=Column(Boolean, nullable=True, default=False))
|
||||
error: str = Field(sa_column=Column(Text, nullable=True))
|
||||
run_time: float = Field(default=0)
|
||||
@@ -67,7 +82,7 @@ class ChatInfo(BaseModel):
|
||||
create_by: int = None
|
||||
brief: str = ''
|
||||
chat_type: str = "chat"
|
||||
datasource: int = None
|
||||
datasource: Optional[int] = None
|
||||
engine_type: str = ''
|
||||
datasource_name: str = ''
|
||||
datasource_exists: bool = True
|
||||
@@ -108,7 +123,24 @@ class AiModelQuestion(BaseModel):
|
||||
def predict_user_question(self):
|
||||
return get_predict_template()['user'].format(fields=self.fields, data=self.data, lang=self.lang)
|
||||
|
||||
def datasource_sys_question(self):
|
||||
return get_datasource_template()['system']
|
||||
|
||||
def datasource_user_question(self, datasource_list: str = "[]"):
|
||||
return get_datasource_template()['user'].format(question=self.question, data=datasource_list, lang=self.lang)
|
||||
|
||||
def datasource_guess_sys_question(self):
|
||||
return get_guess_question_template()['system']
|
||||
|
||||
def datasource_guess_user_question(self, old_questions: str = "[]"):
|
||||
return get_guess_question_template()['user'].format(question=self.question, schema=self.db_schema,
|
||||
old_questions=old_questions, lang=self.lang)
|
||||
|
||||
|
||||
class ChatQuestion(AiModelQuestion):
|
||||
question: str
|
||||
chat_id: int
|
||||
question: str = ''
|
||||
chat_id: int = 0
|
||||
|
||||
|
||||
class ChatMcp(ChatQuestion):
|
||||
token:str = ''
|
||||
@@ -6,12 +6,15 @@ import orjson
|
||||
from langchain_community.utilities import SQLDatabase
|
||||
from langchain_core.language_models import BaseLLM
|
||||
from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage, AIMessage, AIMessageChunk
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import load_only
|
||||
|
||||
from apps.ai_model.model_factory import LLMConfig, LLMFactory, get_llm_config
|
||||
from apps.chat.curd.chat import save_question, save_full_sql_message, save_full_sql_message_and_answer, save_sql, \
|
||||
save_error_message, save_sql_exec_data, save_full_chart_message, save_full_chart_message_and_answer, save_chart, \
|
||||
finish_record, save_full_analysis_message_and_answer, save_full_predict_message_and_answer, save_predict_data
|
||||
from apps.chat.models.chat_model import ChatQuestion, ChatRecord
|
||||
finish_record, save_full_analysis_message_and_answer, save_full_predict_message_and_answer, save_predict_data, \
|
||||
save_full_select_datasource_message_and_answer
|
||||
from apps.chat.models.chat_model import ChatQuestion, ChatRecord, Chat
|
||||
from apps.datasource.models.datasource import CoreDatasource
|
||||
from apps.db.db import exec_sql
|
||||
from apps.system.models.system_model import AiModelDetail
|
||||
@@ -30,6 +33,7 @@ class LLMService:
|
||||
llm: BaseLLM
|
||||
sql_message: List[Union[BaseMessage, dict[str, Any]]] = []
|
||||
chart_message: List[Union[BaseMessage, dict[str, Any]]] = []
|
||||
history_records: List[ChatRecord] = []
|
||||
|
||||
def __init__(self, chat_question: ChatQuestion, aimodel: AiModelDetail, history_records: List[ChatRecord] = [],
|
||||
ds: CoreDatasource = None):
|
||||
@@ -41,17 +45,22 @@ class LLMService:
|
||||
llm_instance = LLMFactory.create_llm(self.config)
|
||||
self.llm = llm_instance.llm
|
||||
|
||||
self.history_records = history_records
|
||||
|
||||
self.init_messages()
|
||||
|
||||
def init_messages(self):
|
||||
# self.agent_executor = create_react_agent(self.llm)
|
||||
last_sql_messages = list(
|
||||
filter(lambda r: True if r.full_sql_message is not None and r.full_sql_message.strip() != '' else False,
|
||||
history_records))
|
||||
self.history_records))
|
||||
last_sql_message_str = "[]" if last_sql_messages is None or len(last_sql_messages) == 0 else last_sql_messages[
|
||||
-1].full_sql_message
|
||||
|
||||
last_chart_messages = list(
|
||||
filter(
|
||||
lambda r: True if r.full_chart_message is not None and r.full_chart_message.strip() != '' else False,
|
||||
history_records))
|
||||
self.history_records))
|
||||
last_chart_message_str = "[]" if last_chart_messages is None or len(last_chart_messages) == 0 else \
|
||||
last_chart_messages[-1].full_chart_message
|
||||
|
||||
@@ -216,6 +225,92 @@ class LLMService:
|
||||
in
|
||||
predict_msg]).decode())
|
||||
|
||||
def select_datasource(self, session: SessionDep):
|
||||
datasource_msg: List[Union[BaseMessage, dict[str, Any]]] = []
|
||||
datasource_msg.append(SystemMessage(self.chat_question.datasource_sys_question()))
|
||||
_ds_list = session.exec(select(CoreDatasource).options(
|
||||
load_only(CoreDatasource.id, CoreDatasource.name, CoreDatasource.description))).all()
|
||||
_ds_list_dict = []
|
||||
for _ds in _ds_list:
|
||||
_ds_list_dict.append({'id': _ds[0].id, 'name': _ds[0].name, 'description': _ds[0].description})
|
||||
datasource_msg.append(
|
||||
HumanMessage(self.chat_question.datasource_user_question(orjson.dumps(_ds_list_dict).decode())))
|
||||
|
||||
history_msg = []
|
||||
if self.record.full_select_datasource_message and self.record.full_select_datasource_message.strip() != '':
|
||||
history_msg = orjson.loads(self.record.full_select_datasource_message)
|
||||
|
||||
self.record = save_full_select_datasource_message_and_answer(session=session, record_id=self.record.id,
|
||||
answer='',
|
||||
full_message=orjson.dumps(history_msg +
|
||||
[{'type': msg.type,
|
||||
'content': msg.content}
|
||||
for msg
|
||||
in
|
||||
datasource_msg]).decode())
|
||||
full_text = ''
|
||||
res = self.llm.stream(datasource_msg)
|
||||
for chunk in res:
|
||||
print(chunk)
|
||||
if isinstance(chunk, dict):
|
||||
full_text += chunk['content']
|
||||
yield chunk['content']
|
||||
continue
|
||||
if isinstance(chunk, AIMessageChunk):
|
||||
full_text += chunk.content
|
||||
yield chunk.content
|
||||
continue
|
||||
datasource_msg.append(AIMessage(full_text))
|
||||
|
||||
json_str = extract_nested_json(full_text)
|
||||
|
||||
_error: Exception | None = None
|
||||
_datasource: int | None = None
|
||||
_engine_type: str | None = None
|
||||
try:
|
||||
data = orjson.loads(json_str)
|
||||
|
||||
if data['id'] and data['id'] != 0:
|
||||
_datasource = data['id']
|
||||
_ds = session.query(CoreDatasource).filter(CoreDatasource.id == _datasource).first()
|
||||
if not _ds:
|
||||
_datasource = None
|
||||
raise Exception(f"Datasource configuration with id {_datasource} not found")
|
||||
self.ds = CoreDatasource(**_ds.model_dump())
|
||||
self.chat_question.engine = _ds.type_name if _ds.type != 'excel' else 'PostgreSQL'
|
||||
_engine_type = self.chat_question.engine
|
||||
# save chat
|
||||
_chat = session.query(Chat).filter(Chat.id == self.record.chat_id).first()
|
||||
_chat.datasource = _datasource
|
||||
_chat.engine_type = _ds.type_name
|
||||
|
||||
session.add(_chat)
|
||||
session.flush()
|
||||
session.refresh(_chat)
|
||||
session.commit()
|
||||
|
||||
elif data['fail']:
|
||||
raise Exception(data['fail'])
|
||||
else:
|
||||
raise Exception('No available datasource configuration found')
|
||||
|
||||
except Exception as e:
|
||||
_error = e
|
||||
|
||||
self.record = save_full_select_datasource_message_and_answer(session=session, record_id=self.record.id,
|
||||
answer=full_text, datasource=_datasource,
|
||||
engine_type=_engine_type,
|
||||
full_message=orjson.dumps(history_msg +
|
||||
[{'type': msg.type,
|
||||
'content': msg.content}
|
||||
for msg
|
||||
in
|
||||
datasource_msg]).decode())
|
||||
self.init_messages()
|
||||
|
||||
if _error:
|
||||
raise _error
|
||||
|
||||
def generate_sql(self, session: SessionDep):
|
||||
# append current question
|
||||
self.sql_message.append(HumanMessage(self.chat_question.sql_user_question()))
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from apps.template.template import get_base_template
|
||||
|
||||
|
||||
def get_guess_question_template():
|
||||
template = get_base_template()
|
||||
return template['template']['guess']
|
||||
@@ -0,0 +1,6 @@
|
||||
from apps.template.template import get_base_template
|
||||
|
||||
|
||||
def get_datasource_template():
|
||||
template = get_base_template()
|
||||
return template['template']['datasource']
|
||||
@@ -25,7 +25,10 @@ wlist = [
|
||||
"/mcp/message",
|
||||
"/datasource/list",
|
||||
"/system/aimodel/list",
|
||||
"/chat/mcp_question"
|
||||
"/chat/mcp_question",
|
||||
"/chat/mcp_start",
|
||||
"/system/license",
|
||||
"/system/config/key"
|
||||
]
|
||||
|
||||
class WhitelistChecker:
|
||||
|
||||
+4
-21
@@ -1,10 +1,7 @@
|
||||
import logging
|
||||
from fastapi.concurrency import asynccontextmanager
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import os
|
||||
import sentry_sdk
|
||||
from fastapi import FastAPI, Path, HTTPException
|
||||
from fastapi import FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
@@ -15,9 +12,7 @@ from common.core.response_middleware import ResponseMiddleware, exception_handle
|
||||
from alembic.config import Config
|
||||
from alembic import command
|
||||
from fastapi_mcp import FastApiMCP
|
||||
|
||||
|
||||
# import sqlbot_xpack
|
||||
import sqlbot_xpack
|
||||
|
||||
def run_migrations():
|
||||
alembic_cfg = Config("alembic.ini")
|
||||
@@ -53,7 +48,7 @@ mcp = FastApiMCP(
|
||||
description="SQLBot MCP Server",
|
||||
describe_all_responses=True,
|
||||
describe_full_response_schema=True,
|
||||
include_operations=["get_datasource_list", "get_model_list", "mcp_question"]
|
||||
include_operations=["get_datasource_list", "get_model_list", "mcp_question", "mcp_start"]
|
||||
)
|
||||
|
||||
mcp.mount(mcp_app)
|
||||
@@ -76,23 +71,11 @@ app.include_router(api_router, prefix=settings.API_V1_STR)
|
||||
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):
|
||||
logging.warning(f"The front-end build directory does not exist: {frontend_dist}")
|
||||
logging.warning("Please make sure you have built the front-end project")
|
||||
|
||||
else:
|
||||
|
||||
@app.get("/", include_in_schema=False)
|
||||
async def read_index():
|
||||
return FileResponse(path=os.path.join(frontend_dist, "index.html"))
|
||||
|
||||
|
||||
app.mount("/", StaticFiles(directory=frontend_dist), name="static")
|
||||
|
||||
mcp.setup_server()
|
||||
|
||||
# sqlbot_xpack.init_fastapi_app(app)
|
||||
sqlbot_xpack.init_fastapi_app(app)
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
|
||||
@@ -35,10 +35,20 @@ dependencies = [
|
||||
"oracledb (>=3.1.1,<4.0.0)",
|
||||
"pyyaml (>=6.0.2,<7.0.0)",
|
||||
"fastapi-mcp (>=0.3.4,<0.4.0)",
|
||||
"sqlbot-xpack==0.0.2",
|
||||
]
|
||||
[[tool.uv.index]]
|
||||
name = "default"
|
||||
url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
|
||||
default = true
|
||||
[[tool.uv.index]]
|
||||
|
||||
name = "testpypi"
|
||||
url = "https://test.pypi.org/simple"
|
||||
explicit = true
|
||||
|
||||
[tool.uv.sources]
|
||||
sqlbot-xpack = { index = "testpypi" }
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
|
||||
+38
-17
@@ -10,9 +10,9 @@ template:
|
||||
- SQL查询的字段名不要自动翻译,别名必须为英文。
|
||||
- 计算占比,百分比类型字段,保留两位小数,以%结尾。
|
||||
- 生成SQL时,必须避免关键字冲突:
|
||||
- 如{engine}是 PostgreSQL、Oracle,则在schema、表名、字段名、别名外层加双引号;
|
||||
- 如{engine}是 MySQL,则在表名、字段名、别名外层加反引号;
|
||||
- 如{engine}是 Microsoft SQL Server,则在schema、表名、字段名、别名外层加方括号。
|
||||
- 如数据库引擎是 PostgreSQL、Oracle,则在schema、表名、字段名、别名外层加双引号;
|
||||
- 如数据库引擎是 MySQL,则在表名、字段名、别名外层加反引号;
|
||||
- 如数据库引擎是 Microsoft SQL Server,则在schema、表名、字段名、别名外层加方括号。
|
||||
- 生成的SQL使用JSON格式返回:
|
||||
{{"success":true,"sql":"生成的SQL语句"}}
|
||||
- 问题与生成SQL无关时,直接回答:
|
||||
@@ -94,30 +94,31 @@ template:
|
||||
guess:
|
||||
system: |
|
||||
### 说明:
|
||||
您的任务是根据给定的表结构和问题,推测用户接下来可能提问的1-4个问题。
|
||||
您的任务是根据给定的表结构,用户问题以及以往用户提问,推测用户接下来可能提问的1-4个问题。
|
||||
请遵循以下规则:
|
||||
- 推测的问题不能与用户问题重复。
|
||||
- 推测的问题必须与给出的表结构相关。
|
||||
- 推测的问题不能与当前用户问题重复
|
||||
- 推测的问题必须与给出的表结构相关
|
||||
- 若有以往用户提问列表,则根据以往用户提问列表,推测用户最频繁提问的问题,加入到你生成的推测问题中
|
||||
- 生成的推测问题使用JSON格式返回:
|
||||
{["推测问题1", "推测问题2", "推测问题3", "推测问题4"]}
|
||||
["推测问题1", "推测问题2", "推测问题3", "推测问题4"]
|
||||
- 最多返回4个你推测出的结果
|
||||
- 若无法推测,则返回空数据JSON:
|
||||
[]
|
||||
|
||||
### 响应:
|
||||
根据您的指示,这是我生成的与 表结构 和 问题 相关的 JSON:
|
||||
```json
|
||||
|
||||
### 请遵守以下输出要求:
|
||||
请逐步推理后给出答案
|
||||
推理过程中不需要输出JSON,仅在最终结果内输出符合要求的JSON
|
||||
步骤1: [思考内容]
|
||||
步骤2: [思考内容]
|
||||
……
|
||||
最终答案: [结果]
|
||||
|
||||
user: |
|
||||
### 请使用 i18n: {lang} 对应的语言输出你的结果
|
||||
|
||||
### 表结构:
|
||||
{schema}
|
||||
|
||||
### 问题:
|
||||
### 当前问题:
|
||||
{question}
|
||||
|
||||
### 以往提问:
|
||||
{old_questions}
|
||||
analysis:
|
||||
system: |
|
||||
### 说明:
|
||||
@@ -148,3 +149,23 @@ template:
|
||||
|
||||
### 数据:
|
||||
{data}
|
||||
datasource:
|
||||
system: |
|
||||
### 说明:
|
||||
你是一个数据分析师,你需要根据用户的提问,以及提供的数据源列表(格式为JSON数组:[{{"id": 数据源ID1,"name":"数据源名称1","description":"数据源描述1"}},{{"id": 数据源ID2,"name":"数据源名称2","description":"数据源描述2"}}]),根据名称和描述找出最符合用户提问的数据源,这个数据源后续将被用来进行数据的分析
|
||||
|
||||
### 要求:
|
||||
- 以JSON格式返回你找到的符合提问的数据源ID,格式为:{{"id": 符合要求的数据源ID}}
|
||||
- 如果匹配到多个数据源,则只需要返回其中一个即可
|
||||
- 如果没有符合要求的数据源,则返回:{{"fail":"没有找到匹配的数据源"}}
|
||||
- 不需要思考过程,请直接返回JSON结果
|
||||
|
||||
```json
|
||||
user: |
|
||||
### 请使用 i18n: {lang} 对应的语言输出你的结果
|
||||
|
||||
### 数据源列表:
|
||||
{data}
|
||||
|
||||
### 问题:
|
||||
{question}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ElMessage } from 'element-plus-secondary'
|
||||
import { useCache } from '@/utils/useCache'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
import { request } from '@/utils/request'
|
||||
const userStore = useUserStore()
|
||||
const { wsCache } = useCache()
|
||||
const whiteList = ['/login']
|
||||
export const watchRouter = (router: any) => {
|
||||
router.beforeEach(async (to: any, from: any, next: any) => {
|
||||
await loadXpackStatic()
|
||||
const token = wsCache.get('user.token')
|
||||
if (whiteList.includes(to.path)) {
|
||||
next()
|
||||
@@ -32,3 +33,14 @@ export const watchRouter = (router: any) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
const loadXpackStatic = () => {
|
||||
if (document.getElementById('sqlbot_xpack_static')) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
const url = '/xpack_static/license-generator.umd.js'
|
||||
return request.loadRemoteScript(url, 'sqlbot_xpack_static', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
LicenseGenerator?.init(import.meta.env.VITE_API_BASE_URL)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,6 +69,15 @@ class HttpService {
|
||||
if (token && config.headers) {
|
||||
config.headers['X-SQLBOT-TOKEN'] = `Bearer ${token}`
|
||||
}
|
||||
if (config.url?.includes('/xpack_static/') && config.baseURL) {
|
||||
config.baseURL = config.baseURL.replace('/api/v1', '')
|
||||
// Skip auth for xpack_static requests
|
||||
return config
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const request_key = LicenseGenerator.generate()
|
||||
config.headers['X-SQLBOT-KEY'] = request_key
|
||||
|
||||
// Request logging
|
||||
// console.log(`[Request] ${config.method?.toUpperCase()} ${config.url}`)
|
||||
@@ -258,6 +267,38 @@ class HttpService {
|
||||
responseType: 'blob',
|
||||
})
|
||||
}
|
||||
|
||||
public loadRemoteScript(url: string, id?: string, cb?: any): Promise<HTMLElement> {
|
||||
if (!url) {
|
||||
return Promise.reject(new Error('URL is required to load remote script'))
|
||||
}
|
||||
if (id && document.getElementById(id)) {
|
||||
return Promise.resolve(document.getElementById(id) as HTMLElement)
|
||||
}
|
||||
return new Promise<HTMLElement>((resolve, reject) => {
|
||||
this.get(url, {
|
||||
responseType: 'text',
|
||||
headers: {
|
||||
'Content-Type': 'application/javascript',
|
||||
},
|
||||
})
|
||||
.then((response: any) => {
|
||||
const script = document.createElement('script')
|
||||
script.textContent = response
|
||||
script.id = id || `remote-script-${Date.now()}`
|
||||
// Append script to head
|
||||
document.head.appendChild(script)
|
||||
if (cb) {
|
||||
cb()
|
||||
}
|
||||
resolve(script)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error(`Failed to load script from ${url}:`, error)
|
||||
reject(new Error(`Failed to load script from ${url}: ${error.message}`))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
|
||||
@@ -63,7 +63,7 @@ defineExpose({
|
||||
<template>
|
||||
<div v-if="message?.record">
|
||||
<div v-if="message.record.chart" class="chart-base-container">
|
||||
<div>
|
||||
<div v-if="data?.length > 0">
|
||||
<ChartComponent
|
||||
v-if="message.record.id"
|
||||
:id="id ?? 'default_chat_id'"
|
||||
@@ -76,6 +76,7 @@ defineExpose({
|
||||
:data="data"
|
||||
/>
|
||||
</div>
|
||||
<el-empty v-else description="No Data" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -409,6 +409,8 @@ const sendMessage = async () => {
|
||||
currentRecord.error = currentRecord.error + 'Error:' + error
|
||||
console.error('Error:', error)
|
||||
isTyping.value = false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user