fix: 修复前端部分的问题
This commit is contained in:
parent
6de12aa954
commit
897e55e672
|
|
@ -112,7 +112,16 @@ const allItems = computed(() => {
|
|||
} else if (step.type === 'tool_result') {
|
||||
// 合并 tool_result 到对应的 tool_call
|
||||
const toolId = step.id_ref || step.id
|
||||
const match = items.findLast(it => it.type === 'tool_call' && it.id === toolId)
|
||||
// Use find() instead of findLast() for better compatibility
|
||||
// Search in reverse order to find the most recent matching tool_call
|
||||
let match = null
|
||||
for (let j = items.length - 1; j >= 0; j--) {
|
||||
if (items[j].type === 'tool_call' && items[j].id === toolId) {
|
||||
match = items[j]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (match) {
|
||||
let resultContent = step.content || ''
|
||||
let displayContent = resultContent
|
||||
|
|
|
|||
|
|
@ -123,7 +123,10 @@ class ParallelStreamManager {
|
|||
break
|
||||
|
||||
case 'agent_error':
|
||||
store.errorAgentStream(roomId, data.agent_id || data.agentId, data.error)
|
||||
store.errorAgentStream(roomId, data.agent_id || data.agentId, {
|
||||
message: data.error,
|
||||
agentName: data.agent_name || data.agentName
|
||||
})
|
||||
break
|
||||
|
||||
case 'parallel_end':
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const roomsLoading = ref(false)
|
|||
const providers = ref([])
|
||||
const showCreate = ref(false)
|
||||
const creating = ref(false)
|
||||
const newRoom = ref({ title: '', task: '', max_rounds: 5, agent_ids: [] })
|
||||
const newRoom = ref({ title: '', task: '', max_rounds: 5, agent_ids: [], execution_mode: 'sequential' })
|
||||
const showAddToRoom = ref(false)
|
||||
|
||||
// ============ Selected room state ============
|
||||
|
|
@ -85,6 +85,25 @@ const canEditRoom = computed(() => room.value?.status !== 'running' && !streamin
|
|||
const executionMode = computed(() => room.value?.execution_mode || 'sequential')
|
||||
const isParallelMode = computed(() => executionMode.value === 'parallel')
|
||||
|
||||
// Parallel mode agent states
|
||||
const parallelAgents = computed(() => {
|
||||
const roomData = store.rooms[selectedId.value]
|
||||
if (!roomData || !roomData.agents) return {}
|
||||
return roomData.agents
|
||||
})
|
||||
|
||||
const parallelAgentList = computed(() => Object.values(parallelAgents.value))
|
||||
|
||||
const parallelStats = computed(() => {
|
||||
const agents = parallelAgentList.value
|
||||
return {
|
||||
total: agents.length,
|
||||
completed: agents.filter(a => a.status === 'completed').length,
|
||||
streaming: agents.filter(a => a.status === 'streaming').length,
|
||||
error: agents.filter(a => a.status === 'error').length
|
||||
}
|
||||
})
|
||||
|
||||
// Group messages by round number for better visual organization
|
||||
const groupedMessages = computed(() => {
|
||||
const groups = []
|
||||
|
|
@ -228,6 +247,7 @@ async function createRoom() {
|
|||
title: newRoom.value.title,
|
||||
task: newRoom.value.task,
|
||||
max_rounds: newRoom.value.max_rounds,
|
||||
execution_mode: newRoom.value.execution_mode,
|
||||
agents
|
||||
})
|
||||
showCreate.value = false
|
||||
|
|
@ -243,7 +263,7 @@ async function createRoom() {
|
|||
}
|
||||
|
||||
function resetNewRoom() {
|
||||
newRoom.value = { title: '', task: '', max_rounds: 5, agent_ids: [] }
|
||||
newRoom.value = { title: '', task: '', max_rounds: 5, agent_ids: [], execution_mode: 'sequential' }
|
||||
}
|
||||
|
||||
function toggleAgentInNewRoom(agentId) {
|
||||
|
|
@ -275,9 +295,15 @@ async function deleteRoom(id) {
|
|||
async function selectRoom(id) {
|
||||
if (streaming.value) {
|
||||
if (abortController) abortController.abort()
|
||||
if (isParallelMode.value) {
|
||||
parallelStreamManager.cancelRoom(selectedId.value)
|
||||
}
|
||||
streaming.value = false
|
||||
}
|
||||
// Clean up both mode states
|
||||
streamingMessages.value = {}
|
||||
store.cleanupRoom(selectedId.value)
|
||||
|
||||
selectedId.value = id
|
||||
error.value = ''
|
||||
editingRoomAgent.value = null
|
||||
|
|
@ -347,9 +373,15 @@ async function startRoom() {
|
|||
if (e.name !== 'AbortError') error.value = e.message
|
||||
} finally {
|
||||
streaming.value = false
|
||||
// Reload messages after parallel execution completes
|
||||
if (selectedId.value) {
|
||||
const res = await chatRoomsAPI.get(selectedId.value)
|
||||
room.value = res.data
|
||||
const [roomRes, msgRes] = await Promise.all([
|
||||
chatRoomsAPI.get(selectedId.value),
|
||||
chatRoomsAPI.getMessages(selectedId.value)
|
||||
])
|
||||
room.value = roomRes.data
|
||||
messages.value = msgRes.data?.messages || []
|
||||
store.cleanupRoom(selectedId.value)
|
||||
await loadRooms()
|
||||
}
|
||||
}
|
||||
|
|
@ -457,11 +489,21 @@ function handleSSEEvent(event, data) {
|
|||
|
||||
async function stopRoom() {
|
||||
try {
|
||||
if (abortController) { abortController.abort(); abortController = null }
|
||||
// Cancel sequential mode
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
abortController = null
|
||||
}
|
||||
// Cancel parallel mode
|
||||
if (isParallelMode.value) {
|
||||
parallelStreamManager.cancelRoom(selectedId.value)
|
||||
}
|
||||
await chatRoomsAPI.stop(selectedId.value)
|
||||
room.value = { ...room.value, status: 'paused' }
|
||||
streaming.value = false
|
||||
streamingMessages.value = {}
|
||||
// Clean up parallel store
|
||||
store.cleanupRoom(selectedId.value)
|
||||
} catch (e) { console.error('Failed to stop room:', e) }
|
||||
}
|
||||
|
||||
|
|
@ -704,12 +746,31 @@ onUnmounted(() => {
|
|||
<span v-else class="mode-badge sequential">📋 Sequential</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="canEditRoom" class="mode-selector">
|
||||
<select v-model="room.execution_mode" @change="updateExecutionMode" class="mode-select">
|
||||
<div class="mode-selector">
|
||||
<select
|
||||
v-model="room.execution_mode"
|
||||
@change="updateExecutionMode"
|
||||
class="mode-select"
|
||||
:disabled="!canEditRoom"
|
||||
>
|
||||
<option value="sequential">📋 Sequential</option>
|
||||
<option value="parallel">⚡ Parallel</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Parallel status indicator -->
|
||||
<div v-if="isParallelMode && streaming && parallelAgentList.length > 0" class="parallel-status">
|
||||
<div class="agent-dots">
|
||||
<span
|
||||
v-for="agent in parallelAgentList"
|
||||
:key="agent.id"
|
||||
class="status-dot-small"
|
||||
:class="agent.status"
|
||||
:style="{ background: agent.message?.sender_color || agent.color || '#2563eb' }"
|
||||
:title="agent.name + ': ' + agent.status"
|
||||
></span>
|
||||
</div>
|
||||
<span class="progress-text">{{ parallelStats.completed }}/{{ parallelStats.total }}</span>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<button v-if="!streaming && room.status !== 'running'" class="btn-ctrl btn-start" @click="startRoom">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
|
||||
|
|
@ -733,7 +794,39 @@ onUnmounted(() => {
|
|||
:room-id="selectedId"
|
||||
mode="parallel"
|
||||
/>
|
||||
<!-- Sequential mode or completed messages -->
|
||||
<!-- Parallel mode history messages (card layout) -->
|
||||
<template v-else-if="isParallelMode">
|
||||
<div v-if="messagesLoading" class="loading-messages"><div class="spinner-small"></div><span>加载中...</span></div>
|
||||
<div v-else-if="messages.length === 0" class="chat-empty"><p>点击「开始」启动多 Agent 对话</p></div>
|
||||
<div v-else class="parallel-history">
|
||||
<div v-for="(roundMsgs, rIdx) in groupedMessages" :key="rIdx" class="parallel-round">
|
||||
<div class="round-divider">
|
||||
<span class="round-divider-line"></span>
|
||||
<span class="round-divider-label">第 {{ roundMsgs[0].round_number || rIdx + 1 }} 轮</span>
|
||||
<span class="round-divider-line"></span>
|
||||
</div>
|
||||
<div class="parallel-grid">
|
||||
<div
|
||||
v-for="msg in roundMsgs"
|
||||
:key="msg.id"
|
||||
class="parallel-card completed"
|
||||
:style="{ borderColor: msg.sender_color }"
|
||||
>
|
||||
<div class="card-header">
|
||||
<span class="agent-avatar" :style="{ background: msg.sender_color || '#2563eb' }">
|
||||
{{ (msg.sender_name || '?').charAt(0) }}
|
||||
</span>
|
||||
<span class="agent-name" :style="{ color: msg.sender_color }">{{ msg.sender_name }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<MessageBubble :message="msg" :deletable="false" :compact="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Sequential mode -->
|
||||
<template v-else>
|
||||
<div v-if="messagesLoading" class="loading-messages"><div class="spinner-small"></div><span>加载中...</span></div>
|
||||
<div v-else-if="messages.length === 0 && Object.keys(streamingMessages).length === 0" class="chat-empty"><p>点击「开始」启动多 Agent 对话</p></div>
|
||||
|
|
@ -783,7 +876,19 @@ onUnmounted(() => {
|
|||
<div class="modal-body">
|
||||
<div class="fg"><label>标题</label><input v-model="newRoom.title" placeholder="项目架构设计讨论" /></div>
|
||||
<div class="fg"><label>任务描述</label><textarea v-model="newRoom.task" rows="3" placeholder="描述需要 Agent 讨论的问题..."></textarea></div>
|
||||
<div class="fg"><label>最大轮次</label><input v-model.number="newRoom.max_rounds" type="number" min="1" max="20" /></div>
|
||||
<div class="fg-row">
|
||||
<div class="fg" style="flex: 1;">
|
||||
<label>最大轮次</label>
|
||||
<input v-model.number="newRoom.max_rounds" type="number" min="1" max="20" />
|
||||
</div>
|
||||
<div class="fg" style="flex: 1;">
|
||||
<label>执行模式</label>
|
||||
<select v-model="newRoom.execution_mode">
|
||||
<option value="sequential">📋 串行 - Agent按顺序发言</option>
|
||||
<option value="parallel">⚡ 并行 - Agent同时发言</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agents-select">
|
||||
<label class="agents-select-label">选择 Agent</label>
|
||||
<div v-if="agentPool.length === 0" class="no-agents-hint">请先在左侧 Agent 池中创建 Agent</div>
|
||||
|
|
@ -977,7 +1082,8 @@ textarea.fi { resize: vertical; min-height: 50px; }
|
|||
.mode-badge.sequential { background: rgba(107, 114, 128, 0.1); color: #6b7280; }
|
||||
|
||||
.mode-selector { margin-left: auto; }
|
||||
.mode-select { padding: 0.25rem 0.5rem; font-size: 0.75rem; border: 1px solid var(--border-light); border-radius: 6px; background: var(--bg-primary); color: var(--text-primary); }
|
||||
.mode-select { padding: 0.25rem 0.5rem; font-size: 0.75rem; border: 1px solid var(--border-light); border-radius: 6px; background: var(--bg-primary); color: var(--text-primary); cursor: pointer; }
|
||||
.mode-select:disabled { opacity: 0.7; cursor: not-allowed; background: var(--bg-secondary); }
|
||||
|
||||
.toolbar-actions { display: flex; gap: 0.4rem; align-items: center; }
|
||||
.btn-ctrl { display: flex; align-items: center; gap: 0.3rem; padding: 0.35rem 0.75rem; border: none; border-radius: 6px; font-size: 0.8rem; cursor: pointer; font-weight: 500; }
|
||||
|
|
@ -999,6 +1105,46 @@ textarea.fi { resize: vertical; min-height: 50px; }
|
|||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.streaming-hint { display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem; color: var(--text-secondary); font-size: 0.8rem; }
|
||||
|
||||
/* Parallel status indicator */
|
||||
.parallel-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 10px;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
.agent-dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.status-dot-small {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.status-dot-small.streaming {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.status-dot-small.completed {
|
||||
opacity: 1;
|
||||
}
|
||||
.status-dot-small.error {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.status-dot-small.pending {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.progress-text {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: #3b82f6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Round group styling */
|
||||
.round-group { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
|
||||
|
|
@ -1040,6 +1186,54 @@ textarea.fi { resize: vertical; min-height: 50px; }
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Parallel history messages */
|
||||
.parallel-history { padding: 0.5rem 0; }
|
||||
.parallel-round { margin-bottom: 1rem; }
|
||||
.parallel-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
.parallel-card {
|
||||
background: var(--bg-primary);
|
||||
border: 2px solid var(--border-light);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.parallel-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.parallel-card .card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
.parallel-card .agent-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
.parallel-card .agent-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.parallel-card .card-body {
|
||||
padding: 12px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Streaming message animation */
|
||||
.streaming-message {
|
||||
opacity: 0.85;
|
||||
|
|
|
|||
|
|
@ -123,14 +123,35 @@ class AgenticLoop:
|
|||
ctx.tool_calls_list, tool_context or {}
|
||||
)
|
||||
|
||||
tool_ids = [tc.get("id") for tc in ctx.tool_calls_list]
|
||||
tool_step_ids = [
|
||||
s.id for s in ctx.all_steps
|
||||
if s.type == StepType.TOOL_CALL and s.id_ref in tool_ids
|
||||
]
|
||||
# Build mapping from LLM tool_call_id to step id
|
||||
# Use index-based matching as fallback when id is empty
|
||||
tool_call_steps = [s for s in ctx.all_steps if s.type == StepType.TOOL_CALL]
|
||||
logger.debug(f"[EXECUTE_TOOLS] tool_call_steps: {[(s.id, s.name, s.id_ref) for s in tool_call_steps]}")
|
||||
logger.debug(f"[EXECUTE_TOOLS] tool_calls_list: {[(tc.get('id'), tc.get('function', {}).get('name')) for tc in ctx.tool_calls_list]}")
|
||||
|
||||
for i, (tr, tc) in enumerate(zip(tool_results, ctx.tool_calls_list)):
|
||||
ref_id = tool_step_ids[i] if i < len(tool_step_ids) else f"step-{len(ctx.all_steps) - len(tool_results) + i}"
|
||||
# Find the corresponding tool_call step
|
||||
tc_id = tc.get("id", "")
|
||||
ref_id = None
|
||||
|
||||
# First try to match by LLM tool_call_id
|
||||
if tc_id:
|
||||
for s in tool_call_steps:
|
||||
if s.id_ref == tc_id:
|
||||
ref_id = s.id
|
||||
logger.debug(f"[EXECUTE_TOOLS] Matched by id: tc_id={tc_id} -> step_id={ref_id}")
|
||||
break
|
||||
|
||||
# Fallback: use index-based matching
|
||||
if ref_id is None and i < len(tool_call_steps):
|
||||
ref_id = tool_call_steps[i].id
|
||||
logger.debug(f"[EXECUTE_TOOLS] Matched by index: i={i} -> step_id={ref_id}")
|
||||
|
||||
# Last resort: generate a step id
|
||||
if ref_id is None:
|
||||
ref_id = f"step-{len(ctx.all_steps) - len(tool_results) + i}"
|
||||
logger.debug(f"[EXECUTE_TOOLS] Generated ref_id: {ref_id}")
|
||||
|
||||
_, event = StreamRenderer.render_tool_result(ctx, tr, ref_id)
|
||||
events.append(event)
|
||||
|
||||
|
|
|
|||
|
|
@ -296,6 +296,7 @@ class ChatRoomOrchestrator:
|
|||
# Yield parallel start event
|
||||
yield sse_event("parallel_start", {
|
||||
"round": round_num,
|
||||
"max_rounds": self.max_rounds,
|
||||
"agents": [{"id": a.id, "name": a.name} for a in agents]
|
||||
})
|
||||
|
||||
|
|
@ -364,8 +365,11 @@ class ChatRoomOrchestrator:
|
|||
):
|
||||
if delta.content:
|
||||
accumulated_content += delta.content
|
||||
# Estimate progress based on content length (assume max ~2000 chars)
|
||||
progress = min(95, int(len(accumulated_content) / 20))
|
||||
yield {"type": "message_chunk", "id": msg_id, "content": delta.content,
|
||||
"accumulated": accumulated_content, "agent_id": agent.id}
|
||||
"accumulated": accumulated_content, "agent_id": agent.id,
|
||||
"progress": progress}
|
||||
|
||||
if delta.is_complete:
|
||||
break
|
||||
|
|
|
|||
|
|
@ -181,6 +181,14 @@ class StreamState:
|
|||
"""Finalize the current step and add to all_steps"""
|
||||
if self.current_step_id is None:
|
||||
return
|
||||
# TOOL_CALL and TOOL_RESULT steps are handled manually in render methods
|
||||
# They have their own data (name, arguments, id_ref) that can't be auto-generated
|
||||
if self.current_step_type in (StepType.TOOL_CALL, StepType.TOOL_RESULT):
|
||||
# Just reset the state without adding a step
|
||||
self.current_step_id = None
|
||||
self.current_step_idx = None
|
||||
self.current_step_type = None
|
||||
return
|
||||
if self.current_step_type == StepType.TEXT:
|
||||
content = self.full_content[self._text_offset:]
|
||||
elif self.current_step_type == StepType.THINKING:
|
||||
|
|
@ -208,6 +216,10 @@ class StreamState:
|
|||
"type": "function",
|
||||
"function": {"name": "", "arguments": ""}
|
||||
})
|
||||
else:
|
||||
# Update id if provided (LLM may send id in a later delta)
|
||||
if tc_delta.get("id"):
|
||||
self.tool_calls_list[idx]["id"] = tc_delta["id"]
|
||||
func = tc_delta.get("function", {})
|
||||
if func.get("name"):
|
||||
self.tool_calls_list[idx]["function"]["name"] += func["name"]
|
||||
|
|
@ -238,31 +250,49 @@ class StreamRenderer:
|
|||
|
||||
@staticmethod
|
||||
def render_tool_calls(state: StreamState) -> List[str]:
|
||||
"""Render tool calls as SSE events"""
|
||||
"""Render tool calls as SSE events
|
||||
|
||||
Note: This manually manages step creation for TOOL_CALL type.
|
||||
The start_step is called to get a unique step id and index,
|
||||
but the step is manually created and added to avoid duplication.
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
events = []
|
||||
for tc in state.tool_calls_list:
|
||||
# Use start_step to auto-finalize previous and create new step
|
||||
state.start_step(StepType.TOOL_CALL)
|
||||
# Get step id and index from start_step
|
||||
step_id = state.start_step(StepType.TOOL_CALL)
|
||||
step = Step(
|
||||
id=state.current_step_id,
|
||||
id=step_id,
|
||||
index=state.current_step_idx,
|
||||
type=StepType.TOOL_CALL,
|
||||
name=tc["function"]["name"],
|
||||
arguments=tc["function"]["arguments"],
|
||||
id_ref=tc.get("id", "")
|
||||
)
|
||||
# Append again since start_step finalized previous (if any)
|
||||
# Manually add the step (finalize_step won't add it for TOOL_CALL type)
|
||||
state.all_steps.append(step)
|
||||
logger.debug(f"[TOOL_CALL] Created step: id={step.id}, index={step.index}, name={step.name}, id_ref={step.id_ref}")
|
||||
events.append(sse_event("process_step", {"step": step.to_dict()}))
|
||||
# Reset current step state since we manually handled it
|
||||
state.current_step_id = None
|
||||
state.current_step_idx = None
|
||||
state.current_step_type = None
|
||||
return events
|
||||
|
||||
@staticmethod
|
||||
def render_tool_result(state: StreamState, result: Dict, ref_step_id: str) -> tuple:
|
||||
"""Render a tool result as SSE event"""
|
||||
import json
|
||||
"""Render a tool result as SSE event
|
||||
|
||||
Note: This does NOT call start_step because tool_result should be linked
|
||||
to its corresponding tool_call via id_ref, not create a new independent step.
|
||||
The index is derived from the tool_call step to maintain proper ordering.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Use start_step to auto-finalize previous and create new step
|
||||
state.start_step(StepType.TOOL_RESULT)
|
||||
content = result.get("content", "")
|
||||
success = True
|
||||
|
||||
|
|
@ -273,9 +303,19 @@ class StreamRenderer:
|
|||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
# Find the corresponding tool_call step to get its index
|
||||
tool_call_step = None
|
||||
for s in state.all_steps:
|
||||
if s.type == StepType.TOOL_CALL and s.id == ref_step_id:
|
||||
tool_call_step = s
|
||||
break
|
||||
|
||||
# Use the same index as the tool_call for proper ordering in frontend
|
||||
step_index = tool_call_step.index if tool_call_step else state.step_index
|
||||
|
||||
step = Step(
|
||||
id=state.current_step_id,
|
||||
index=state.current_step_idx,
|
||||
id=f"result-{ref_step_id}", # Unique id based on tool_call step id
|
||||
index=step_index, # Same index as tool_call for proper ordering
|
||||
type=StepType.TOOL_RESULT,
|
||||
name=result.get("name", ""),
|
||||
content=content,
|
||||
|
|
@ -285,6 +325,8 @@ class StreamRenderer:
|
|||
state.all_steps.append(step)
|
||||
state.add_tool_result(result)
|
||||
|
||||
logger.debug(f"[TOOL_RESULT] Created step: id={step.id}, index={step.index}, id_ref={step.id_ref}, ref_step_id={ref_step_id}")
|
||||
|
||||
return step, sse_event("process_step", {"step": step.to_dict()})
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -159,20 +159,26 @@ class ToolExecutor:
|
|||
tool_calls: List[Dict[str, Any]],
|
||||
context: Dict[str, Any]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Process tool calls in parallel"""
|
||||
"""Process tool calls in parallel
|
||||
|
||||
IMPORTANT: Results are returned in the SAME ORDER as input tool_calls,
|
||||
not in completion order. This ensures proper matching between tool_call
|
||||
and tool_result steps in the frontend.
|
||||
"""
|
||||
if len(tool_calls) <= 1:
|
||||
return self.process_tool_calls(tool_calls, context)
|
||||
|
||||
tool_ctx = self._build_tool_context(context)
|
||||
|
||||
try:
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
futures = {}
|
||||
cached_results = []
|
||||
# Store futures with their original index to maintain order
|
||||
futures_with_index = {}
|
||||
results = [None] * len(tool_calls)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
||||
for call in tool_calls:
|
||||
for idx, call in enumerate(tool_calls):
|
||||
call_id = call.get("id", "")
|
||||
name = call.get("function", {}).get("name", "")
|
||||
args = self._parse_arguments(call)
|
||||
|
|
@ -183,24 +189,25 @@ class ToolExecutor:
|
|||
|
||||
if cached is not None:
|
||||
self.history.record(name, args, cached)
|
||||
cached_results.append(self._create_tool_result(call_id, name, cached))
|
||||
# Store result at the correct index
|
||||
results[idx] = self._create_tool_result(call_id, name, cached)
|
||||
else:
|
||||
# Submit task
|
||||
# Submit task with index
|
||||
future = executor.submit(
|
||||
registry.execute, name, args, context=tool_ctx
|
||||
)
|
||||
futures[future] = (call_id, name, args, cache_key)
|
||||
futures_with_index[future] = (idx, call_id, name, args, cache_key)
|
||||
|
||||
results = list(cached_results)
|
||||
|
||||
for future in as_completed(futures):
|
||||
call_id, name, args, cache_key = futures[future]
|
||||
# Wait for all futures and store results at correct indices
|
||||
for future in futures_with_index:
|
||||
idx, call_id, name, args, cache_key = futures_with_index[future]
|
||||
result = future.result()
|
||||
self.cache.set(cache_key, result)
|
||||
self.history.record(name, args, result)
|
||||
results.append(self._create_tool_result(call_id, name, result))
|
||||
results[idx] = self._create_tool_result(call_id, name, result)
|
||||
|
||||
return results
|
||||
# Filter out None values (shouldn't happen, but safety check)
|
||||
return [r for r in results if r is not None]
|
||||
|
||||
except ImportError:
|
||||
return self.process_tool_calls(tool_calls, context)
|
||||
|
|
|
|||
Loading…
Reference in New Issue