diff --git a/Dockerfile b/Dockerfile index 930b6350..a33a28e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/Dockerfile-base b/Dockerfile-base index 259e560c..89d2b711 100644 --- a/Dockerfile-base +++ b/Dockerfile-base @@ -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/* \ diff --git a/README.md b/README.md index ea21a73c..f28a3624 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/backend/apps/chat/curd/chat.py b/backend/apps/chat/curd/chat.py index cebcc223..01081553 100644 --- a/backend/apps/chat/curd/chat.py +++ b/backend/apps/chat/curd/chat.py @@ -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: diff --git a/backend/apps/chat/models/chat_model.py b/backend/apps/chat/models/chat_model.py index 0057e9cd..eb418251 100644 --- a/backend/apps/chat/models/chat_model.py +++ b/backend/apps/chat/models/chat_model.py @@ -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 diff --git a/backend/apps/chat/task/llm.py b/backend/apps/chat/task/llm.py index b99275d8..0237a4c3 100644 --- a/backend/apps/chat/task/llm.py +++ b/backend/apps/chat/task/llm.py @@ -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''' +{last_execute_sql_error} +''' + else: + self.chat_question.error_msg = '' + self.init_messages() @classmethod diff --git a/backend/common/core/config.py b/backend/common/core/config.py index 920ee73f..ac0eb8d7 100644 --- a/backend/common/core/config.py +++ b/backend/common/core/config.py @@ -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%40mysql@127.0.0.1:3306/sqlbot' diff --git a/backend/common/core/sqlbot_cache.py b/backend/common/core/sqlbot_cache.py index 6c827207..15018019 100644 --- a/backend/common/core/sqlbot_cache.py +++ b/backend/common/core/sqlbot_cache.py @@ -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 未启用缓存, 可使用多进程模式") - \ No newline at end of file + + +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 \ No newline at end of file diff --git a/backend/template.yaml b/backend/template.yaml index 5d16992c..dd182300 100644 --- a/backend/template.yaml +++ b/backend/template.yaml @@ -98,7 +98,7 @@ template: - 柱状图或折线图:适合展示在横轴的字段优先排序,若SQL包含分类字段,则分类字段次一级排序 - 如果用户没有指定数据条数的限制,输出的查询SQL需要加上1000条的数据条数限制 + 如果用户没有指定数据条数的限制,输出的查询SQL必须加上1000条的数据条数限制 如果用户指定的限制大于1000,则按1000处理 以PostgreSQL为例,查询Schema为TEST表TABLE下id字段,则生成的SQL为: @@ -235,6 +235,7 @@ template: {current_time} + {error_msg} {question} diff --git a/docker-compose.yaml b/docker-compose.yaml index 71b177df..73bd99ca 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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: diff --git a/frontend/src/stores/dashboard/dashboard.ts b/frontend/src/stores/dashboard/dashboard.ts index 2504e784..6e593680 100644 --- a/frontend/src/stores/dashboard/dashboard.ts +++ b/frontend/src/stores/dashboard/dashboard.ts @@ -13,6 +13,10 @@ export const dashboardStore = defineStore('dashboard', { canvasViewInfo: {}, fullscreenFlag: false, dataPrepareState: false, + baseMatrixCount: { + x: 72, + y: 36, + }, dashboardInfo: { id: null, name: null, diff --git a/frontend/src/views/dashboard/canvas/CanvasCore.vue b/frontend/src/views/dashboard/canvas/CanvasCore.vue index e574ebb6..dac6b9e3 100644 --- a/frontend/src/views/dashboard/canvas/CanvasCore.vue +++ b/frontend/src/views/dashboard/canvas/CanvasCore.vue @@ -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, }) diff --git a/frontend/src/views/dashboard/editor/DashboardEditor.vue b/frontend/src/views/dashboard/editor/DashboardEditor.vue index d4a4a49a..6f7fd5db 100644 --- a/frontend/src/views/dashboard/editor/DashboardEditor.vue +++ b/frontend/src/views/dashboard/editor/DashboardEditor.vue @@ -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(() => { diff --git a/frontend/src/views/dashboard/editor/Toolbar.vue b/frontend/src/views/dashboard/editor/Toolbar.vue index 4dc96031..74830acd 100644 --- a/frontend/src/views/dashboard/editor/Toolbar.vue +++ b/frontend/src/views/dashboard/editor/Toolbar.vue @@ -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')" > diff --git a/frontend/src/views/dashboard/editor/index.vue b/frontend/src/views/dashboard/editor/index.vue index 98c80b3e..f5167e09 100644 --- a/frontend/src/views/dashboard/editor/index.vue +++ b/frontend/src/views/dashboard/editor/index.vue @@ -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) +}