feat: 优化聊天代理功能,增加时间成本计算;调整对话状态管理,确保消息流处理更流畅;更新样式以提升用户体验。

This commit is contained in:
Wenjie Zhang 2025-10-15 02:37:42 +08:00
parent a92cc54675
commit 578682abfa
4 changed files with 92 additions and 82 deletions

View File

@ -123,6 +123,7 @@ async def chat_agent(
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""使用特定智能体进行对话(需要登录)""" """使用特定智能体进行对话(需要登录)"""
start_time = asyncio.get_event_loop().time()
logger.info(f"agent_id: {agent_id}, query: {query}, config: {config}, meta: {meta}") logger.info(f"agent_id: {agent_id}, query: {query}, config: {config}, meta: {meta}")
@ -319,6 +320,7 @@ async def chat_agent(
yield make_chunk(message="检测到敏感内容,已中断输出", status="error") yield make_chunk(message="检测到敏感内容,已中断输出", status="error")
return return
meta["time_cost"] = asyncio.get_event_loop().time() - start_time
yield make_chunk(status="finished", meta=meta) yield make_chunk(status="finished", meta=meta)
# After streaming finished, save all messages from LangGraph state # After streaming finished, save all messages from LangGraph state

View File

@ -2,20 +2,20 @@
/* color palette from <https://github.com/vuejs/theme> */ /* color palette from <https://github.com/vuejs/theme> */
/* https://material-foundation.github.io/material-theme-builder/ */ /* https://material-foundation.github.io/material-theme-builder/ */
:root { :root {
--main-1000: #01151f; /* 更深沉的深蓝黑 */ --main-1000: #01151f;
--main-900: #023944; /* 深海蓝 */ --main-900: #023944;
--main-800: #035065; /* 深蓝绿 */ --main-800: #035065;
--main-700: #046a82; /* 增加明度的蓝青色 */ --main-700: #046a82;
--main-600: #24839a; /* 冷色调中略带灰 */ --main-600: #24839a;
--main-500: #3996ae; /* 平衡且主色调友好 */ --main-500: #3996ae;
--main-400: #5faec2; /* 更加柔和的中亮蓝 */ --main-400: #5faec2;
--main-300: #82c3d6; /* 明亮通透,视觉清爽 */ --main-300: #82c3d6;
--main-200: #a3d8e8; /* 比原先更清澈淡蓝 */ --main-200: #a3d8e8;
--main-100: #c4eaf5; /* 接近原色,但稍微柔和 */ --main-100: #c4eaf5;
--main-50: #e1f6fb; /* 极浅天蓝,适合背景过渡 */ --main-50: #e1f6fb;
--main-40: #eaf3f5; /* 淡灰蓝调 */ --main-40: #eaf3f5;
--main-30: #f2fbfd; /* 几近白的蓝调提示 */ --main-30: #f7fbfd;
--main-20: #f5f9fa; /* 比原配色更均匀些 */ --main-20: #f6f9fa;
--main-10: #fafcfd; --main-10: #fafcfd;
--main-5: #fcfefe; --main-5: #fcfefe;
--main-1: #fefefe; --main-1: #fefefe;
@ -23,21 +23,21 @@
--gray-10000: #000000; /* 保持纯黑 */ --gray-10000: #000000; /* 保持纯黑 */
--gray-2000: #0d0d0d; /* 更精致的暗灰,用于极深背景 */ --gray-2000: #0c0d0d; /* 更精致的暗灰,用于极深背景 */
--gray-1000: #161616; /* 深灰黑,适用于主文本 */ --gray-1000: #151616; /* 深灰黑,适用于主文本 */
--gray-900: #1f1f1f; --gray-900: #1e1f1f;
--gray-800: #333333; /* 标准深灰,适合背景 */ --gray-800: #323333; /* 标准深灰,适合背景 */
--gray-700: #4d4d4d; --gray-700: #4c4d4d;
--gray-600: #707070; /* 调和中灰,适合次要文本 */ --gray-600: #697070; /* 调和中灰,适合次要文本 */
--gray-500: #999999; /* 更柔和的中灰色 */ --gray-500: #979999; /* 更柔和的中灰色 */
--gray-400: #bfbfbf; --gray-400: #bdbfbf;
--gray-300: #d9d9d9; /* 修正了原300/200重复 */ --gray-300: #d7d9d9; /* 修正了原300/200重复 */
--gray-200: #e6e6e6; /* 稍微加深一点,便于分层 */ --gray-200: #e4e6e6; /* 稍微加深一点,便于分层 */
--gray-150: #f0f0f0; --gray-150: #eef0f0;
--gray-100: #f2f2f2; --gray-100: #eff2f2;
--gray-50: #f7f7f7; --gray-50: #f5f7f7;
--gray-25: #fafafa; --gray-25: #f8fafa;
--gray-10: #fcfcfc; --gray-10: #fafcfc;
--gray-0: #ffffff; --gray-0: #ffffff;
--main-color: #016179; --main-color: #016179;

View File

@ -244,13 +244,26 @@ const onGoingConvMessages = computed(() => {
const conversations = computed(() => { const conversations = computed(() => {
const historyConvs = MessageProcessor.convertServerHistoryToMessages(currentThreadMessages.value); const historyConvs = MessageProcessor.convertServerHistoryToMessages(currentThreadMessages.value);
if (onGoingConvMessages.value.length > 0) { const threadState = currentThreadState.value;
// 线
if (onGoingConvMessages.value.length > 0 && threadState?.isStreaming) {
const onGoingConv = { const onGoingConv = {
messages: onGoingConvMessages.value, messages: onGoingConvMessages.value,
status: 'streaming' status: 'streaming'
}; };
return [...historyConvs, onGoingConv]; return [...historyConvs, onGoingConv];
} }
// 使
if (historyConvs.length === 0 && onGoingConvMessages.value.length > 0 && !threadState?.isStreaming) {
const finalConv = {
messages: onGoingConvMessages.value,
status: 'finished'
};
return [finalConv];
}
return historyConvs; return historyConvs;
}); });
@ -322,7 +335,7 @@ const cleanupThreadState = (threadId) => {
}; };
// ==================== STREAM HANDLING LOGIC ==================== // ==================== STREAM HANDLING LOGIC ====================
const resetOnGoingConv = (threadId = null) => { const resetOnGoingConv = (threadId = null, preserveMessages = false) => {
if (threadId) { if (threadId) {
// 线 // 线
const threadState = getThreadState(threadId); const threadState = getThreadState(threadId);
@ -331,8 +344,18 @@ const resetOnGoingConv = (threadId = null) => {
threadState.streamAbortController.abort(); threadState.streamAbortController.abort();
threadState.streamAbortController = null; threadState.streamAbortController = null;
} }
//
if (preserveMessages) {
//
setTimeout(() => {
if (threadState.onGoingConv) {
threadState.onGoingConv = { msgChunks: {} }; threadState.onGoingConv = { msgChunks: {} };
} }
}, 100);
} else {
threadState.onGoingConv = { msgChunks: {} };
}
}
} else { } else {
// 线线 // 线线
const targetThreadId = currentChatId.value; const targetThreadId = currentChatId.value;
@ -343,8 +366,16 @@ const resetOnGoingConv = (threadId = null) => {
threadState.streamAbortController.abort(); threadState.streamAbortController.abort();
threadState.streamAbortController = null; threadState.streamAbortController = null;
} }
if (preserveMessages) {
setTimeout(() => {
if (threadState.onGoingConv) {
threadState.onGoingConv = { msgChunks: {} }; threadState.onGoingConv = { msgChunks: {} };
} }
}, 100);
} else {
threadState.onGoingConv = { msgChunks: {} };
}
}
} else { } else {
// 线线 // 线线
Object.keys(chatState.threadStates).forEach(tid => { Object.keys(chatState.threadStates).forEach(tid => {
@ -390,13 +421,26 @@ const _processStreamChunk = (chunk, threadId) => {
resetOnGoingConv(threadId); resetOnGoingConv(threadId);
return true; return true;
case 'finished': case 'finished':
fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId }); //
resetOnGoingConv(threadId); if (threadState) {
threadState.isStreaming = false;
}
//
fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId })
.finally(() => {
//
resetOnGoingConv(threadId, true);
});
return true; return true;
case 'interrupted': case 'interrupted':
// //
fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId }); if (threadState) {
resetOnGoingConv(threadId); threadState.isStreaming = false;
}
fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId })
.finally(() => {
resetOnGoingConv(threadId, true);
});
return true; return true;
} }

View File

@ -1,6 +1,7 @@
<template> <template>
<div class="refs" v-if="showRefs"> <div class="refs" v-if="showRefs">
<div class="tags"> <div class="tags">
<!-- 反馈 -->
<span <span
class="item btn" class="item btn"
:class="{ 'disabled': feedbackState.hasSubmitted }" :class="{ 'disabled': feedbackState.hasSubmitted }"
@ -19,35 +20,20 @@
<DislikeFilled v-if="feedbackState.rating === 'dislike'" /> <DislikeFilled v-if="feedbackState.rating === 'dislike'" />
<DislikeOutlined v-else /> <DislikeOutlined v-else />
</span> </span>
<!-- 模型名称 -->
<span v-if="showKey('model') && getModelName(msg)" class="item" @click="console.log(msg)"> <span v-if="showKey('model') && getModelName(msg)" class="item" @click="console.log(msg)">
<BulbOutlined /> {{ getModelName(msg) }} <BulbOutlined /> {{ getModelName(msg) }}
</span> </span>
<!-- 复制 -->
<span <span
v-if="showKey('copy')" v-if="showKey('copy')"
class="item btn" @click="copyText(msg.content)" title="复制"><CopyOutlined /></span> class="item btn" @click="copyText(msg.content)" title="复制"><CopyOutlined />
</span>
<!-- 重试 -->
<span <span
v-if="showKey('regenerate')" v-if="showKey('regenerate')"
class="item btn" @click="regenerateMessage()" title="重新生成"><ReloadOutlined /></span> class="item btn" @click="regenerateMessage()" title="重新生成"><ReloadOutlined />
<span
v-if="isLatestMessage && showKey('subGraph') && hasSubGraphData(msg)"
class="item btn"
@click="openGlobalRefs('graph')"
>
<DeploymentUnitOutlined /> 关系图
</span>
<span
class="item btn"
v-if="isLatestMessage && showKey('webSearch') && msg.refs?.web_search.results.length > 0"
@click="openGlobalRefs('webSearch')"
>
<GlobalOutlined /> 网页搜索 {{ msg.refs.web_search?.results.length }}
</span>
<span
class="item btn"
v-if="isLatestMessage && showKey('knowledgeBase') && hasKnowledgeBaseData(msg)"
@click="openGlobalRefs('knowledgeBase')"
>
<FileTextOutlined /> 知识库
</span> </span>
</div> </div>
</div> </div>
@ -77,10 +63,7 @@ import { ref, computed, reactive, onMounted } from 'vue'
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { import {
GlobalOutlined,
FileTextOutlined,
CopyOutlined, CopyOutlined,
DeploymentUnitOutlined,
BulbOutlined, BulbOutlined,
ReloadOutlined, ReloadOutlined,
LikeOutlined, LikeOutlined,
@ -152,25 +135,6 @@ const showRefs = computed(() => {
return (msg.value.role=='received' || msg.value.role=='assistant') && msg.value.status=='finished'; return (msg.value.role=='received' || msg.value.role=='assistant') && msg.value.status=='finished';
}) })
// refs
const openGlobalRefs = (type) => {
emit('openRefs', {
type,
refs: msg.value.refs
})
}
const hasSubGraphData = (msg) => {
return msg.refs &&
msg.refs.graph_base &&
msg.refs.graph_base.results.nodes.length > 0;
}
const hasKnowledgeBaseData = (msg) => {
return msg.refs &&
msg.refs.knowledge_base &&
msg.refs.knowledge_base.results.length > 0;
}
// //
const regenerateMessage = () => { const regenerateMessage = () => {