feat: 统一工具调用的规范化处理,优化消息解析逻辑
This commit is contained in:
parent
5310fdaa09
commit
14daa43e28
@ -17,6 +17,7 @@ from yuxi.repositories.agent_repository import AgentRepository
|
|||||||
from yuxi.repositories.agent_run_repository import TERMINAL_RUN_STATUSES, AgentRunRepository
|
from yuxi.repositories.agent_run_repository import TERMINAL_RUN_STATUSES, AgentRunRepository
|
||||||
from yuxi.repositories.conversation_repository import ConversationRepository
|
from yuxi.repositories.conversation_repository import ConversationRepository
|
||||||
from yuxi.services.run_queue_service import (
|
from yuxi.services.run_queue_service import (
|
||||||
|
build_run_event_envelope,
|
||||||
get_arq_pool,
|
get_arq_pool,
|
||||||
get_last_run_stream_seq,
|
get_last_run_stream_seq,
|
||||||
list_run_stream_events,
|
list_run_stream_events,
|
||||||
@ -273,14 +274,13 @@ async def stream_agent_run_events(
|
|||||||
if terminal_seq in {"", "0-0"}:
|
if terminal_seq in {"", "0-0"}:
|
||||||
terminal_seq = None
|
terminal_seq = None
|
||||||
yield _format_sse(
|
yield _format_sse(
|
||||||
{
|
build_run_event_envelope(
|
||||||
"schema_version": 1,
|
run_id=run_id,
|
||||||
"run_id": run_id,
|
thread_id=run.thread_id,
|
||||||
"thread_id": run.thread_id,
|
event_type="end",
|
||||||
"event": "end",
|
payload={"status": run.status},
|
||||||
"payload": {"status": run.status},
|
created_at=utc_now_naive().isoformat(),
|
||||||
"created_at": utc_now_naive().isoformat(),
|
),
|
||||||
},
|
|
||||||
event="end",
|
event="end",
|
||||||
event_id=terminal_seq,
|
event_id=terminal_seq,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -55,6 +55,32 @@ def normalize_after_seq(after_seq: str | None) -> str:
|
|||||||
return "0-0"
|
return "0-0"
|
||||||
|
|
||||||
|
|
||||||
|
def build_run_event_envelope(
|
||||||
|
*,
|
||||||
|
run_id: str,
|
||||||
|
event_type: str,
|
||||||
|
payload: dict | None = None,
|
||||||
|
thread_id: str | None = None,
|
||||||
|
created_at: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
return {
|
||||||
|
"schema_version": 1,
|
||||||
|
"run_id": run_id,
|
||||||
|
"thread_id": thread_id,
|
||||||
|
"event": event_type,
|
||||||
|
"payload": payload or {},
|
||||||
|
"created_at": created_at or datetime.now(tz=UTC).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _payload_thread_id(payload: dict | None) -> str | None:
|
||||||
|
chunk = payload.get("chunk") if isinstance(payload, dict) else None
|
||||||
|
if not isinstance(chunk, dict):
|
||||||
|
return None
|
||||||
|
thread_id = chunk.get("thread_id")
|
||||||
|
return thread_id.strip() if isinstance(thread_id, str) and thread_id.strip() else None
|
||||||
|
|
||||||
|
|
||||||
async def get_redis_client():
|
async def get_redis_client():
|
||||||
global _redis_client
|
global _redis_client
|
||||||
if _redis_client is not None:
|
if _redis_client is not None:
|
||||||
@ -164,14 +190,14 @@ async def append_run_stream_event(run_id: str, event_type: str, payload: dict, *
|
|||||||
key = _event_stream_key(run_id)
|
key = _event_stream_key(run_id)
|
||||||
now = datetime.now(tz=UTC)
|
now = datetime.now(tz=UTC)
|
||||||
now_ms = int(now.timestamp() * 1000)
|
now_ms = int(now.timestamp() * 1000)
|
||||||
envelope = {
|
event_thread_id = thread_id or _payload_thread_id(payload)
|
||||||
"schema_version": 1,
|
envelope = build_run_event_envelope(
|
||||||
"run_id": run_id,
|
run_id=run_id,
|
||||||
"thread_id": thread_id,
|
event_type=event_type,
|
||||||
"event": event_type,
|
payload=payload or {},
|
||||||
"payload": payload or {},
|
thread_id=event_thread_id,
|
||||||
"created_at": now.isoformat(),
|
created_at=now.isoformat(),
|
||||||
}
|
)
|
||||||
fields = {
|
fields = {
|
||||||
"event_type": event_type,
|
"event_type": event_type,
|
||||||
"payload": json.dumps(envelope, ensure_ascii=False),
|
"payload": json.dumps(envelope, ensure_ascii=False),
|
||||||
|
|||||||
@ -97,7 +97,11 @@ async def test_run_stream_event_roundtrip(monkeypatch: pytest.MonkeyPatch):
|
|||||||
|
|
||||||
run_id = "run-1"
|
run_id = "run-1"
|
||||||
seq1 = await run_queue_service.append_run_stream_event(run_id, "loading", {"items": [1]})
|
seq1 = await run_queue_service.append_run_stream_event(run_id, "loading", {"items": [1]})
|
||||||
seq2 = await run_queue_service.append_run_stream_event(run_id, "finished", {"chunk": {"status": "finished"}})
|
seq2 = await run_queue_service.append_run_stream_event(
|
||||||
|
run_id,
|
||||||
|
"finished",
|
||||||
|
{"chunk": {"status": "finished", "thread_id": "child-thread"}},
|
||||||
|
)
|
||||||
|
|
||||||
assert seq1 < seq2
|
assert seq1 < seq2
|
||||||
|
|
||||||
@ -106,6 +110,7 @@ async def test_run_stream_event_roundtrip(monkeypatch: pytest.MonkeyPatch):
|
|||||||
assert events[0]["payload"]["schema_version"] == 1
|
assert events[0]["payload"]["schema_version"] == 1
|
||||||
assert events[0]["payload"]["run_id"] == run_id
|
assert events[0]["payload"]["run_id"] == run_id
|
||||||
assert events[0]["payload"]["payload"] == {"items": [1]}
|
assert events[0]["payload"]["payload"] == {"items": [1]}
|
||||||
|
assert events[1]["payload"]["thread_id"] == "child-thread"
|
||||||
|
|
||||||
next_events = await run_queue_service.list_run_stream_events(run_id, after_seq=seq1, limit=100)
|
next_events = await run_queue_service.list_run_stream_events(run_id, after_seq=seq1, limit=100)
|
||||||
assert len(next_events) == 1
|
assert len(next_events) == 1
|
||||||
|
|||||||
@ -67,7 +67,7 @@
|
|||||||
- 收敛用户身份命名:原业务登录标识统一改为 `uid`,Agent/LangGraph runtime、conversation、agent_run、sandbox 路径和前端用户态均使用字符串 `uid`;`user_id` 仅保留给外部响应中的数值 `users.id` 或真实外键场景。
|
- 收敛用户身份命名:原业务登录标识统一改为 `uid`,Agent/LangGraph runtime、conversation、agent_run、sandbox 路径和前端用户态均使用字符串 `uid`;`user_id` 仅保留给外部响应中的数值 `users.id` 或真实外键场景。
|
||||||
- 工作区知识库分类显示:知识库侧边栏按创建者分组为“我的知识库”和“共享知识库”,自己创建的知识库显示在“我的知识库”下,非自己创建的显示在“共享知识库”下;`knowledge_bases` 表新增 `created_by` 字段记录创建者 uid。
|
- 工作区知识库分类显示:知识库侧边栏按创建者分组为“我的知识库”和“共享知识库”,自己创建的知识库显示在“我的知识库”下,非自己创建的显示在“共享知识库”下;`knowledge_bases` 表新增 `created_by` 字段记录创建者 uid。
|
||||||
- 聊天附件新增 MinIO tmp 临时上传、可选 PDF/图片解析、确认后加入线程附件的流程;前端改为弹窗内上传、解析与确认。
|
- 聊天附件新增 MinIO tmp 临时上传、可选 PDF/图片解析、确认后加入线程附件的流程;前端改为弹窗内上传、解析与确认。
|
||||||
- 标准化 Agent run/SSE 执行链路:run 创建时持久化输入消息并提交后入队,worker 统一写入 Redis Stream envelope,SSE 输出 `event/data/id`、心跳注释、`Last-Event-ID` 回放和终止 `end` 事件;前端强制使用 run API 并支持 ask_user_question 中断后以 resume run 恢复。
|
- 标准化 Agent run/SSE 执行链路:run 创建时持久化输入消息并提交后入队,worker 统一写入 Redis Stream envelope,SSE 输出 `event/data/id`、心跳注释、`Last-Event-ID` 回放和终止 `end` 事件;前端强制使用 run API 并支持 ask_user_question 中断后以 resume run 恢复;事件 envelope 构造收敛到统一 helper,前端优先使用 envelope 一级 `thread_id` 路由。
|
||||||
- 收敛后端模块边界:文档解析从 `plugins.parser` 移动到 `knowledge.parser`,内容审查从 `plugins.guard` 移动到 `services.guard`。
|
- 收敛后端模块边界:文档解析从 `plugins.parser` 移动到 `knowledge.parser`,内容审查从 `plugins.guard` 移动到 `services.guard`。
|
||||||
- 收敛文件服务边界:文件预览判断抽为独立服务,Viewer 文件系统的 workspace 分支复用用户 workspace 服务,线程运行时上下文解析从泛化 `filesystem_service` 拆出为 agent runtime helper。
|
- 收敛文件服务边界:文件预览判断抽为独立服务,Viewer 文件系统的 workspace 分支复用用户 workspace 服务,线程运行时上下文解析从泛化 `filesystem_service` 拆出为 agent runtime helper。
|
||||||
- 升级 DeepAgents 到 0.6.7 并适配新版文件系统协议:SubAgentMiddleware 改为显式 subagent spec,Skills prompt 补齐新版占位符;sandbox/skills backend 复用新版 `ReadResult`、`GlobResult`、`GrepResult` 等协议类型,文件权限在 backend 层明确区分 skills、uploads、outputs 与 workspace,保留最小 `CustomCompositeBackend` 以避免非 route glob 误扫其他 route;Agent 上下文压缩改为复用 DeepAgents SummarizationMiddleware,历史摘要与大工具结果统一 offload 到 outputs。
|
- 升级 DeepAgents 到 0.6.7 并适配新版文件系统协议:SubAgentMiddleware 改为显式 subagent spec,Skills prompt 补齐新版占位符;sandbox/skills backend 复用新版 `ReadResult`、`GlobResult`、`GrepResult` 等协议类型,文件权限在 backend 层明确区分 skills、uploads、outputs 与 workspace,保留最小 `CustomCompositeBackend` 以避免非 route glob 误扫其他 route;Agent 上下文压缩改为复用 DeepAgents SummarizationMiddleware,历史摘要与大工具结果统一 offload 到 outputs。
|
||||||
|
|||||||
@ -45,10 +45,7 @@
|
|||||||
<div class="chat-box">
|
<div class="chat-box">
|
||||||
<template v-for="row in conversationRows" :key="row.key">
|
<template v-for="row in conversationRows" :key="row.key">
|
||||||
<div v-if="row.type === 'conversation'" class="conv-box">
|
<div v-if="row.type === 'conversation'" class="conv-box">
|
||||||
<template
|
<template v-for="(displayItem, itemIndex) in row.displayItems" :key="displayItem.key">
|
||||||
v-for="(displayItem, itemIndex) in getConversationDisplayItems(row.conv)"
|
|
||||||
:key="displayItem.key"
|
|
||||||
>
|
|
||||||
<AgentMessageComponent
|
<AgentMessageComponent
|
||||||
v-if="displayItem.type === 'message'"
|
v-if="displayItem.type === 'message'"
|
||||||
:message="displayItem.message"
|
:message="displayItem.message"
|
||||||
@ -62,9 +59,7 @@
|
|||||||
<ToolCallsGroupComponent
|
<ToolCallsGroupComponent
|
||||||
v-else
|
v-else
|
||||||
:tool-calls="displayItem.toolCalls"
|
:tool-calls="displayItem.toolCalls"
|
||||||
:is-active="
|
:is-active="isToolGroupActive(row.conv, itemIndex, row.displayItems)"
|
||||||
isToolGroupActive(row.conv, itemIndex, getConversationDisplayItems(row.conv))
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<AgentArtifactsCard
|
<AgentArtifactsCard
|
||||||
@ -366,6 +361,7 @@ import { useAgentMentionConfig } from '@/composables/useAgentMentionConfig'
|
|||||||
import AgentArtifactsCard from '@/components/AgentArtifactsCard.vue'
|
import AgentArtifactsCard from '@/components/AgentArtifactsCard.vue'
|
||||||
import AgentPanel from '@/components/AgentPanel.vue'
|
import AgentPanel from '@/components/AgentPanel.vue'
|
||||||
import AttachmentTmpUploadModal from '@/components/AttachmentTmpUploadModal.vue'
|
import AttachmentTmpUploadModal from '@/components/AttachmentTmpUploadModal.vue'
|
||||||
|
import { normalizeToolCalls } from '@/components/ToolCallingResult/toolRegistry'
|
||||||
|
|
||||||
// ==================== PROPS & EMITS ====================
|
// ==================== PROPS & EMITS ====================
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -821,7 +817,8 @@ const conversationRows = computed(() => {
|
|||||||
const rows = conversations.value.map((conv, index) => ({
|
const rows = conversations.value.map((conv, index) => ({
|
||||||
type: 'conversation',
|
type: 'conversation',
|
||||||
key: conv.status === 'streaming' ? 'ongoing-conversation' : `history-${index}`,
|
key: conv.status === 'streaming' ? 'ongoing-conversation' : `history-${index}`,
|
||||||
conv
|
conv,
|
||||||
|
displayItems: getConversationDisplayItems(conv)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (currentThreadConfigNotice.value) {
|
if (currentThreadConfigNotice.value) {
|
||||||
@ -1769,27 +1766,10 @@ const handleResizingChange = (isResizingState, clientX = 0) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== HELPER FUNCTIONS ====================
|
// ==================== HELPER FUNCTIONS ====================
|
||||||
const extractAssistantMessageBody = (message) => {
|
|
||||||
let content = typeof message?.content === 'string' ? message.content.trim() : ''
|
|
||||||
let reasoningContent = message?.additional_kwargs?.reasoning_content || ''
|
|
||||||
|
|
||||||
if (!reasoningContent && content) {
|
|
||||||
const thinkRegex = /<think>(.*?)<\/think>|<think>(.*?)$/s
|
|
||||||
const thinkMatch = content.match(thinkRegex)
|
|
||||||
|
|
||||||
if (thinkMatch) {
|
|
||||||
reasoningContent = (thinkMatch[1] || thinkMatch[2] || '').trim()
|
|
||||||
content = content.replace(thinkMatch[0], '').trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { content, reasoningContent }
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasVisibleAssistantBody = (message) => {
|
const hasVisibleAssistantBody = (message) => {
|
||||||
if (!message || message.type !== 'ai') return true
|
if (!message || message.type !== 'ai') return true
|
||||||
|
|
||||||
const { content, reasoningContent } = extractAssistantMessageBody(message)
|
const { content, reasoningContent } = MessageProcessor.parseAssistantMessageBody(message)
|
||||||
return Boolean(
|
return Boolean(
|
||||||
content ||
|
content ||
|
||||||
reasoningContent ||
|
reasoningContent ||
|
||||||
@ -1800,19 +1780,8 @@ const hasVisibleAssistantBody = (message) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getMessageToolCalls = (message) => {
|
const getMessageToolCalls = (message) => {
|
||||||
if (!Array.isArray(message?.tool_calls)) return []
|
return normalizeToolCalls(message?.tool_calls, {
|
||||||
|
mapToolCall: (toolCall) => {
|
||||||
return message.tool_calls
|
|
||||||
.filter((toolCall) => {
|
|
||||||
return (
|
|
||||||
toolCall &&
|
|
||||||
(toolCall.id || toolCall.name || toolCall.function?.name) &&
|
|
||||||
(toolCall.args !== undefined ||
|
|
||||||
toolCall.function?.arguments !== undefined ||
|
|
||||||
toolCall.tool_call_result !== undefined)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.map((toolCall) => {
|
|
||||||
const subagentRun = toolCall.id ? currentSubagentRunById.value.get(String(toolCall.id)) : null
|
const subagentRun = toolCall.id ? currentSubagentRunById.value.get(String(toolCall.id)) : null
|
||||||
if (!subagentRun) return toolCall
|
if (!subagentRun) return toolCall
|
||||||
|
|
||||||
@ -1821,7 +1790,8 @@ const getMessageToolCalls = (message) => {
|
|||||||
subagent_run: subagentRun,
|
subagent_run: subagentRun,
|
||||||
display_label: subagentRun.subagent_name || subagentRun.subagent_type || undefined
|
display_label: subagentRun.subagent_name || subagentRun.subagent_type || undefined
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将 AI 消息拆成“正文块”和“工具块”,再跨消息合并相邻工具块。
|
// 将 AI 消息拆成“正文块”和“工具块”,再跨消息合并相邻工具块。
|
||||||
|
|||||||
@ -148,6 +148,7 @@ import { storeToRefs } from 'pinia'
|
|||||||
import { MessageProcessor } from '@/utils/messageProcessor'
|
import { MessageProcessor } from '@/utils/messageProcessor'
|
||||||
import { normalizeAttachmentPreviews } from '@/utils/file_utils'
|
import { normalizeAttachmentPreviews } from '@/utils/file_utils'
|
||||||
import { buildMentionDisplayLabels } from '@/utils/mention_utils'
|
import { buildMentionDisplayLabels } from '@/utils/mention_utils'
|
||||||
|
import { normalizeToolCalls } from '@/components/ToolCallingResult/toolRegistry'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
// 消息角色:'user'|'assistant'|'sent'|'received'
|
// 消息角色:'user'|'assistant'|'sent'|'received'
|
||||||
@ -281,50 +282,13 @@ const messageSources = computed(() => {
|
|||||||
return { knowledgeChunks: [], webSources: [] }
|
return { knowledgeChunks: [], webSources: [] }
|
||||||
})
|
})
|
||||||
|
|
||||||
// 过滤有效的工具调用
|
const validToolCalls = computed(() => normalizeToolCalls(props.message.tool_calls))
|
||||||
const validToolCalls = computed(() => {
|
|
||||||
if (!props.message.tool_calls || !Array.isArray(props.message.tool_calls)) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.message.tool_calls.filter((toolCall) => {
|
|
||||||
// 过滤掉无效的工具调用
|
|
||||||
return (
|
|
||||||
toolCall &&
|
|
||||||
(toolCall.id || toolCall.name || toolCall.function?.name) &&
|
|
||||||
(toolCall.args !== undefined ||
|
|
||||||
toolCall.function?.arguments !== undefined ||
|
|
||||||
toolCall.tool_call_result !== undefined)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const parsedData = computed(() => {
|
const parsedData = computed(() => {
|
||||||
// Start with default values from the prop to avoid mutation.
|
const { content, reasoningContent } = MessageProcessor.parseAssistantMessageBody(props.message)
|
||||||
let content = props.message.content.trim() || ''
|
|
||||||
let reasoning_content = props.message.additional_kwargs?.reasoning_content || ''
|
|
||||||
|
|
||||||
if (reasoning_content) {
|
|
||||||
return {
|
|
||||||
content,
|
|
||||||
reasoning_content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regex to find <think>...</think> or an unclosed <think>... at the end of the string.
|
|
||||||
const thinkRegex = /<think>(.*?)<\/think>|<think>(.*?)$/s
|
|
||||||
const thinkMatch = content.match(thinkRegex)
|
|
||||||
|
|
||||||
if (thinkMatch) {
|
|
||||||
// The captured reasoning is in either group 1 (closed tag) or 2 (unclosed tag).
|
|
||||||
reasoning_content = (thinkMatch[1] || thinkMatch[2] || '').trim()
|
|
||||||
// Remove the entire matched <think> block from the original content.
|
|
||||||
content = content.replace(thinkMatch[0], '').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content,
|
content,
|
||||||
reasoning_content
|
reasoning_content: reasoningContent
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -41,7 +41,7 @@ import MysqlDescribeTableTool from './tools/MysqlDescribeTableTool.vue'
|
|||||||
import MysqlListTablesTool from './tools/MysqlListTablesTool.vue'
|
import MysqlListTablesTool from './tools/MysqlListTablesTool.vue'
|
||||||
import AskUserQuestionTool from './tools/AskUserQuestionTool.vue'
|
import AskUserQuestionTool from './tools/AskUserQuestionTool.vue'
|
||||||
import ExecuteTool from './tools/ExecuteTool.vue'
|
import ExecuteTool from './tools/ExecuteTool.vue'
|
||||||
import { getToolCallId, HIDDEN_TOOL_CALL_IDS } from './toolRegistry'
|
import { getToolCallId, isHiddenToolCall } from './toolRegistry'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
toolCall: {
|
toolCall: {
|
||||||
@ -91,7 +91,7 @@ const TOOL_RENDERERS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentRenderer = computed(() => TOOL_RENDERERS[toolId.value] || null)
|
const currentRenderer = computed(() => TOOL_RENDERERS[toolId.value] || null)
|
||||||
const isHidden = computed(() => HIDDEN_TOOL_CALL_IDS.includes(toolId.value))
|
const isHidden = computed(() => isHiddenToolCall(props.toolCall))
|
||||||
|
|
||||||
const toolRendererRef = ref(null)
|
const toolRendererRef = ref(null)
|
||||||
const refreshGraph = () => {
|
const refreshGraph = () => {
|
||||||
|
|||||||
@ -53,4 +53,27 @@ export const HIDDEN_TOOL_CALL_IDS = ['present_artifacts']
|
|||||||
|
|
||||||
export const getToolCallId = (toolCall) => toolCall?.name || toolCall?.function?.name || ''
|
export const getToolCallId = (toolCall) => toolCall?.name || toolCall?.function?.name || ''
|
||||||
|
|
||||||
|
export const isHiddenToolCall = (toolCall) => HIDDEN_TOOL_CALL_IDS.includes(getToolCallId(toolCall))
|
||||||
|
|
||||||
|
export const isValidToolCall = (toolCall) => {
|
||||||
|
return Boolean(
|
||||||
|
toolCall &&
|
||||||
|
(toolCall.id || toolCall.name || toolCall.function?.name) &&
|
||||||
|
(toolCall.args !== undefined ||
|
||||||
|
toolCall.function?.arguments !== undefined ||
|
||||||
|
toolCall.tool_call_result !== undefined)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeToolCalls = (toolCalls, { includeHidden = false, mapToolCall } = {}) => {
|
||||||
|
if (!Array.isArray(toolCalls)) return []
|
||||||
|
|
||||||
|
return toolCalls
|
||||||
|
.filter((toolCall) => {
|
||||||
|
if (!isValidToolCall(toolCall)) return false
|
||||||
|
return includeHidden || !isHiddenToolCall(toolCall)
|
||||||
|
})
|
||||||
|
.map((toolCall) => (mapToolCall ? mapToolCall(toolCall) : toolCall))
|
||||||
|
}
|
||||||
|
|
||||||
export const getToolIcon = (toolId) => TOOL_ICON_MAP[toolId] || null
|
export const getToolIcon = (toolId) => TOOL_ICON_MAP[toolId] || null
|
||||||
|
|||||||
@ -42,7 +42,7 @@
|
|||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { ChevronDown, ChevronRight, Wrench } from 'lucide-vue-next'
|
import { ChevronDown, ChevronRight, Wrench } from 'lucide-vue-next'
|
||||||
import { ToolCallRenderer } from '@/components/ToolCallingResult'
|
import { ToolCallRenderer } from '@/components/ToolCallingResult'
|
||||||
import { getToolCallId, HIDDEN_TOOL_CALL_IDS } from '@/components/ToolCallingResult/toolRegistry'
|
import { getToolCallId, normalizeToolCalls } from '@/components/ToolCallingResult/toolRegistry'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
toolCalls: {
|
toolCalls: {
|
||||||
@ -55,20 +55,7 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const normalizedToolCalls = computed(() => {
|
const normalizedToolCalls = computed(() => normalizeToolCalls(props.toolCalls))
|
||||||
return (props.toolCalls || []).filter((toolCall) => {
|
|
||||||
const toolId = getToolCallId(toolCall)
|
|
||||||
|
|
||||||
return (
|
|
||||||
toolCall &&
|
|
||||||
!HIDDEN_TOOL_CALL_IDS.includes(toolId) &&
|
|
||||||
(toolCall.id || toolCall.name || toolCall.function?.name) &&
|
|
||||||
(toolCall.args !== undefined ||
|
|
||||||
toolCall.function?.arguments !== undefined ||
|
|
||||||
toolCall.tool_call_result !== undefined)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const shouldCollapseToolCalls = computed(() => normalizedToolCalls.value.length > 0)
|
const shouldCollapseToolCalls = computed(() => normalizedToolCalls.value.length > 0)
|
||||||
const areToolCallsExpanded = ref(false)
|
const areToolCallsExpanded = ref(false)
|
||||||
|
|||||||
@ -55,9 +55,9 @@ const getThreadIdFromObject = (value) => {
|
|||||||
|
|
||||||
const resolveChunkThreadId = ({ envelope, payload, chunk, fallbackThreadId }) => {
|
const resolveChunkThreadId = ({ envelope, payload, chunk, fallbackThreadId }) => {
|
||||||
return (
|
return (
|
||||||
getThreadIdFromObject(chunk) ||
|
|
||||||
getThreadIdFromObject(payload) ||
|
|
||||||
getThreadIdFromObject(envelope) ||
|
getThreadIdFromObject(envelope) ||
|
||||||
|
getThreadIdFromObject(payload) ||
|
||||||
|
getThreadIdFromObject(chunk) ||
|
||||||
fallbackThreadId
|
fallbackThreadId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -109,6 +109,12 @@ const run = () => {
|
|||||||
assert.equal(conversations[0].messages.at(-1).isLast, true)
|
assert.equal(conversations[0].messages.at(-1).isLast, true)
|
||||||
assert.equal(conversations[0].status, 'finished')
|
assert.equal(conversations[0].status, 'finished')
|
||||||
|
|
||||||
|
const assistantBody = MessageProcessor.parseAssistantMessageBody({
|
||||||
|
type: 'ai',
|
||||||
|
content: '<think>推理过程</think>最终答案'
|
||||||
|
})
|
||||||
|
assert.deepEqual(assistantBody, { content: '最终答案', reasoningContent: '推理过程' })
|
||||||
|
|
||||||
console.log('messageProcessor extractKnowledgeChunksFromConversation: all assertions passed')
|
console.log('messageProcessor extractKnowledgeChunksFromConversation: all assertions passed')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -283,6 +283,28 @@ export class MessageProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析助手消息正文与推理内容,保持渲染和列表拆分使用同一套规则。
|
||||||
|
* @param {Object} message - AI 消息对象
|
||||||
|
* @returns {{content: string, reasoningContent: string}}
|
||||||
|
*/
|
||||||
|
static parseAssistantMessageBody(message) {
|
||||||
|
let content = typeof message?.content === 'string' ? message.content.trim() : ''
|
||||||
|
let reasoningContent = message?.additional_kwargs?.reasoning_content || ''
|
||||||
|
|
||||||
|
if (!reasoningContent && content) {
|
||||||
|
const thinkRegex = /<think>(.*?)<\/think>|<think>(.*?)$/s
|
||||||
|
const thinkMatch = content.match(thinkRegex)
|
||||||
|
|
||||||
|
if (thinkMatch) {
|
||||||
|
reasoningContent = (thinkMatch[1] || thinkMatch[2] || '').trim()
|
||||||
|
content = content.replace(thinkMatch[0], '').trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { content, reasoningContent }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 合并消息块
|
* 合并消息块
|
||||||
* @param {Array} chunks - 消息块数组
|
* @param {Array} chunks - 消息块数组
|
||||||
@ -398,96 +420,6 @@ export class MessageProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理流式响应数据块
|
|
||||||
* @param {Object} data - 响应数据
|
|
||||||
* @param {Object} onGoingConv - 进行中的对话对象
|
|
||||||
* @param {Object} state - 状态对象
|
|
||||||
* @param {Function} getAgentHistory - 获取历史记录函数
|
|
||||||
* @param {Function} handleError - 错误处理函数
|
|
||||||
*/
|
|
||||||
static async processResponseChunk(data, onGoingConv, state, getAgentHistory, handleError) {
|
|
||||||
try {
|
|
||||||
switch (data.status) {
|
|
||||||
case 'init':
|
|
||||||
// 代表服务端收到请求并返回第一个响应
|
|
||||||
state.waitingServerResponse = false
|
|
||||||
onGoingConv.msgChunks[data.request_id] = [data.msg]
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'loading':
|
|
||||||
if (data.msg.id) {
|
|
||||||
if (!onGoingConv.msgChunks[data.msg.id]) {
|
|
||||||
onGoingConv.msgChunks[data.msg.id] = []
|
|
||||||
}
|
|
||||||
onGoingConv.msgChunks[data.msg.id].push(data.msg)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'error':
|
|
||||||
console.error('流式处理出错:', data.message)
|
|
||||||
handleError(new Error(data.message), 'stream')
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'finished':
|
|
||||||
await getAgentHistory()
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.warn('未知的响应状态:', data.status)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, 'stream')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理流式响应
|
|
||||||
* @param {Response} response - 响应对象
|
|
||||||
* @param {Function} processChunk - 处理块的函数
|
|
||||||
* @param {Function} scrollToBottom - 滚动到底部函数
|
|
||||||
* @param {Function} handleError - 错误处理函数
|
|
||||||
*/
|
|
||||||
static async handleStreamResponse(response, processChunk, scrollToBottom, handleError) {
|
|
||||||
try {
|
|
||||||
const reader = response.body.getReader()
|
|
||||||
let buffer = ''
|
|
||||||
const decoder = new TextDecoder()
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read()
|
|
||||||
if (done) break
|
|
||||||
|
|
||||||
buffer += decoder.decode(value, { stream: true })
|
|
||||||
const lines = buffer.split('\n')
|
|
||||||
buffer = lines.pop() || '' // 保留最后一行可能不完整的内容
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.trim()) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(line.trim())
|
|
||||||
await processChunk(data)
|
|
||||||
} catch (e) {
|
|
||||||
console.debug('解析JSON出错:', e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await scrollToBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理缓冲区中可能剩余的内容
|
|
||||||
if (buffer.trim()) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(buffer.trim())
|
|
||||||
await processChunk(data)
|
|
||||||
} catch {
|
|
||||||
console.warn('最终缓冲区内容无法解析:', buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, 'stream')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MessageProcessor
|
export default MessageProcessor
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user