mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 09:17:06 +08:00
feat: create chat first record
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""018_modify_chat
|
||||
|
||||
Revision ID: 863105882eba
|
||||
Revises: a0ba8268868d
|
||||
Create Date: 2025-06-30 16:44:17.123791
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel.sql.sqltypes
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '863105882eba'
|
||||
down_revision = 'a0ba8268868d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('chat', 'id',
|
||||
existing_type=sa.INTEGER(),
|
||||
type_=sa.BigInteger(),
|
||||
existing_nullable=False,
|
||||
autoincrement=True,
|
||||
existing_server_default=sa.Identity(always=True, start=1, increment=1, minvalue=1, maxvalue=2147483647, cycle=False, cache=1))
|
||||
op.alter_column('chat', 'datasource',
|
||||
existing_type=sa.INTEGER(),
|
||||
type_=sa.BigInteger(),
|
||||
existing_nullable=True)
|
||||
op.alter_column('chat_record', 'id',
|
||||
existing_type=sa.INTEGER(),
|
||||
type_=sa.BigInteger(),
|
||||
existing_nullable=False,
|
||||
autoincrement=True,
|
||||
existing_server_default=sa.Identity(always=True, start=1, increment=1, minvalue=1, maxvalue=2147483647, cycle=False, cache=1))
|
||||
op.alter_column('chat_record', 'chat_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
type_=sa.BigInteger(),
|
||||
existing_nullable=False)
|
||||
op.alter_column('chat_record', 'ai_modal_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
type_=sa.BigInteger(),
|
||||
existing_nullable=True)
|
||||
op.alter_column('chat_record', 'datasource',
|
||||
existing_type=sa.INTEGER(),
|
||||
type_=sa.BigInteger(),
|
||||
existing_nullable=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('chat_record', 'datasource',
|
||||
existing_type=sa.BigInteger(),
|
||||
type_=sa.INTEGER(),
|
||||
existing_nullable=True)
|
||||
op.alter_column('chat_record', 'ai_modal_id',
|
||||
existing_type=sa.BigInteger(),
|
||||
type_=sa.INTEGER(),
|
||||
existing_nullable=True)
|
||||
op.alter_column('chat_record', 'chat_id',
|
||||
existing_type=sa.BigInteger(),
|
||||
type_=sa.INTEGER(),
|
||||
existing_nullable=False)
|
||||
op.alter_column('chat_record', 'id',
|
||||
existing_type=sa.BigInteger(),
|
||||
type_=sa.INTEGER(),
|
||||
existing_nullable=False,
|
||||
autoincrement=True,
|
||||
existing_server_default=sa.Identity(always=True, start=1, increment=1, minvalue=1, maxvalue=2147483647, cycle=False, cache=1))
|
||||
op.alter_column('chat', 'datasource',
|
||||
existing_type=sa.BigInteger(),
|
||||
type_=sa.INTEGER(),
|
||||
existing_nullable=True)
|
||||
op.alter_column('chat', 'id',
|
||||
existing_type=sa.BigInteger(),
|
||||
type_=sa.INTEGER(),
|
||||
existing_nullable=False,
|
||||
autoincrement=True,
|
||||
existing_server_default=sa.Identity(always=True, start=1, increment=1, minvalue=1, maxvalue=2147483647, cycle=False, cache=1))
|
||||
# ### end Alembic commands ###
|
||||
@@ -61,7 +61,7 @@ def get_chat_with_records(session: SessionDep, chart_id: int, current_user: Curr
|
||||
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.recommended_question, ChatRecord.first_chat,
|
||||
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()
|
||||
|
||||
@@ -111,6 +111,29 @@ def create_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj:
|
||||
chat_info.datasource_exists = True
|
||||
chat_info.datasource_name = ds.name
|
||||
|
||||
if require_datasource and ds:
|
||||
# generate first empty record
|
||||
record = ChatRecord()
|
||||
record.chat_id = chat.id
|
||||
record.datasource = ds.id
|
||||
record.engine_type = ds.type_name
|
||||
record.first_chat = True
|
||||
record.finish = True
|
||||
record.create_time = datetime.datetime.now()
|
||||
record.create_by = current_user.id
|
||||
|
||||
_record = ChatRecord(**record.model_dump())
|
||||
|
||||
session.add(record)
|
||||
session.flush()
|
||||
session.refresh(record)
|
||||
_record.id = record.id
|
||||
session.commit()
|
||||
|
||||
# todo suggest questions
|
||||
|
||||
chat_info.records.append(_record)
|
||||
|
||||
return chat_info
|
||||
|
||||
|
||||
@@ -131,6 +154,7 @@ def save_question(session: SessionDep, current_user: CurrentUser, question: Chat
|
||||
record.create_by = current_user.id
|
||||
record.datasource = chat.datasource
|
||||
record.engine_type = chat.engine_type
|
||||
record.ai_modal_id = question.ai_modal_id
|
||||
|
||||
result = ChatRecord(**record.model_dump())
|
||||
|
||||
|
||||
@@ -15,25 +15,25 @@ from apps.template.select_datasource.generator import get_datasource_template
|
||||
|
||||
class Chat(SQLModel, table=True):
|
||||
__tablename__ = "chat"
|
||||
id: Optional[int] = Field(sa_column=Column(Integer, Identity(always=True), primary_key=True))
|
||||
id: Optional[int] = Field(sa_column=Column(BigInteger, Identity(always=True), primary_key=True))
|
||||
create_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True))
|
||||
create_by: int = Field(sa_column=Column(BigInteger, nullable=True))
|
||||
brief: str = Field(max_length=64, nullable=True)
|
||||
chat_type: str = Field(max_length=20, default="chat") # chat, datasource
|
||||
datasource: int = Field(sa_column=Column(Integer, nullable=True))
|
||||
datasource: int = Field(sa_column=Column(BigInteger, nullable=True))
|
||||
engine_type: str = Field(max_length=64)
|
||||
|
||||
|
||||
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, nullable=False))
|
||||
ai_modal_id: Optional[int] = Field(sa_column=Column(Integer))
|
||||
id: Optional[int] = Field(sa_column=Column(BigInteger, Identity(always=True), primary_key=True))
|
||||
chat_id: int = Field(sa_column=Column(BigInteger, nullable=False))
|
||||
ai_modal_id: Optional[int] = Field(sa_column=Column(BigInteger))
|
||||
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=True))
|
||||
datasource: int = Field(sa_column=Column(BigInteger, 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))
|
||||
@@ -90,6 +90,7 @@ class ChatInfo(BaseModel):
|
||||
|
||||
|
||||
class AiModelQuestion(BaseModel):
|
||||
ai_modal_id: int = None
|
||||
engine: str = ""
|
||||
db_schema: str = ""
|
||||
sql: str = ""
|
||||
|
||||
@@ -39,6 +39,7 @@ class LLMService:
|
||||
ds: CoreDatasource = None):
|
||||
self.ds = ds
|
||||
self.chat_question = chat_question
|
||||
self.chat_question.ai_modal_id = aimodel.id
|
||||
self.config = get_llm_config(aimodel)
|
||||
|
||||
# Create LLM instance through factory
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface ChatMessage {
|
||||
content?: string | number
|
||||
record?: ChatRecord
|
||||
isTyping?: boolean
|
||||
isWelcome?: boolean
|
||||
first_chat?: boolean
|
||||
}
|
||||
|
||||
export class ChatRecord {
|
||||
@@ -42,6 +42,7 @@ export class ChatRecord {
|
||||
finish?: boolean = false
|
||||
error?: string
|
||||
run_time: number = 0
|
||||
first_chat: boolean = false
|
||||
|
||||
constructor()
|
||||
constructor(
|
||||
@@ -60,7 +61,8 @@ export class ChatRecord {
|
||||
predict_data: string | undefined,
|
||||
finish: boolean,
|
||||
error: string | undefined,
|
||||
run_time: number
|
||||
run_time: number,
|
||||
first_chat: boolean
|
||||
)
|
||||
constructor(
|
||||
id?: number,
|
||||
@@ -78,7 +80,8 @@ export class ChatRecord {
|
||||
predict_data?: string,
|
||||
finish?: boolean,
|
||||
error?: string,
|
||||
run_time?: number
|
||||
run_time?: number,
|
||||
first_chat?: boolean
|
||||
) {
|
||||
this.id = id
|
||||
this.chat_id = chat_id
|
||||
@@ -93,9 +96,10 @@ export class ChatRecord {
|
||||
this.analysis = analysis
|
||||
this.predict = predict
|
||||
this.predict_data = predict_data
|
||||
this.finish = finish
|
||||
this.finish = !!finish
|
||||
this.error = error
|
||||
this.run_time = run_time ?? 0
|
||||
this.first_chat = !!first_chat
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +218,8 @@ function toChatRecord(data?: any): ChatRecord | undefined {
|
||||
data.predict_data,
|
||||
data.finish,
|
||||
data.error,
|
||||
data.run_time
|
||||
data.run_time,
|
||||
data.first_chat
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -217,6 +217,10 @@ class HttpService {
|
||||
if (token) {
|
||||
heads['X-SQLBOT-TOKEN'] = `Bearer ${token}`
|
||||
}
|
||||
// @ts-ignore
|
||||
const request_key = LicenseGenerator.generate()
|
||||
heads['X-SQLBOT-KEY'] = request_key
|
||||
|
||||
const real_url = import.meta.env.VITE_API_BASE_URL
|
||||
return fetch(real_url + url, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import ChatBlock from './ChatBlock.vue'
|
||||
import { ChatInfo, type ChatMessage } from '@/api/chat.ts'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const props = defineProps<{
|
||||
msg: ChatMessage
|
||||
currentChat: ChatInfo
|
||||
}>()
|
||||
|
||||
const show = computed(() => {
|
||||
return props.msg.first_chat
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ChatBlock v-if="show">
|
||||
<div class="welcome-content">
|
||||
<div class="ds-select-row">
|
||||
<div>{{ t('qa.selected_datasource') }}:</div>
|
||||
</div>
|
||||
<div>
|
||||
{{ currentChat.datasource_name }}
|
||||
</div>
|
||||
</div>
|
||||
</ChatBlock>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.welcome-content {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sub {
|
||||
color: grey;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.ds-select-row {
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,31 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import ChatBlock from './ChatBlock.vue'
|
||||
import WelcomeBlock from './WelcomeBlock.vue'
|
||||
import ChatRecordFirst from './ChatRecordFirst.vue'
|
||||
import { ChatInfo, type ChatMessage } from '@/api/chat.ts'
|
||||
import { computed } from 'vue'
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
|
||||
const props = withDefaults(
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
msg: ChatMessage
|
||||
currentChat: ChatInfo
|
||||
datasource?: number
|
||||
}>(),
|
||||
{
|
||||
datasource: undefined,
|
||||
}
|
||||
{}
|
||||
)
|
||||
|
||||
const emits = defineEmits(['update:datasource'])
|
||||
|
||||
const _datasource = computed({
|
||||
get() {
|
||||
return props.datasource
|
||||
},
|
||||
set(v) {
|
||||
emits('update:datasource', v)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -36,13 +21,13 @@ const _datasource = computed({
|
||||
<UserFilled />
|
||||
</el-icon>
|
||||
</el-avatar>
|
||||
<ChatBlock v-if="!msg.isWelcome" :msg="msg" :class="{ 'row-full': msg.role === 'assistant' }">
|
||||
<ChatBlock v-if="!msg.first_chat" :msg="msg" :class="{ 'row-full': msg.role === 'assistant' }">
|
||||
<slot></slot>
|
||||
<template #footer>
|
||||
<slot name="footer"></slot>
|
||||
</template>
|
||||
</ChatBlock>
|
||||
<WelcomeBlock v-else v-model="_datasource" :current-chat="currentChat" class="row-full" />
|
||||
<ChatRecordFirst v-else :current-chat="currentChat" :msg="msg" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import ChatBlock from './ChatBlock.vue'
|
||||
import { ChatInfo } from '@/api/chat.ts'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { datasourceApi } from '@/api/datasource.ts'
|
||||
import DatasourceItemCard from '../ds/DatasourceItemCard.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: number
|
||||
currentChat: ChatInfo
|
||||
}>(),
|
||||
{
|
||||
modelValue: undefined,
|
||||
}
|
||||
)
|
||||
|
||||
const dsList = ref<Array<any>>([])
|
||||
|
||||
const editable = computed(() => {
|
||||
return props.currentChat?.id === undefined
|
||||
})
|
||||
|
||||
const ds = computed(() => {
|
||||
for (let i = 0; i < dsList.value.length; i++) {
|
||||
const item = dsList.value[i]
|
||||
if (props.modelValue === item.id) {
|
||||
return item
|
||||
}
|
||||
}
|
||||
if (props.currentChat && props.currentChat.datasource !== undefined) {
|
||||
return {
|
||||
id: props.currentChat.datasource,
|
||||
name: props.currentChat.datasource_name ?? 'Datasource does not exist',
|
||||
type: props.currentChat.engine_type,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
|
||||
function selectDs(ds: any) {
|
||||
if (editable.value) {
|
||||
emits('update:modelValue', ds.id)
|
||||
}
|
||||
}
|
||||
|
||||
function listDs() {
|
||||
datasourceApi.list().then((res) => {
|
||||
dsList.value = res
|
||||
})
|
||||
}
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
|
||||
const DatasourceListRef = ref()
|
||||
|
||||
const innerDs = ref()
|
||||
|
||||
function showDs() {
|
||||
innerDs.value = props.modelValue
|
||||
listDs()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function selectDsInDialog(ds: any) {
|
||||
innerDs.value = ds.id
|
||||
}
|
||||
|
||||
function confirmSelectDs() {
|
||||
if (innerDs.value) {
|
||||
emits('update:modelValue', innerDs.value)
|
||||
dialogVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
listDs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ChatBlock>
|
||||
<div class="welcome-content">
|
||||
<div>{{ t('qa.greeting') }}</div>
|
||||
<div class="sub">{{ t('qa.description') }}</div>
|
||||
<template v-if="editable">
|
||||
<template v-if="dsList.length > 0">
|
||||
<div class="ds-select-row">
|
||||
<div>{{ t('qa.select_datasource') }}</div>
|
||||
<el-button link type="primary" @click="showDs">{{ t('qa.view_more') }}</el-button>
|
||||
</div>
|
||||
<div class="ds-row-container">
|
||||
<template v-for="(item, _index) in dsList" :key="_index">
|
||||
<DatasourceItemCard
|
||||
v-if="_index < 3 || item?.id === modelValue"
|
||||
:ds="item"
|
||||
class="ds-card"
|
||||
:class="[item?.id === modelValue ? 'ds-card-selected' : '']"
|
||||
@click="selectDs(item)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>
|
||||
{{ t('qa.empty_datasource') }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="ds-select-row">
|
||||
<div>{{ t('qa.selected_datasource') }}</div>
|
||||
</div>
|
||||
<div class="ds-row-container">
|
||||
<DatasourceItemCard :ds="ds" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<el-drawer
|
||||
ref="DatasourceListRef"
|
||||
v-model="dialogVisible"
|
||||
title="Choose Datasource"
|
||||
direction="btt"
|
||||
size="100%"
|
||||
>
|
||||
<el-scrollbar>
|
||||
<div class="ds-row-container">
|
||||
<template v-for="(item, _index) in dsList" :key="_index">
|
||||
<DatasourceItemCard
|
||||
:ds="item"
|
||||
class="ds-card"
|
||||
:class="[item?.id === innerDs ? 'ds-card-selected' : '']"
|
||||
@click="selectDsInDialog(item)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="confirmSelectDs"> Confirm </el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</ChatBlock>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.welcome-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
color: grey;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.ds-select-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.ds-row-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.ds-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ds-card-selected {
|
||||
box-shadow: 0 1px 3px var(--ed-color-primary-light-5);
|
||||
border: 1px solid var(--ed-color-primary-light-5);
|
||||
}
|
||||
</style>
|
||||
@@ -39,11 +39,7 @@
|
||||
</div>
|
||||
<el-scrollbar v-if="computedMessages.length > 0" ref="chatListRef">
|
||||
<template v-for="(message, _index) in computedMessages" :key="_index">
|
||||
<ChatRow
|
||||
v-model:datasource="currentChat.datasource"
|
||||
:current-chat="currentChat"
|
||||
:msg="message"
|
||||
>
|
||||
<ChatRow :current-chat="currentChat" :msg="message">
|
||||
<template v-if="message.role === 'assistant'">
|
||||
<ChatAnswer v-slot="{ data }" :message="message">
|
||||
<template v-if="message.record?.chart">
|
||||
@@ -172,21 +168,13 @@ const isAnalysisTyping = ref<boolean>(false)
|
||||
const isPredictTyping = ref<boolean>(false)
|
||||
|
||||
const computedMessages = computed<Array<ChatMessage>>(() => {
|
||||
const firstMessage: ChatMessage = {
|
||||
role: 'assistant',
|
||||
create_time: currentChat.value?.create_time,
|
||||
content: currentChat.value?.datasource,
|
||||
isTyping: false,
|
||||
isWelcome: true,
|
||||
}
|
||||
const messages: Array<ChatMessage> = []
|
||||
if (currentChatId.value === undefined) {
|
||||
return messages
|
||||
}
|
||||
messages.push(firstMessage)
|
||||
for (let i = 0; i < currentChat.value.records.length; i++) {
|
||||
const record = currentChat.value.records[i]
|
||||
if (record.question !== undefined) {
|
||||
if (record.question !== undefined && !record.first_chat) {
|
||||
messages.push({
|
||||
role: 'user',
|
||||
create_time: record.create_time,
|
||||
@@ -198,9 +186,12 @@ const computedMessages = computed<Array<ChatMessage>>(() => {
|
||||
create_time: record.create_time,
|
||||
record: record,
|
||||
isTyping: i === currentChat.value.records.length - 1 && isTyping.value,
|
||||
first_chat: record.first_chat,
|
||||
})
|
||||
}
|
||||
|
||||
console.log(messages)
|
||||
|
||||
return messages
|
||||
})
|
||||
|
||||
@@ -283,7 +274,6 @@ onMounted(() => {
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!inputMessage.value.trim()) return
|
||||
if (computedMessages.value[0].content === undefined) return
|
||||
|
||||
loading.value = true
|
||||
isTyping.value = true
|
||||
|
||||
Reference in New Issue
Block a user