This commit is contained in:
dataeaseShu
2025-09-05 10:35:02 +08:00
15 changed files with 124 additions and 62 deletions
+3 -5
View File
@@ -54,11 +54,9 @@ ENV SQLBOT_HOME=/opt/sqlbot
ENV PYTHONPATH=${SQLBOT_HOME}/app
ENV PATH="${SQLBOT_HOME}/app/.venv/bin:$PATH"
# ENV POSTGRES_SERVER=localhost
# ENV POSTGRES_PORT=5432
# ENV POSTGRES_DB=sqlbot
# ENV POSTGRES_USER=root
# ENV POSTGRES_PASSWORD=Password123@pg
ENV POSTGRES_DB=sqlbot
ENV POSTGRES_USER=root
ENV POSTGRES_PASSWORD=Password123@pg
# Copy necessary files from builder
COPY start.sh /opt/sqlbot/app/start.sh
+1
View File
@@ -22,6 +22,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libgif-dev \
librsvg2-dev \
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& curl -qL https://www.npmjs.com/install.sh | sh - \
&& apt-get install -y nodejs \
&& curl -L --connect-timeout 60 -m 1800 https://fit2cloud-support.oss-cn-beijing.aliyuncs.com/xpack-license/get-validator-linux | sh \
&& rm -rf /var/lib/apt/lists/* \
+12 -11
View File
@@ -19,18 +19,19 @@ SQLBot 是一款基于大模型和 RAG 的智能问数系统。SQLBot 的优势
### 安装部署
准备一台 Linux 服务器,执行以下一键安装脚本。
在运行 SQLBot 前,请确保已安装好 [Docker](https://docs.docker.com/get-docker/) 和 [Docker Compose](https://docs.docker.com/compose/install/)。
在运行 SQLBot 前,请确保已安装好 [Docker](https://docs.docker.com/get-docker/)。
```bash
# 创建目录
mkdir -p /opt/sqlbot
cd /opt/sqlbot
# 下载 docker-compose.yaml
curl -o docker-compose.yaml https://raw.githubusercontent.com/dataease/SQLBot/main/docker-compose.yaml
# 启动服务
docker compose up -d
docker run -d \
--name sqlbot \
--restart unless-stopped \
-p 8000:8000 \
-p 8001:8001 \
-v ./data/sqlbot/excel:/opt/sqlbot/data/excel \
-v ./data/sqlbot/images:/opt/sqlbot/images \
-v ./data/sqlbot/logs:/opt/sqlbot/logs \
-v ./data/postgresql:/var/lib/postgresql/data \
dataease/sqlbot:v1.1.1
```
你也可以通过 [1Panel 应用商店](https://apps.fit2cloud.com/1panel) 快速部署 SQLBot。
@@ -58,8 +59,8 @@ docker compose up -d
## Roadmap
- 术语库:开发中...
- SQL 示例库:开发中...
- 自定义提示词:开发中...
## Star History
+17 -1
View File
@@ -71,6 +71,21 @@ def get_chart_config(session: SessionDep, chart_record_id: int):
return {}
def get_last_execute_sql_error(session: SessionDep, chart_id: int):
stmt = select(ChatRecord.error).where(and_(ChatRecord.chat_id == chart_id)).order_by(
ChatRecord.create_time.desc()).limit(1)
res = session.execute(stmt).scalar()
if res:
try:
obj = orjson.loads(res)
if obj.get('type') and obj.get('type') == 'exec-sql-err':
return obj.get('traceback')
except Exception:
pass
return None
def get_chat_chart_data(session: SessionDep, chart_record_id: int):
stmt = select(ChatRecord.data).where(and_(ChatRecord.id == chart_record_id))
res = session.execute(stmt)
@@ -701,7 +716,8 @@ def get_old_questions(session: SessionDep, datasource: int):
if not datasource:
return records
stmt = select(ChatRecord.question).where(
and_(ChatRecord.datasource == datasource, ChatRecord.question.isnot(None), ChatRecord.error.is_(None))).order_by(
and_(ChatRecord.datasource == datasource, ChatRecord.question.isnot(None),
ChatRecord.error.is_(None))).order_by(
ChatRecord.create_time.desc()).limit(20)
result = session.execute(stmt)
for r in result:
+3 -2
View File
@@ -159,6 +159,7 @@ class ChatInfo(BaseModel):
class AiModelQuestion(BaseModel):
question: str = None
ai_modal_id: int = None
ai_modal_name: str = None # Specific model name
engine: str = ""
@@ -171,6 +172,7 @@ class AiModelQuestion(BaseModel):
filter: str = []
sub_query: Optional[list[dict]] = None
terminologies: str = ""
error_msg: str = ""
def sql_sys_question(self):
return get_sql_template()['system'].format(engine=self.engine, schema=self.db_schema, question=self.question,
@@ -178,7 +180,7 @@ class AiModelQuestion(BaseModel):
def sql_user_question(self, current_time: str):
return get_sql_template()['user'].format(engine=self.engine, schema=self.db_schema, question=self.question,
rule=self.rule, current_time=current_time)
rule=self.rule, current_time=current_time, error_msg=self.error_msg)
def chart_sys_question(self):
return get_chart_template()['system'].format(sql=self.sql, question=self.question, lang=self.lang)
@@ -226,7 +228,6 @@ class AiModelQuestion(BaseModel):
class ChatQuestion(AiModelQuestion):
question: str
chat_id: int
+13 -1
View File
@@ -24,7 +24,8 @@ from apps.chat.curd.chat import save_question, save_sql_answer, save_sql, \
finish_record, save_analysis_answer, save_predict_answer, save_predict_data, \
save_select_datasource_answer, save_recommend_question_answer, \
get_old_questions, save_analysis_predict_record, rename_chat, get_chart_config, \
get_chat_chart_data, list_generate_sql_logs, list_generate_chart_logs, start_log, end_log
get_chat_chart_data, list_generate_sql_logs, list_generate_chart_logs, start_log, end_log, \
get_last_execute_sql_error
from apps.chat.models.chat_model import ChatQuestion, ChatRecord, Chat, RenameChat, ChatLog, OperationEnum
from apps.datasource.crud.datasource import get_table_schema
from apps.datasource.crud.permission import get_row_permission_filters, is_normal_user
@@ -70,6 +71,8 @@ class LLMService:
chunk_list: List[str] = []
future: Future
last_execute_sql_error: str = None
def __init__(self, current_user: CurrentUser, chat_question: ChatQuestion,
current_assistant: Optional[CurrentAssistant] = None, no_reasoning: bool = False,
config: LLMConfig = None):
@@ -127,6 +130,15 @@ class LLMService:
llm_instance = LLMFactory.create_llm(self.config)
self.llm = llm_instance.llm
# get last_execute_sql_error
last_execute_sql_error = get_last_execute_sql_error(self.session, self.chat_question.chat_id)
if last_execute_sql_error:
self.chat_question.error_msg = f'''<error-msg>
{last_execute_sql_error}
</error-msg>'''
else:
self.chat_question.error_msg = ''
self.init_messages()
@classmethod
+1 -1
View File
@@ -47,7 +47,7 @@ class Settings(BaseSettings):
POSTGRES_SERVER: str = 'localhost'
POSTGRES_PORT: int = 5432
POSTGRES_USER: str = 'root'
POSTGRES_PASSWORD: str = "123456"
POSTGRES_PASSWORD: str = "Password123@pg"
POSTGRES_DB: str = "sqlbot"
SQLBOT_DB_URL: str = ''
# SQLBOT_DB_URL: str = 'mysql+pymysql://root:Password123%[email protected]:3306/sqlbot'
+20 -3
View File
@@ -68,7 +68,7 @@ def cache(
@wraps(func)
async def wrapper(*args, **kwargs):
if not settings.CACHE_TYPE or settings.CACHE_TYPE.lower() == "none":
if not settings.CACHE_TYPE or settings.CACHE_TYPE.lower() == "none" or not is_cache_initialized():
return await func(*args, **kwargs)
# 生成缓存键
cache_key = used_key_builder(
@@ -96,7 +96,7 @@ def clear_cache(
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
if not settings.CACHE_TYPE or settings.CACHE_TYPE.lower() == "none":
if not settings.CACHE_TYPE or settings.CACHE_TYPE.lower() == "none" or not is_cache_initialized():
return await func(*args, **kwargs)
cache_key = custom_key_builder(
func=func,
@@ -138,4 +138,21 @@ def init_sqlbot_cache():
SQLBotLogUtil.info(f"SQLBot 使用Redis缓存, 可使用多进程模式")
else:
SQLBotLogUtil.warning("SQLBot 未启用缓存, 可使用多进程模式")
def is_cache_initialized() -> bool:
# 检查必要的属性是否存在
if not hasattr(FastAPICache, "_backend") or not hasattr(FastAPICache, "_prefix"):
return False
# 检查属性值是否为 None
if FastAPICache._backend is None or FastAPICache._prefix is None:
return False
# 尝试获取后端确认
try:
backend = FastAPICache.get_backend()
return backend is not None
except (AssertionError, AttributeError, Exception) as e:
SQLBotLogUtil.debug(f"缓存初始化检查失败: {str(e)}")
return False
+2 -1
View File
@@ -98,7 +98,7 @@ template:
- 柱状图或折线图:适合展示在横轴的字段优先排序,若SQL包含分类字段,则分类字段次一级排序
</rule>
<rule>
如果用户没有指定数据条数的限制,输出的查询SQL需要加上1000条的数据条数限制
如果用户没有指定数据条数的限制,输出的查询SQL必须加上1000条的数据条数限制
如果用户指定的限制大于1000,则按1000处理
<example>
以PostgreSQL为例,查询Schema为TEST表TABLE下id字段,则生成的SQL为:
@@ -235,6 +235,7 @@ template:
{current_time}
</current-time>
<background-infos>
{error_msg}
<user-question>
{question}
</user-question>
+5 -24
View File
@@ -1,6 +1,6 @@
services:
sqlbot:
image: dataease/sqlbot:v1.0.0
image: dataease/sqlbot:v1.1.0
container_name: sqlbot
restart: always
networks:
@@ -10,11 +10,11 @@ services:
- 8001:8001
environment:
# Database configuration
POSTGRES_SERVER: sqlbot-db
POSTGRES_SERVER: localhost
POSTGRES_PORT: 5432
POSTGRES_DB: sqlbot
POSTGRES_USER: sqlbot
POSTGRES_PASSWORD: sqlbot
POSTGRES_USER: root
POSTGRES_PASSWORD: Password123@pg
# Project basic settings
PROJECT_NAME: "SQLBot"
DEFAULT_PWD: "SQLBot@123456"
@@ -31,26 +31,7 @@ services:
- ./data/sqlbot/excel:/opt/sqlbot/data/excel
- ./data/sqlbot/images:/opt/sqlbot/images
- ./data/sqlbot/logs:/opt/sqlbot/logs
depends_on:
sqlbot-db:
condition: service_healthy
sqlbot-db:
image: postgres:17.5
container_name: sqlbot-db
restart: always
networks:
- sqlbot-network
volumes:
- ./data/postgresql:/var/lib/postgresql/data
environment:
POSTGRES_DB: sqlbot
POSTGRES_USER: sqlbot
POSTGRES_PASSWORD: sqlbot
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 3s
timeout: 5s
retries: 5
networks:
sqlbot-network:
@@ -13,6 +13,10 @@ export const dashboardStore = defineStore('dashboard', {
canvasViewInfo: {},
fullscreenFlag: false,
dataPrepareState: false,
baseMatrixCount: {
x: 72,
y: 36,
},
dashboardInfo: {
id: null,
name: null,
@@ -1069,8 +1069,6 @@ const forceComputed = () => {
}
function addItemBox(item: CanvasItem) {
// @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
item.x = findPositionX(item)
canvasComponentData.value.push(item)
forceComputed()
nextTick(() => {
@@ -1177,8 +1175,7 @@ function tabMoveInCheckSQ() {
/**
* Find position box
*/
function findPositionX(item: CanvasItem) {
const width = item.sizeX
function findPositionX(width: number) {
let resultX = 1
let checkPointYIndex = -1 // -1 means not occupying any Y-direction canvas
// Component width
@@ -1245,6 +1242,7 @@ defineExpose({
removeItem,
findClosetCoords,
makeCoordinate,
findPositionX,
})
</script>
@@ -85,6 +85,11 @@ const addItemToBox = (item: CanvasItem) => {
canvasCoreRef.value.addItemBox(item)
}
const findPositionX = (width: number) => {
// @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
return canvasCoreRef.value.findPositionX(width)
}
useEmitt({
name: 'custom-canvas-resize',
callback: canvasSizeInit,
@@ -93,6 +98,7 @@ useEmitt({
defineExpose({
canvasSizeInit,
addItemToBox,
findPositionX,
})
onMounted(() => {
@@ -22,7 +22,7 @@ const { dashboardInfo, fullscreenFlag } = storeToRefs(dashboardStore)
const snapshotStore = snapshotStoreWithOut()
const { snapshotIndex } = storeToRefs(snapshotStore)
const emits = defineEmits(['addComponent'])
const emits = defineEmits(['addComponents'])
const resourceGroupOptRef = ref(null)
const chatChartSelectionRef = ref(null)
const openViewDialog = () => {
@@ -139,10 +139,11 @@ const backToMain = () => {
}
const addChatChart = (views: any) => {
emits('addComponents', 'SQView', views)
views.forEach((view: any) => {
const target = cloneDeep(view)
delete target.chart.sourceType
emits('addComponent', 'SQView', target)
emits('addComponents', 'SQView', target)
})
ElMessage({
type: 'success',
@@ -212,14 +213,14 @@ const previewInner = () => {
:title="t('dashboard.text')"
themes="light"
is-label
@custom-click="() => emits('addComponent', 'SQText')"
@custom-click="() => emits('addComponents', 'SQText')"
></component-button-label>
<component-button-label
:icon-name="dvTab"
title="Tab"
themes="light"
is-label
@custom-click="() => emits('addComponent', 'SQTab')"
@custom-click="() => emits('addComponents', 'SQTab')"
>
</component-button-label>
</div>
+30 -5
View File
@@ -13,7 +13,8 @@ import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const dashboardStore = dashboardStoreWithOut()
const { componentData, canvasViewInfo, fullscreenFlag } = storeToRefs(dashboardStore)
const { componentData, canvasViewInfo, fullscreenFlag, baseMatrixCount } =
storeToRefs(dashboardStore)
const dataInitState = ref(true)
const state = reactive({
@@ -23,8 +24,26 @@ const state = reactive({
})
const dashboardEditorInnerRef = ref(null)
const addComponent = (componentType: string, viewInfo?: any) => {
const addComponents = (componentType: string, views?: any) => {
const component = cloneDeep(findNewComponentFromList(componentType))
// @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
component.x = findPositionX(component.sizeX)
if (views) {
views.forEach((view: any, index: number) => {
const target = cloneDeep(view)
delete target.chart.sourceType
if (index > 0) {
// @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
component.x = ((component.x + component?.sizeX - 1) % baseMatrixCount.value.x) + 1
}
addComponent(component, target)
})
} else {
addComponent(component)
}
}
const addComponent = (componentSource: any, viewInfo?: any) => {
const component = cloneDeep(componentSource)
if (component && dashboardEditorInnerRef.value) {
component.id = guid()
// add view
@@ -34,9 +53,7 @@ const addComponent = (componentType: string, viewInfo?: any) => {
dashboardStore.addCanvasViewInfo(viewInfo)
} else if (component.component === 'SQTab') {
const subTabName = guid('tab')
// @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
component.propValue[0].name = subTabName
// @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
component.propValue[0].title = t('dashboard.new_tab')
component.activeTabName = subTabName
}
@@ -85,12 +102,20 @@ const baseParams = computed(() => {
pid: state.routerPid,
}
})
const findPositionX = (width: number) => {
// @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
return dashboardEditorInnerRef.value.findPositionX(width)
}
</script>
<template>
<div class="editor-content" :class="{ 'editor-content-fullscreen': fullscreenFlag }">
<div class="editor-main">
<Toolbar :base-params="baseParams" @add-component="addComponent"></Toolbar>
<Toolbar
:base-params="baseParams"
:find-position-x="findPositionX"
@add-components="addComponents"
></Toolbar>
<DashboardEditor
v-if="dataInitState"
ref="dashboardEditorInnerRef"