Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions service/app/core/chat/langchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,14 @@ async def _handle_messages_mode(data: Any, ctx: StreamContext) -> AsyncGenerator
return

if not ctx.is_streaming:
# Emit synthetic node_start if no node was detected
# This handles prebuilt agents (like ReAct) that don't include langgraph_node metadata
if ctx.event_ctx and not ctx.current_node:
node_start_event = AgentEventStreamHandler.create_node_start_event(ctx, "agent")
if node_start_event:
logger.info("[AgentEvent/Messages] Emitting synthetic node_start for 'agent'")
yield node_start_event
Comment on lines +472 to +476
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: 请再次确认将节点名称硬编码为 "agent" 是否总是适合作为合成的 node_start 事件名称。

这里使用 "agent" 意味着所有合成的 node_start 事件都共享相同的节点标识符。如果不同的预构建 agent(或未来新增的)应该有各自不同的节点名称,这可能会影响可追踪性,或者与其他命名约定冲突。建议考虑从 ctx 中推导名称(例如基于 agent 类型 / 配置),或者为默认名称定义一个共享常量,以便在需要时可以集中修改。

Original comment in English

question: Double-check whether the hardcoded node name "agent" is always appropriate for synthetic node_start events.

Using "agent" here assumes all synthetic node_start events share the same node identifier. If different prebuilt agents (or future ones) should have distinct node names, this could hurt traceability or clash with other naming conventions. Consider deriving the name from ctx (e.g., agent type/config), or defining a shared constant for the default name so it can be changed centrally.


logger.debug("Emitting streaming_start for stream_id=%s", ctx.stream_id)
ctx.is_streaming = True
yield StreamingEventHandler.create_streaming_start(ctx.stream_id)
Expand Down
39 changes: 33 additions & 6 deletions web/src/components/layouts/components/ChatBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,39 @@ function ChatBubble({ message }: ChatBubbleProps) {
{isLoading ? (
<LoadingMessage size="medium" className="text-sm" />
) : (
// Show markdownContent when:
// 1. No agentExecution (regular chat)
// 2. agentExecution is react type (simple agent without timeline)
// For multi-phase agents, content is shown in timeline phases
(!agentExecution || agentExecution.agentType === "react") &&
markdownContent
// Show content based on agent type:
// 1. No agentExecution: show message.content
// 2. React/simple agents with phases: show phase.streamedContent
// 3. React agents without phases: show message.content (fallback)
// 4. Multi-phase agents: content is shown in timeline phases
(() => {
// No agent execution - regular chat
if (!agentExecution) {
return markdownContent;
}

// React/simple agents - show streaming content from phase or message.content
if (agentExecution.agentType === "react") {
// If we have phases with streamedContent, show that
if (agentExecution.phases.length > 0) {
const activePhase = agentExecution.phases.find(
(p) => p.status === "running",
);
const phaseContent =
activePhase?.streamedContent ||
agentExecution.phases[agentExecution.phases.length - 1]
?.streamedContent;
if (phaseContent) {
return <Markdown content={phaseContent} />;
}
}
// Fallback to message.content
return markdownContent;
}

// Multi-phase agents - content is shown in timeline, don't show here
return null;
})()
)}

{/* For multi-phase agents, show final report below timeline when completed */}
Expand Down
18 changes: 18 additions & 0 deletions web/src/store/slices/chatSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,25 @@ export const createChatSlice: StateCreator<
const agentMsgIndex = channel.messages.findLastIndex(
(m) => m.agentExecution?.status === "running",
);

if (agentMsgIndex !== -1) {
const execution =
channel.messages[agentMsgIndex].agentExecution;

// Create default "Response" phase if no phases exist
// This handles prebuilt agents (like ReAct) that don't emit node_start events
if (execution && execution.phases.length === 0) {
execution.phases.push({
id: "response",
name: "Response",
status: "running",
startedAt: Date.now(),
nodes: [],
streamedContent: "",
});
execution.currentNode = "response";
}

// Agent execution exists, mark it as streaming but don't create a new message
// Content will be routed to phase.streamedContent in streaming_chunk
channel.messages[agentMsgIndex] = {
Expand Down
Loading