feat: create chat first record

This commit is contained in:
ulleo
2025-06-30 16:50:24 +08:00
parent a1bf97aaea
commit 3d8c0431be
10 changed files with 185 additions and 229 deletions
@@ -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 ###
+25 -1
View File
@@ -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())
+7 -6
View File
@@ -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 = ""
+1
View File
@@ -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
+10 -5
View File
@@ -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
)
}
+4
View File
@@ -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>
+5 -20
View File
@@ -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>
-182
View File
@@ -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>
+5 -15
View File
@@ -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