mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
Merge branch 'main' of https://github.com/dataease/SQLBot
This commit is contained in:
@@ -15,7 +15,6 @@ from langchain.chat_models.base import BaseChatModel
|
||||
from langchain_community.utilities import SQLDatabase
|
||||
from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage, AIMessage, BaseMessageChunk
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import DBAPIError
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlmodel import create_engine, Session
|
||||
|
||||
@@ -30,13 +29,13 @@ from apps.chat.models.chat_model import ChatQuestion, ChatRecord, Chat, RenameCh
|
||||
from apps.datasource.crud.datasource import get_table_schema
|
||||
from apps.datasource.crud.permission import get_row_permission_filters, is_normal_user
|
||||
from apps.datasource.models.datasource import CoreDatasource
|
||||
from apps.db.db import exec_sql, get_version
|
||||
from apps.db.db import exec_sql, get_version, check_connection
|
||||
from apps.system.crud.assistant import AssistantOutDs, AssistantOutDsFactory, get_assistant_ds
|
||||
from apps.system.schemas.system_schema import AssistantOutDsSchema
|
||||
from apps.terminology.curd.terminology import get_terminology_template
|
||||
from common.core.config import settings
|
||||
from common.core.deps import CurrentAssistant, CurrentUser
|
||||
from common.error import SingleMessageError
|
||||
from common.error import SingleMessageError, SQLBotDBError, ParseSQLResultError, SQLBotDBConnectionError
|
||||
from common.utils.utils import SQLBotLogUtil, extract_nested_json, prepare_for_orjson
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
@@ -868,7 +867,14 @@ class LLMService:
|
||||
Query results
|
||||
"""
|
||||
SQLBotLogUtil.info(f"Executing SQL on ds_id {self.ds.id}: {sql}")
|
||||
return exec_sql(self.ds, sql)
|
||||
try:
|
||||
return exec_sql(self.ds, sql)
|
||||
except Exception as e:
|
||||
if isinstance(e, ParseSQLResultError):
|
||||
raise e
|
||||
else:
|
||||
err = traceback.format_exc(limit=1, chain=True)
|
||||
raise SQLBotDBError(err)
|
||||
|
||||
def pop_chunk(self):
|
||||
try:
|
||||
@@ -940,6 +946,12 @@ class LLMService:
|
||||
ds=self.ds)
|
||||
else:
|
||||
self.validate_history_ds()
|
||||
|
||||
# check connection
|
||||
connected = check_connection(ds=self.ds, trans=None)
|
||||
if not connected:
|
||||
raise SQLBotDBConnectionError('Connect DB failed')
|
||||
|
||||
# generate sql
|
||||
sql_res = self.generate_sql()
|
||||
full_sql_text = ''
|
||||
@@ -1059,13 +1071,12 @@ class LLMService:
|
||||
error_msg: str
|
||||
if isinstance(e, SingleMessageError):
|
||||
error_msg = str(e)
|
||||
elif isinstance(e, ConnectionError):
|
||||
elif isinstance(e, SQLBotDBConnectionError):
|
||||
error_msg = orjson.dumps(
|
||||
{'message': str(e), 'traceback': traceback.format_exc(limit=1),
|
||||
'type': 'db-connection-err'}).decode()
|
||||
elif isinstance(e, DBAPIError):
|
||||
{'message': str(e), 'type': 'db-connection-err'}).decode()
|
||||
elif isinstance(e, SQLBotDBError):
|
||||
error_msg = orjson.dumps(
|
||||
{'message': str(e), 'traceback': traceback.format_exc(limit=1), 'type': 'exec-sql-err'}).decode()
|
||||
{'message': 'Execute SQL Failed', 'traceback': str(e), 'type': 'exec-sql-err'}).decode()
|
||||
else:
|
||||
error_msg = orjson.dumps({'message': str(e), 'traceback': traceback.format_exc(limit=1)}).decode()
|
||||
self.save_error(message=error_msg)
|
||||
|
||||
@@ -3,8 +3,10 @@ import json
|
||||
import platform
|
||||
import urllib.parse
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
from apps.db.db_sql import get_table_sql, get_field_sql, get_version_sql
|
||||
from common.error import ParseSQLResultError
|
||||
|
||||
if platform.system() != "Darwin":
|
||||
import dmPython
|
||||
@@ -100,7 +102,7 @@ def get_session(ds: CoreDatasource | AssistantOutDsSchema):
|
||||
return session
|
||||
|
||||
|
||||
def check_connection(trans: Trans, ds: CoreDatasource, is_raise: bool = False):
|
||||
def check_connection(trans: Optional[Trans], ds: CoreDatasource, is_raise: bool = False):
|
||||
db = DB.get_db(ds.type)
|
||||
if db.connect_type == ConnectType.sqlalchemy:
|
||||
conn = get_engine(ds, 10)
|
||||
@@ -209,7 +211,8 @@ def get_schema(ds: CoreDatasource):
|
||||
if ds.type == "sqlServer":
|
||||
sql = f"""select name from sys.schemas"""
|
||||
elif ds.type == "pg" or ds.type == "excel":
|
||||
sql = """SELECT nspname FROM pg_namespace"""
|
||||
sql = """SELECT nspname
|
||||
FROM pg_namespace"""
|
||||
elif ds.type == "oracle":
|
||||
sql = f"""select * from all_users"""
|
||||
with session.execute(text(sql)) as result:
|
||||
@@ -325,7 +328,7 @@ def exec_sql(ds: CoreDatasource | AssistantOutDsSchema, sql: str, origin_column=
|
||||
return {"fields": columns, "data": result_list,
|
||||
"sql": bytes.decode(base64.b64encode(bytes(sql, 'utf-8')))}
|
||||
except Exception as ex:
|
||||
raise ex
|
||||
raise ParseSQLResultError(str(ex))
|
||||
else:
|
||||
conf = DatasourceConf(**json.loads(aes_decrypt(ds.configuration)))
|
||||
if ds.type == 'dm':
|
||||
@@ -345,7 +348,7 @@ def exec_sql(ds: CoreDatasource | AssistantOutDsSchema, sql: str, origin_column=
|
||||
return {"fields": columns, "data": result_list,
|
||||
"sql": bytes.decode(base64.b64encode(bytes(sql, 'utf-8')))}
|
||||
except Exception as ex:
|
||||
raise ex
|
||||
raise ParseSQLResultError(str(ex))
|
||||
elif ds.type == 'doris':
|
||||
with pymysql.connect(user=conf.username, passwd=conf.password, host=conf.host,
|
||||
port=conf.port, db=conf.database, connect_timeout=conf.timeout,
|
||||
@@ -364,7 +367,7 @@ def exec_sql(ds: CoreDatasource | AssistantOutDsSchema, sql: str, origin_column=
|
||||
return {"fields": columns, "data": result_list,
|
||||
"sql": bytes.decode(base64.b64encode(bytes(sql, 'utf-8')))}
|
||||
except Exception as ex:
|
||||
raise ex
|
||||
raise ParseSQLResultError(str(ex))
|
||||
elif ds.type == 'redshift':
|
||||
with redshift_connector.connect(host=conf.host, port=conf.port, database=conf.database, user=conf.username,
|
||||
password=conf.password,
|
||||
@@ -383,4 +386,4 @@ def exec_sql(ds: CoreDatasource | AssistantOutDsSchema, sql: str, origin_column=
|
||||
return {"fields": columns, "data": result_list,
|
||||
"sql": bytes.decode(base64.b64encode(bytes(sql, 'utf-8')))}
|
||||
except Exception as ex:
|
||||
raise ex
|
||||
raise ParseSQLResultError(str(ex))
|
||||
|
||||
+13
-1
@@ -4,4 +4,16 @@ class SingleMessageError(Exception):
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
return self.message
|
||||
|
||||
|
||||
class SQLBotDBConnectionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SQLBotDBError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ParseSQLResultError(Exception):
|
||||
pass
|
||||
|
||||
@@ -17,7 +17,7 @@ const showBlock = computed(() => {
|
||||
})
|
||||
|
||||
const errorMessage = computed(() => {
|
||||
const obj = { message: props.error, showMore: false, traceback: '', type: '' }
|
||||
const obj = { message: props.error, showMore: false, traceback: '', type: undefined }
|
||||
if (showBlock.value && props.error?.trim().startsWith('{') && props.error?.trim().endsWith('}')) {
|
||||
try {
|
||||
const json = JSON.parse(props.error?.trim())
|
||||
@@ -44,7 +44,7 @@ function showTraceBack() {
|
||||
<template>
|
||||
<div v-if="showBlock">
|
||||
<div
|
||||
v-if="!errorMessage.showMore"
|
||||
v-if="!errorMessage.showMore && errorMessage.type == undefined"
|
||||
v-dompurify-html="errorMessage.message"
|
||||
class="error-container"
|
||||
></div>
|
||||
@@ -58,7 +58,9 @@ function showTraceBack() {
|
||||
<template v-else>
|
||||
{{ t('chat.error') }}
|
||||
</template>
|
||||
<el-button text @click="showTraceBack">{{ t('chat.show_error_detail') }}</el-button>
|
||||
<el-button v-if="errorMessage.showMore" text @click="showTraceBack">
|
||||
{{ t('chat.show_error_detail') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-drawer
|
||||
|
||||
@@ -216,7 +216,7 @@ function addToDashboard() {
|
||||
// @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
const chartBaseInfo = JSON.parse(props.message?.record?.chart)
|
||||
recordeInfo['chart'] = {
|
||||
type: chartBaseInfo.type,
|
||||
type: currentChartType.value,
|
||||
title: chartBaseInfo.title,
|
||||
columns: chartBaseInfo.columns,
|
||||
xAxis: chartBaseInfo.axis?.x ? [chartBaseInfo.axis.x] : [],
|
||||
@@ -395,7 +395,7 @@ watch(
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
<div v-if="message?.record?.chart">
|
||||
<div v-if="message?.record?.chart && isCompletePage">
|
||||
<el-tooltip effect="dark" :content="t('chat.add_to_dashboard')" placement="top">
|
||||
<el-button class="tool-btn" text @click="addToDashboard">
|
||||
<el-icon size="16">
|
||||
|
||||
@@ -1063,6 +1063,8 @@ 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(() => {
|
||||
@@ -1166,6 +1168,49 @@ function tabMoveInCheckSQ() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find position box
|
||||
*/
|
||||
function findPositionX(item: CanvasItem) {
|
||||
const width = item.sizeX
|
||||
let resultX = 1
|
||||
let checkPointYIndex = -1 // -1 means not occupying any Y-direction canvas
|
||||
// Component width
|
||||
let pb = positionBox.value
|
||||
if (width <= 0) return
|
||||
// Find the highest position index of the component. Component rule: the latest y is 1.
|
||||
canvasComponentData.value.forEach((component) => {
|
||||
const componentYIndex = component.y + component.sizeY - 2
|
||||
if (checkPointYIndex < componentYIndex) {
|
||||
checkPointYIndex = componentYIndex
|
||||
}
|
||||
})
|
||||
// Start checking from index i in the X direction;
|
||||
const pbX = pb[checkPointYIndex]
|
||||
// Get the last column array in the X direction
|
||||
if (checkPointYIndex < 0 || !pbX) {
|
||||
return 1
|
||||
} else {
|
||||
// The width to check is the component width. The end index of the check is checkEndIndex = i + width - 1;
|
||||
// The exit condition for the check is when the end index checkEndIndex is out of bounds (exceeds the end index of pbX).
|
||||
for (let i = 0, checkEndIndex = width - 1; checkEndIndex < pbX.length; i++, checkEndIndex++) {
|
||||
let adaptorCount = 0
|
||||
// Locate the occupied position in the last column
|
||||
for (let k = 0; k < width; k++) {
|
||||
// pbX[i + k].el === false indicates that the current matrix point is not occupied. When the width of consecutive unoccupied matrix points equals the component width, the starting point i is available.
|
||||
if (!pbX[i + k].el) {
|
||||
adaptorCount++
|
||||
}
|
||||
}
|
||||
if (adaptorCount === width) {
|
||||
resultX = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
return resultX
|
||||
}
|
||||
}
|
||||
|
||||
useEmitt({
|
||||
name: `editor-delete-${props.canvasId}`,
|
||||
callback: removeItemById,
|
||||
|
||||
@@ -20,7 +20,6 @@ const optInit = (viewInfo: any) => {
|
||||
resourceDialogShow.value = true
|
||||
state.viewInfo = viewInfo
|
||||
}
|
||||
const curOptDashboardId = ref(null)
|
||||
const state = reactive({
|
||||
dashboardList: [] as SQTreeNode[],
|
||||
viewInfo: null,
|
||||
@@ -127,20 +126,19 @@ const saveResourcePrepare = () => {
|
||||
const saveResource = (params: any, commonParams: any) => {
|
||||
saveDashboardResourceTarget(params, commonParams, (res: any) => {
|
||||
const messageTips = t('dashboard.add_success')
|
||||
curOptDashboardId.value = res?.id
|
||||
openMessageLoading(messageTips, 'success', callbackExportSuc)
|
||||
openMessageLoading(messageTips, 'success', res?.id, callbackExportSuc)
|
||||
resetForm()
|
||||
})
|
||||
}
|
||||
|
||||
const callbackExportSuc = () => {
|
||||
const callbackExportSuc = (curOptDashboardIdValue: any) => {
|
||||
// do open dashboard
|
||||
const url = `#/canvas?resourceId=${curOptDashboardId.value}`
|
||||
const url = `#/canvas?resourceId=${curOptDashboardIdValue}`
|
||||
window.open(url, '_self')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
const openMessageLoading = (text: string, type = 'success', cb: Function) => {
|
||||
const openMessageLoading = (text: string, type = 'success', dvId: any, cb: Function) => {
|
||||
// success error loading
|
||||
const customClass = `sq-message-${type || 'success'} sq-message-export`
|
||||
ElMessage({
|
||||
@@ -161,7 +159,7 @@ const openMessageLoading = (text: string, type = 'success', cb: Function) => {
|
||||
size: 'small',
|
||||
class: 'btn-text',
|
||||
onClick: () => {
|
||||
cb()
|
||||
cb(dvId)
|
||||
},
|
||||
},
|
||||
t('dashboard.open_dashboard')
|
||||
@@ -169,7 +167,7 @@ const openMessageLoading = (text: string, type = 'success', cb: Function) => {
|
||||
]),
|
||||
type,
|
||||
showClose: true,
|
||||
duration: 0,
|
||||
duration: 2000,
|
||||
customClass,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -91,6 +91,10 @@ const editCanvasName = () => {
|
||||
nameInput.value?.focus()
|
||||
})
|
||||
}
|
||||
const handleEnterEditCanvasName = (event: Event) => {
|
||||
// @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
event.target?.blur()
|
||||
}
|
||||
const closeEditCanvasName = () => {
|
||||
nameEdit.value = false
|
||||
if (!inputName.value || !inputName.value.trim()) {
|
||||
@@ -240,6 +244,7 @@ const previewInner = () => {
|
||||
<input
|
||||
ref="nameInput"
|
||||
v-model="inputName"
|
||||
@keydown.enter="handleEnterEditCanvasName"
|
||||
@change="onDvNameChange"
|
||||
@blur="closeEditCanvasName"
|
||||
/>
|
||||
|
||||
@@ -40,12 +40,23 @@ const addComponent = (componentType: string, viewInfo?: any) => {
|
||||
component.propValue[0].title = t('dashboard.new_tab')
|
||||
component.activeTabName = subTabName
|
||||
}
|
||||
component.y = 100
|
||||
component.y = maxYComponentCount() + 10
|
||||
// @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
dashboardEditorInnerRef.value.addItemToBox(component)
|
||||
}
|
||||
}
|
||||
|
||||
const maxYComponentCount = () => {
|
||||
if (componentData.value.length === 0) {
|
||||
return 1
|
||||
} else {
|
||||
return componentData.value
|
||||
.filter((item) => item['y'])
|
||||
.map((item) => item['y'] + item['sizeY']) // Calculate the y+sizeY of each element
|
||||
.reduce((max, current) => Math.max(max, current), 0)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
state.opt = router.currentRoute.value.query.opt
|
||||
|
||||
@@ -199,7 +199,24 @@ const search = () => {
|
||||
}
|
||||
|
||||
const termFormRef = ref()
|
||||
|
||||
const validateUrl = (_: any, value: any, callback: any) => {
|
||||
if (value === '') {
|
||||
callback(
|
||||
new Error(
|
||||
t('datasource.please_enter') + t('common.empty') + t('embedded.cross_domain_settings')
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// var Expression = /(https?:\/\/)?([\da-z\.-]+)\.([a-z]{2,6})(:\d{1,5})?([\/\w\.-]*)*\/?(#[\S]+)?/ // eslint-disable-line
|
||||
var Expression = /^https?:\/\/[^\s/?#]+(:\d+)?/i
|
||||
var objExp = new RegExp(Expression)
|
||||
if (objExp.test(value) && !value.endsWith('/')) {
|
||||
callback()
|
||||
} else {
|
||||
callback(t('embedded.format_is_incorrect'))
|
||||
}
|
||||
}
|
||||
}
|
||||
const rules = {
|
||||
name: [
|
||||
{
|
||||
@@ -210,8 +227,8 @@ const rules = {
|
||||
domain: [
|
||||
{
|
||||
required: true,
|
||||
message:
|
||||
t('datasource.please_enter') + t('common.empty') + t('embedded.cross_domain_settings'),
|
||||
validator: validateUrl,
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -258,7 +258,8 @@ const validateUrl = (_: any, value: any, callback: any) => {
|
||||
)
|
||||
)
|
||||
} else {
|
||||
var Expression = /(https?:\/\/)?([\da-z\.-]+)\.([a-z]{2,6})(:\d{1,5})?([\/\w\.-]*)*\/?(#[\S]+)?/ // eslint-disable-line
|
||||
// var Expression = /(https?:\/\/)?([\da-z\.-]+)\.([a-z]{2,6})(:\d{1,5})?([\/\w\.-]*)*\/?(#[\S]+)?/ // eslint-disable-line
|
||||
var Expression = /^https?:\/\/[^\s/?#]+(:\d+)?/i
|
||||
var objExp = new RegExp(Expression)
|
||||
if (objExp.test(value) && !value.endsWith('/')) {
|
||||
callback()
|
||||
|
||||
@@ -9,7 +9,7 @@ SQLBOT_MCP_PORT=8001
|
||||
## 是否使用外部数据库
|
||||
SQLBOT_EXTERNAL_DB=false
|
||||
## 数据库地址
|
||||
SQLBOT_DB_HOST=sqlbot-db
|
||||
SQLBOT_DB_HOST=localhost
|
||||
## 数据库端口 (仅使用外部数据库时才生效)
|
||||
SQLBOT_DB_PORT=5432
|
||||
## SQLBot 数据库库名
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
services:
|
||||
sqlbot-db:
|
||||
image: registry.cn-qingdao.aliyuncs.com/dataease/postgres:17.6
|
||||
container_name: sqlbot-db
|
||||
restart: always
|
||||
networks:
|
||||
- sqlbot-network
|
||||
volumes:
|
||||
- ./data/postgresql:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- conf/sqlbot.conf
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready"]
|
||||
interval: 3s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -8,7 +8,6 @@ services:
|
||||
ports:
|
||||
- ${SQLBOT_WEB_PORT}:8000
|
||||
- ${SQLBOT_MCP_PORT}:8001
|
||||
- 5432:5432
|
||||
env_file:
|
||||
- conf/sqlbot.conf
|
||||
volumes:
|
||||
@@ -16,9 +15,6 @@ services:
|
||||
- ./data/sqlbot/images:/opt/sqlbot/images
|
||||
- ./data/sqlbot/logs:/opt/sqlbot/logs
|
||||
- ./data/postgresql:/var/lib/postgresql/data
|
||||
depends_on:
|
||||
sqlbot-db:
|
||||
condition: service_healthy
|
||||
|
||||
networks:
|
||||
sqlbot-network:
|
||||
|
||||
Reference in New Issue
Block a user