Merge remote-tracking branch 'origin/main'

This commit is contained in:
wangjiahao
2025-07-21 17:44:06 +08:00
12 changed files with 262 additions and 99 deletions
@@ -0,0 +1,33 @@
"""031_modify_chat_record
Revision ID: bd2ed188b5bd
Revises: c1d7ac00b3a8
Create Date: 2025-07-21 17:27:55.985821
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'bd2ed188b5bd'
down_revision = 'c1d7ac00b3a8'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('chat_record', 'engine_type',
existing_type=sa.VARCHAR(length=64),
nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('chat_record', 'engine_type',
existing_type=sa.VARCHAR(length=64),
nullable=False)
# ### end Alembic commands ###
+1 -1
View File
@@ -36,7 +36,7 @@ class ChatRecord(SQLModel, table=True):
finish_time: datetime = Field(sa_column=Column(DateTime(timezone=False), nullable=True))
create_by: int = Field(sa_column=Column(BigInteger, nullable=True))
datasource: int = Field(sa_column=Column(BigInteger, nullable=True))
engine_type: str = Field(max_length=64)
engine_type: str = Field(max_length=64, nullable=True)
question: str = Field(sa_column=Column(Text, nullable=True))
sql_answer: str = Field(sa_column=Column(Text, nullable=True))
sql: str = Field(sa_column=Column(Text, nullable=True))
+10 -8
View File
@@ -403,22 +403,24 @@ class LLMService:
if data['id'] and data['id'] != 0:
_datasource = data['id']
if self.current_assistant.type == 1:
_chat = self.session.get(Chat, self.record.chat_id)
_chat.datasource = _datasource
if self.current_assistant and self.current_assistant.type == 1:
_ds = self.out_ds_instance.get_ds(data['id'])
self.ds = _ds
self.chat_question.engine = _ds.type
_engine_type = self.chat_question.engine
_chat.engine_type = _ds.type
else:
_ds = self.session.get(CoreDatasource, _datasource)
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
self.chat_question.engine = _ds.type_name if _ds.type != 'excel' else 'PostgreSQL'
_engine_type = self.chat_question.engine
_chat.engine_type = _ds.type_name
# save chat
_chat = self.session.get(Chat, self.record.chat_id)
_chat.datasource = _datasource
_chat.engine_type = _ds.type_name
self.session.add(_chat)
self.session.flush()
self.session.refresh(_chat)
@@ -734,7 +736,7 @@ def run_task(llm_service: LLMService, in_chat: bool = True):
'type': 'datasource-result'}).decode() + '\n\n'
if in_chat:
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'
'engine_type': llm_service.ds.type_name or llm_service.ds.type, 'type': 'datasource'}).decode() + '\n\n'
llm_service.chat_question.db_schema = llm_service.out_ds_instance.get_db_schema() if llm_service.out_ds_instance else get_table_schema(session=llm_service.session, current_user=llm_service.current_user, ds=llm_service.ds)
+11 -4
View File
@@ -230,19 +230,26 @@ def updateField(session: SessionDep, field: CoreField):
def preview(session: SessionDep, id: int, data: TableObj):
if data.fields is None or len(data.fields) == 0:
return {"fields": [], "data": [], "sql": ''}
fields = [f.field_name for f in data.fields if f.checked]
if fields is None or len(fields) == 0:
return {"fields": [], "data": [], "sql": ''}
ds = session.query(CoreDatasource).filter(CoreDatasource.id == id).first()
conf = DatasourceConf(**json.loads(aes_decrypt(ds.configuration))) if ds.type != "excel" else get_engine_config()
sql: str = ""
if ds.type == "mysql":
sql = f"""SELECT `{"`, `".join([f.field_name for f in data.fields if f.checked])}` FROM `{data.table.table_name}` LIMIT 100"""
sql = f"""SELECT `{"`, `".join(fields)}` FROM `{data.table.table_name}` LIMIT 100"""
elif ds.type == "sqlServer":
sql = f"""SELECT [{"], [".join([f.field_name for f in data.fields if f.checked])}] FROM [{conf.dbSchema}].[{data.table.table_name}]
sql = f"""SELECT [{"], [".join(fields)}] FROM [{conf.dbSchema}].[{data.table.table_name}]
ORDER BY [{data.fields[0].field_name}]
OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY"""
elif ds.type == "pg" or ds.type == "excel":
sql = f"""SELECT "{'", "'.join([f.field_name for f in data.fields if f.checked])}" FROM "{conf.dbSchema}"."{data.table.table_name}" LIMIT 100"""
sql = f"""SELECT "{'", "'.join(fields)}" FROM "{conf.dbSchema}"."{data.table.table_name}" LIMIT 100"""
elif ds.type == "oracle":
sql = f"""SELECT "{'", "'.join([f.field_name for f in data.fields if f.checked])}" FROM "{conf.dbSchema}"."{data.table.table_name}"
sql = f"""SELECT "{'", "'.join(fields)}" FROM "{conf.dbSchema}"."{data.table.table_name}"
ORDER BY "{data.fields[0].field_name}"
OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY"""
return exec_sql(ds, sql)
+23 -8
View File
@@ -8,7 +8,7 @@ from sqlmodel import Session, select
from apps.datasource.models.datasource import CoreDatasource
from apps.system.models.system_model import AssistantModel
from apps.system.schemas.auth import CacheName, CacheNamespace
from apps.system.schemas.system_schema import UserInfoDTO
from apps.system.schemas.system_schema import AssistantOutDsSchema, UserInfoDTO
from common.core.sqlbot_cache import cache
from common.core.db import engine
from starlette.middleware.cors import CORSMiddleware
@@ -73,7 +73,7 @@ def init_dynamic_cors(app: FastAPI):
class AssistantOutDs:
assistant: AssistantModel
ds_list: Optional[list[dict]] = None
ds_list: Optional[list[AssistantOutDsSchema]] = None
certificate: Optional[str] = None
def __init__(self, assistant: AssistantModel, certificate: Optional[str] = None):
self.assistant = assistant
@@ -99,9 +99,11 @@ class AssistantOutDs:
result_json: dict[any] = json.loads(res.json())
if result_json.get('code') == 0:
temp_list = result_json.get('data', [])
for idx, item in enumerate(temp_list, start=1):
item["id"] = idx
self.ds_list = temp_list
self.ds_list = [
AssistantOutDsSchema(**{**item, "id": idx})
for idx, item in enumerate(temp_list, start=1)
]
return self.ds_list
else:
raise Exception(f"Failed to get datasource list from {endpoint}, error: {result_json.get('message')}")
@@ -110,12 +112,25 @@ class AssistantOutDs:
def get_simple_ds_list(self):
if self.ds_list:
return [{'id': ds['id'], 'name': ds['name'], 'description': ds['comment']} for ds in self.ds_list]
return [{'id': ds.id, 'name': ds.name, 'description': ds.comment} for ds in self.ds_list]
else:
raise Exception("Datasource list is not found.")
def get_db_schema(self, ds_id: int):
return None
def get_db_schema(self, ds_id: int) -> str:
ds = self.get_ds(ds_id)
schema_str = ""
db_name = ds.schema
schema_str += f"【DB_ID】 {db_name}\n【Schema】\n"
for table in ds.tables:
schema_str += f"# Table: {db_name}.{table.name}"
schema_str += f", {table.comment}\n[\n"
field_list = []
for field in table.fields:
field_list.append(f"({field.name}:{field.type}, {field.comment})")
schema_str += ",\n".join(field_list)
schema_str += '\n]\n'
return schema_str
def get_ds(self, ds_id: int):
if self.ds_list:
for ds in self.ds_list:
+50 -1
View File
@@ -101,4 +101,53 @@ class UserWs(BaseCreatorDTO):
name: str
class UserWsOption(UserWs):
account: str
account: str
class AssistantFieldSchema(BaseModel):
id: Optional[int] = None
name: Optional[str] = None
type: Optional[str] = None
comment: Optional[str] = None
class AssistantTableSchema(BaseModel):
id: Optional[int] = None
name: Optional[str] = None
comment: Optional[str] = None
rule: Optional[str] = None
sql: Optional[str] = None
fields: Optional[list[AssistantFieldSchema]] = None
class AssistantOutDsBase(BaseModel):
id: Optional[int] = None
name: str
type: Optional[str] = None
type_name: Optional[str] = None
comment: Optional[str] = None
description: Optional[str] = None
def __init__(self, id: Optional[int] = None, name: str = '', type: Optional[str] = None,
type_name: Optional[str] = None, comment: Optional[str] = None):
super().__init__(id=id, name=name, type=type, type_name=type_name, comment=comment)
class AssistantOutDsSchema(AssistantOutDsBase):
host: Optional[str] = None
port: Optional[int] = None
user: Optional[str] = None
password: Optional[str] = None
schema: Optional[str] = None
tables: Optional[list[AssistantTableSchema]] = None
def __init__(self, id: int, name: str, comment: Optional[str] = None, type: Optional[str] = None,
type_name: Optional[str] = None, host: Optional[str] = None, port: Optional[int] = None,
username: Optional[str] = None, password: Optional[str] = None, schema: Optional[str] = None):
self.id = id
self.name = name
self.comment = comment
self.type = type
self.type_name = type_name
self.host = host
self.port = port
self.username = username
self.password = password
self.schema = schema
self.description = comment
+102 -5
View File
@@ -1,18 +1,37 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, nextTick, ref } from 'vue'
import { endsWith, startsWith } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { chatApi, ChatInfo } from '@/api/chat.ts'
const props = withDefaults(
defineProps<{
recordId?: number
currentChat?: ChatInfo
questions?: string
firstChat?: boolean
}>(),
{
recordId: undefined,
currentChat: () => new ChatInfo(),
questions: '[]',
firstChat: false,
}
)
const emits = defineEmits(['clickQuestion', 'update:currentChat'])
const loading = ref(false)
const _currentChat = computed({
get() {
return props.currentChat
},
set(v) {
emits('update:currentChat', v)
},
})
const computedQuestions = computed<string>(() => {
if (
props.questions &&
@@ -25,20 +44,98 @@ const computedQuestions = computed<string>(() => {
return []
})
const emits = defineEmits(['clickQuestion'])
const { t } = useI18n()
function clickQuestion(question: string): void {
emits('clickQuestion', question)
}
async function getRecommendQuestions() {
loading.value = true
try {
const response = await chatApi.recommendQuestions(props.recordId)
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
const chunk = decoder.decode(value)
let _list = [chunk]
const lines = chunk.trim().split('}\n\n{')
if (lines.length > 1) {
_list = []
for (let line of lines) {
if (!line.trim().startsWith('{')) {
line = '{' + line.trim()
}
if (!line.trim().endsWith('}')) {
line = line.trim() + '}'
}
_list.push(line)
}
}
for (const str of _list) {
let data
try {
data = JSON.parse(str)
} catch (err) {
console.error('JSON string:', str)
throw err
}
if (data.code && data.code !== 200) {
ElMessage({
message: data.msg,
type: 'error',
showClose: true,
})
return
}
switch (data.type) {
case 'recommended_question':
if (
data.content &&
data.content.length > 0 &&
startsWith(data.content.trim(), '[') &&
endsWith(data.content.trim(), ']')
) {
if (_currentChat.value?.records) {
for (let record of _currentChat.value.records) {
if (record.id === props.recordId) {
record.recommended_question = data.content
await nextTick()
}
}
}
}
}
}
}
} finally {
loading.value = false
}
}
defineExpose({ getRecommendQuestions, id: () => props.recordId })
</script>
<template>
<div v-if="computedQuestions.length > 0" class="recommend-questions">
<div v-if="computedQuestions.length > 0 || loading" class="recommend-questions">
<div v-if="firstChat">{{ t('qa.guess_u_ask') }}</div>
<div v-else class="continue-ask">{{ t('qa.continue_to_ask') }}</div>
<div class="question-grid">
<div v-if="loading">
<el-button style="min-width: unset" type="primary" link loading />
</div>
<div v-else class="question-grid">
<div
v-for="(question, index) in computedQuestions"
:key="index"
@@ -5,9 +5,9 @@ import { computed, nextTick } from 'vue'
import MdComponent from '@/views/chat/component/MdComponent.vue'
const props = withDefaults(
defineProps<{
chatList: Array<ChatInfo>
chatList?: Array<ChatInfo>
currentChatId?: number
currentChat: ChatInfo
currentChat?: ChatInfo
message?: ChatMessage
loading?: boolean
}>(),
@@ -4,9 +4,9 @@ import { Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord, questionApi } fr
import { computed, nextTick } from 'vue'
const props = withDefaults(
defineProps<{
chatList: Array<ChatInfo>
chatList?: Array<ChatInfo>
currentChatId?: number
currentChat: ChatInfo
currentChat?: ChatInfo
message?: ChatMessage
loading?: boolean
reasoningName: 'sql_answer' | 'chart_answer' | Array<'sql_answer' | 'chart_answer'>
@@ -5,9 +5,9 @@ import { computed, nextTick } from 'vue'
import MdComponent from '@/views/chat/component/MdComponent.vue'
const props = withDefaults(
defineProps<{
chatList: Array<ChatInfo>
chatList?: Array<ChatInfo>
currentChatId?: number
currentChat: ChatInfo
currentChat?: ChatInfo
message?: ChatMessage
loading?: boolean
}>(),
@@ -193,8 +193,9 @@ function addToDashboard() {
<template>
<div
v-if="
(!isPredict && (message?.record?.sql || message?.record?.chart)) ||
(isPredict && message?.record?.chart && data.length > 0)
!message.isTyping &&
((!isPredict && (message?.record?.sql || message?.record?.chart)) ||
(isPredict && message?.record?.chart && data.length > 0))
"
class="chart-component-container"
:class="{ 'full-screen': enlarge }"
+23 -64
View File
@@ -93,6 +93,9 @@
<ChatRow :current-chat="currentChat" :msg="message" :hide-avatar="message.first_chat">
<RecommendQuestion
v-if="message.role === 'assistant' && message.first_chat"
ref="recommendQuestionRef"
:current-chat="currentChat"
:record-id="message.record?.id"
:questions="message.recommended_question"
:first-chat="message.first_chat"
@click-question="quickAsk"
@@ -181,6 +184,9 @@
</template>
<template #footer>
<RecommendQuestion
ref="recommendQuestionRef"
:current-chat="currentChat"
:record-id="message.record?.id"
:questions="message.recommended_question"
:first-chat="message.first_chat"
@click-question="quickAsk"
@@ -303,7 +309,7 @@ import ChatListContainer from './ChatListContainer.vue'
import ChatCreator from '@/views/chat/ChatCreator.vue'
import ChatToolBar from './ChatToolBar.vue'
import { useI18n } from 'vue-i18n'
import { endsWith, find, startsWith } from 'lodash-es'
import { find } from 'lodash-es'
import icon_new_chat_outlined from '@/assets/svg/icon_new_chat_outlined.svg'
import icon_sidebar_outlined from '@/assets/svg/icon_sidebar_outlined.svg'
import icon_replace_outlined from '@/assets/svg/icon_replace_outlined.svg'
@@ -373,6 +379,7 @@ const computedMessages = computed<Array<ChatMessage>>(() => {
})
}
console.log(messages)
return messages
})
@@ -465,72 +472,24 @@ function onChatCreated(chat: ChatInfo) {
}
}
async function getRecommendQuestions(record_id?: number) {
const response = await chatApi.recommendQuestions(record_id)
const reader = response.body.getReader()
const decoder = new TextDecoder()
const recommendQuestionRef = ref()
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
const chunk = decoder.decode(value)
let _list = [chunk]
const lines = chunk.trim().split('}\n\n{')
if (lines.length > 1) {
_list = []
for (let line of lines) {
if (!line.trim().startsWith('{')) {
line = '{' + line.trim()
}
if (!line.trim().endsWith('}')) {
line = line.trim() + '}'
}
_list.push(line)
}
}
for (const str of _list) {
let data
try {
data = JSON.parse(str)
} catch (err) {
console.error('JSON string:', str)
throw err
}
if (data.code && data.code !== 200) {
ElMessage({
message: data.msg,
type: 'error',
showClose: true,
})
return
}
switch (data.type) {
case 'recommended_question':
if (
data.content &&
data.content.length > 0 &&
startsWith(data.content.trim(), '[') &&
endsWith(data.content.trim(), ']')
) {
if (currentChat.value?.records) {
for (let record of currentChat.value.records) {
if (record.id === record_id) {
record.recommended_question = data.content
}
}
}
function getRecommendQuestions(id?: number) {
nextTick(() => {
if (recommendQuestionRef.value) {
if (recommendQuestionRef.value instanceof Array) {
for (let i = 0; i < recommendQuestionRef.value.length; i++) {
const _id = recommendQuestionRef.value[i].id()
if (_id === id) {
recommendQuestionRef.value[i].getRecommendQuestions()
break
}
}
} else {
recommendQuestionRef.value.getRecommendQuestions()
}
}
}
})
}
onMounted(() => {
@@ -549,7 +508,7 @@ const chartAnswerRef = ref()
async function onChartAnswerFinish(id: number) {
loading.value = false
isTyping.value = false
await getRecommendQuestions(id)
getRecommendQuestions(id)
}
function onChartAnswerError() {