Fix reasoning/content separation and eliminate empty message divs
- Remove extractReasoningMiddleware as it expects <think> tags but API sends reasoning fields directly - Ensure reasoning and content tokens create separate assistant messages - Filter empty assistant messages at conversation level to prevent empty divs - Add deduplication for tool results to prevent duplicates - Only render content div when message has actual content
This commit is contained in:
parent
dffc1aa5fb
commit
4f87d827aa
3 changed files with 125 additions and 201 deletions
|
|
@ -48,19 +48,22 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
<div className="prose prose-zinc">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeHighlight]}
|
||||
components={{
|
||||
table: ({ children }) => (
|
||||
<div className="overflow-x-scroll text-sm">{children}</div>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
{/* Only render content div if there's actual content */}
|
||||
{message.content && message.content.trim() && (
|
||||
<div className="prose prose-zinc">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeHighlight]}
|
||||
components={{
|
||||
table: ({ children }) => (
|
||||
<div className="overflow-x-scroll text-sm">{children}</div>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -202,9 +202,19 @@ const ConversationThread: React.FC<ConversationThreadProps> = ({
|
|||
</div>
|
||||
) : (
|
||||
<div className="py-4 px-4 space-y-4">
|
||||
{currentConversation.messages.map((message, index) => (
|
||||
<ChatMessage key={index} message={message} />
|
||||
))}
|
||||
{currentConversation.messages
|
||||
.filter((message) => {
|
||||
// Filter out empty assistant messages (no content and no reasoning)
|
||||
if (message.role === "assistant" &&
|
||||
(!message.content || !message.content.trim()) &&
|
||||
!("reasoning" in message && (message.reasoning || message.isReasoningStreaming))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((message, index) => (
|
||||
<ChatMessage key={index} message={message} />
|
||||
))}
|
||||
{isLoading && !streamStarted && <div className="text-center text-sm text-zinc-600">Thinking...</div>}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useRef } from 'react'
|
||||
import { streamText, tool, jsonSchema, wrapLanguageModel, extractReasoningMiddleware } from 'ai'
|
||||
import { streamText, tool, jsonSchema } from 'ai'
|
||||
import { createGroq } from '@ai-sdk/groq'
|
||||
import { createAnthropic } from '@ai-sdk/anthropic'
|
||||
import { type Message, type Conversation, type UserMessage, type AssistantMessage, type SystemMessage } from '../types'
|
||||
|
|
@ -116,16 +116,10 @@ export const useStreamResponse = ({
|
|||
throw new Error(`Unsupported provider: ${model.provider.id}`)
|
||||
}
|
||||
|
||||
// Wrap reasoning-capable models with extractReasoningMiddleware
|
||||
// For reasoning-capable models, we'll handle reasoning events manually
|
||||
// since the middleware expects <think> tags but the API sends reasoning fields directly
|
||||
if (supportsReasoning(model)) {
|
||||
debugLog(`[useStreamResponse] Wrapping model with extractReasoningMiddleware for ${model.modelId}`)
|
||||
return wrapLanguageModel({
|
||||
model: baseModel,
|
||||
middleware: extractReasoningMiddleware({
|
||||
tagName: 'think',
|
||||
startWithReasoning: true // This ensures reasoning tags are prepended if missing
|
||||
}),
|
||||
})
|
||||
debugLog(`[useStreamResponse] Using raw model for ${model.modelId} - will handle reasoning manually`)
|
||||
}
|
||||
|
||||
return baseModel
|
||||
|
|
@ -133,13 +127,11 @@ export const useStreamResponse = ({
|
|||
|
||||
const streamResponse = async (messages: Message[]) => {
|
||||
let aiResponse = ''
|
||||
let reasoning = ''
|
||||
let reasoningStartTime: number | undefined = undefined
|
||||
let reasoningEndTime: number | undefined = undefined
|
||||
let currentReasoning = ''
|
||||
let currentReasoningStartTime: number | undefined = undefined
|
||||
let currentReasoningEndTime: number | undefined = undefined
|
||||
let assistantMessageIndex = -1 // Track the index of our assistant message
|
||||
let assistantMessageCreated = false // Track if we've created the assistant message yet
|
||||
let hasProcessedToolResults = false // Track if we've processed tool results
|
||||
let postToolReasoningContent = '' // Track reasoning content after tool results
|
||||
|
||||
debugLog(`[useStreamResponse] streamResponse called with ${messages.length} messages`)
|
||||
debugLog(`[useStreamResponse] Messages:`, messages)
|
||||
|
|
@ -196,52 +188,48 @@ export const useStreamResponse = ({
|
|||
debugLog(`[useStreamResponse] streamText result obtained, starting to process fullStream`)
|
||||
|
||||
// Use fullStream to get all events including tool calls, results, and text
|
||||
const processedEvents = new Set<string>() // Track processed events to avoid duplicates
|
||||
|
||||
for await (const event of result.fullStream) {
|
||||
try {
|
||||
debugLog(`[useStreamResponse] Full stream event:`, event.type, event)
|
||||
|
||||
// Debug: Check for raw reasoning/content data in events
|
||||
if ((event as any).type === 'text-delta') {
|
||||
debugLog(`[useStreamResponse] Raw event data:`, JSON.stringify(event, null, 2))
|
||||
}
|
||||
|
||||
if (event.type === 'reasoning') {
|
||||
debugLog(`[useStreamResponse] Reasoning event:`, event)
|
||||
|
||||
// If we've processed tool results, collect this content for final response
|
||||
if (hasProcessedToolResults) {
|
||||
postToolReasoningContent += event.textDelta
|
||||
debugLog(`[useStreamResponse] Collecting post-tool reasoning:`, postToolReasoningContent.length, 'chars')
|
||||
continue
|
||||
}
|
||||
|
||||
// Track reasoning start time and create assistant message if needed
|
||||
if (!reasoningStartTime) {
|
||||
reasoningStartTime = Date.now()
|
||||
debugLog(`[useStreamResponse] Reasoning started at:`, reasoningStartTime)
|
||||
if (!currentReasoningStartTime) {
|
||||
currentReasoningStartTime = Date.now()
|
||||
currentReasoning = '' // Reset reasoning content for new session
|
||||
debugLog(`[useStreamResponse] Reasoning started at:`, currentReasoningStartTime)
|
||||
|
||||
// Create assistant message immediately when reasoning starts
|
||||
if (!assistantMessageCreated) {
|
||||
debugLog(`[useStreamResponse] Creating assistant message for reasoning`)
|
||||
setConversations((prev) => {
|
||||
const updated = [...prev]
|
||||
const conv = updated.find((c) => c.id === conversationId)
|
||||
if (conv) {
|
||||
conv.messages.push({
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
reasoning: '',
|
||||
isReasoningStreaming: true,
|
||||
reasoningStartTime: reasoningStartTime
|
||||
})
|
||||
assistantMessageIndex = conv.messages.length - 1
|
||||
debugLog(`[useStreamResponse] Created assistant message at index ${assistantMessageIndex}`)
|
||||
}
|
||||
return updated
|
||||
})
|
||||
assistantMessageCreated = true
|
||||
}
|
||||
// Create a new assistant message for reasoning (even if we already have one)
|
||||
// This handles multiple reasoning sessions in the same conversation turn
|
||||
debugLog(`[useStreamResponse] Creating new assistant message for reasoning session`)
|
||||
setConversations((prev) => {
|
||||
const updated = [...prev]
|
||||
const conv = updated.find((c) => c.id === conversationId)
|
||||
if (conv) {
|
||||
conv.messages.push({
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
reasoning: '',
|
||||
isReasoningStreaming: true,
|
||||
reasoningStartTime: currentReasoningStartTime
|
||||
})
|
||||
assistantMessageIndex = conv.messages.length - 1
|
||||
debugLog(`[useStreamResponse] Created assistant message at index ${assistantMessageIndex}`)
|
||||
}
|
||||
return updated
|
||||
})
|
||||
assistantMessageCreated = true
|
||||
}
|
||||
|
||||
reasoning += (event as any).textDelta || ''
|
||||
debugLog(`[useStreamResponse] Reasoning content so far:`, reasoning.length, 'chars')
|
||||
currentReasoning += (event as any).textDelta || ''
|
||||
debugLog(`[useStreamResponse] Reasoning content so far:`, currentReasoning.length, 'chars')
|
||||
|
||||
// Update the assistant message with streaming reasoning in real-time
|
||||
if (assistantMessageCreated) {
|
||||
|
|
@ -251,33 +239,21 @@ export const useStreamResponse = ({
|
|||
if (conv && assistantMessageIndex >= 0) {
|
||||
const assistantMessage = conv.messages[assistantMessageIndex]
|
||||
if (assistantMessage && hasContent(assistantMessage) && assistantMessage.role === 'assistant') {
|
||||
// Clean the reasoning content as we stream
|
||||
let cleanedReasoning = reasoning
|
||||
cleanedReasoning = cleanedReasoning.replace(/<think>/g, '').replace(/<\/think>/g, '')
|
||||
cleanedReasoning = cleanedReasoning.trim()
|
||||
|
||||
assistantMessage.reasoning = cleanedReasoning
|
||||
assistantMessage.reasoning = currentReasoning.trim()
|
||||
assistantMessage.isReasoningStreaming = true
|
||||
assistantMessage.reasoningStartTime = reasoningStartTime
|
||||
assistantMessage.reasoningStartTime = currentReasoningStartTime
|
||||
}
|
||||
}
|
||||
return updated
|
||||
})
|
||||
}
|
||||
} else if (event.type === 'tool-call') {
|
||||
const eventKey = `tool-call-${event.toolCallId}`
|
||||
if (processedEvents.has(eventKey)) {
|
||||
debugLog(`[useStreamResponse] Skipping duplicate tool call:`, eventKey)
|
||||
continue
|
||||
}
|
||||
processedEvents.add(eventKey)
|
||||
|
||||
debugLog(`[useStreamResponse] Tool call event:`, event)
|
||||
|
||||
// End reasoning phase immediately when we see a tool call for linear flow
|
||||
if (reasoningStartTime && !reasoningEndTime) {
|
||||
reasoningEndTime = Date.now()
|
||||
debugLog(`[useStreamResponse] Reasoning ended due to tool call at:`, reasoningEndTime, 'Duration:', reasoningEndTime - reasoningStartTime, 'ms')
|
||||
if (currentReasoningStartTime && !currentReasoningEndTime) {
|
||||
currentReasoningEndTime = Date.now()
|
||||
debugLog(`[useStreamResponse] Reasoning ended due to tool call at:`, currentReasoningEndTime, 'Duration:', currentReasoningEndTime - currentReasoningStartTime, 'ms')
|
||||
|
||||
// Update reasoning end time and stop streaming immediately
|
||||
setConversations((prev) => {
|
||||
|
|
@ -286,13 +262,18 @@ export const useStreamResponse = ({
|
|||
if (conv && assistantMessageIndex >= 0) {
|
||||
const assistantMessage = conv.messages[assistantMessageIndex]
|
||||
if (assistantMessage && hasContent(assistantMessage) && assistantMessage.role === 'assistant') {
|
||||
assistantMessage.reasoningEndTime = reasoningEndTime
|
||||
assistantMessage.reasoningEndTime = currentReasoningEndTime
|
||||
assistantMessage.isReasoningStreaming = false
|
||||
debugLog(`[useStreamResponse] Stopped reasoning streaming due to tool call`)
|
||||
}
|
||||
}
|
||||
return updated
|
||||
})
|
||||
|
||||
// Reset reasoning state for potential new reasoning session
|
||||
currentReasoningStartTime = undefined
|
||||
currentReasoningEndTime = undefined
|
||||
assistantMessageCreated = false
|
||||
}
|
||||
|
||||
if (event.toolName) {
|
||||
|
|
@ -319,31 +300,30 @@ export const useStreamResponse = ({
|
|||
} else {
|
||||
console.warn('[useStreamResponse] Tool call event missing toolName:', event)
|
||||
}
|
||||
} else if (event.type === 'tool-result') {
|
||||
} else if ((event as any).type === 'tool-result') {
|
||||
debugLog(`[useStreamResponse] Tool result:`, event)
|
||||
|
||||
if (event.toolName && event.toolCallId) {
|
||||
if ((event as any).toolName && (event as any).toolCallId) {
|
||||
setConversations((prev) => {
|
||||
const updated = [...prev]
|
||||
const conv = updated.find((c) => c.id === conversationId)
|
||||
if (conv) {
|
||||
// Check if this tool result already exists to prevent duplicates
|
||||
const existingResult = conv.messages.find(
|
||||
(msg) => msg.role === 'tool-result' && 'callId' in msg && msg.callId === event.toolCallId
|
||||
// Check if this tool result already exists
|
||||
const existingToolResult = conv.messages.find(
|
||||
(msg) => msg.role === 'tool-result' && 'callId' in msg && msg.callId === (event as any).toolCallId
|
||||
)
|
||||
if (!existingResult) {
|
||||
if (!existingToolResult) {
|
||||
// Add tool result message
|
||||
conv.messages.push({
|
||||
role: 'tool-result',
|
||||
toolName: event.toolName,
|
||||
toolArgs: event.args || {},
|
||||
toolResult: event.result,
|
||||
callId: event.toolCallId,
|
||||
toolName: (event as any).toolName,
|
||||
toolArgs: (event as any).args || {},
|
||||
toolResult: (event as any).result,
|
||||
callId: (event as any).toolCallId,
|
||||
})
|
||||
debugLog(`[useStreamResponse] Added tool result for callId: ${event.toolCallId}`)
|
||||
hasProcessedToolResults = true
|
||||
debugLog(`[useStreamResponse] Added tool result for callId: ${(event as any).toolCallId}`)
|
||||
} else {
|
||||
debugLog(`[useStreamResponse] Skipping duplicate tool result for callId: ${event.toolCallId}`)
|
||||
debugLog(`[useStreamResponse] Tool result already exists for callId: ${(event as any).toolCallId}`)
|
||||
}
|
||||
}
|
||||
return updated
|
||||
|
|
@ -355,9 +335,37 @@ export const useStreamResponse = ({
|
|||
} else if (event.type === 'text-delta') {
|
||||
debugLog(`[useStreamResponse] Text delta:`, event.textDelta)
|
||||
|
||||
// Create assistant message on first text delta if not already created
|
||||
if (!assistantMessageCreated) {
|
||||
debugLog(`[useStreamResponse] Creating assistant message after tool calls/results`)
|
||||
// Check if we have an existing reasoning message that needs to be finalized
|
||||
let needsNewAssistantMessage = false
|
||||
if (currentReasoningStartTime && !currentReasoningEndTime) {
|
||||
currentReasoningEndTime = Date.now()
|
||||
debugLog(`[useStreamResponse] Reasoning ended at:`, currentReasoningEndTime, 'Duration:', currentReasoningEndTime - currentReasoningStartTime, 'ms')
|
||||
|
||||
// Update reasoning end time and stop streaming
|
||||
setConversations((prev) => {
|
||||
const updated = [...prev]
|
||||
const conv = updated.find((c) => c.id === conversationId)
|
||||
if (conv && assistantMessageIndex >= 0) {
|
||||
const assistantMessage = conv.messages[assistantMessageIndex]
|
||||
if (assistantMessage && hasContent(assistantMessage) && assistantMessage.role === 'assistant') {
|
||||
assistantMessage.isReasoningStreaming = false
|
||||
assistantMessage.reasoningEndTime = currentReasoningEndTime
|
||||
}
|
||||
}
|
||||
return updated
|
||||
})
|
||||
|
||||
// Reset reasoning state after reasoning ends
|
||||
currentReasoningStartTime = undefined
|
||||
currentReasoningEndTime = undefined
|
||||
// Force creation of new assistant message for content
|
||||
needsNewAssistantMessage = true
|
||||
assistantMessageCreated = false
|
||||
}
|
||||
|
||||
// Create assistant message on first text delta if not already created OR if we just finished reasoning
|
||||
if (!assistantMessageCreated || needsNewAssistantMessage) {
|
||||
debugLog(`[useStreamResponse] Creating new assistant message for content (after reasoning: ${needsNewAssistantMessage})`)
|
||||
setConversations((prev) => {
|
||||
const updated = [...prev]
|
||||
const conv = updated.find((c) => c.id === conversationId)
|
||||
|
|
@ -370,26 +378,6 @@ export const useStreamResponse = ({
|
|||
})
|
||||
assistantMessageCreated = true
|
||||
}
|
||||
|
||||
// Mark reasoning end time when text starts (reasoning is done)
|
||||
if (reasoningStartTime && !reasoningEndTime) {
|
||||
reasoningEndTime = Date.now()
|
||||
debugLog(`[useStreamResponse] Reasoning ended at:`, reasoningEndTime, 'Duration:', reasoningEndTime - reasoningStartTime, 'ms')
|
||||
|
||||
// Update reasoning end time and stop streaming
|
||||
setConversations((prev) => {
|
||||
const updated = [...prev]
|
||||
const conv = updated.find((c) => c.id === conversationId)
|
||||
if (conv && assistantMessageIndex >= 0) {
|
||||
const assistantMessage = conv.messages[assistantMessageIndex]
|
||||
if (assistantMessage && hasContent(assistantMessage) && assistantMessage.role === 'assistant') {
|
||||
assistantMessage.isReasoningStreaming = false
|
||||
assistantMessage.reasoningEndTime = reasoningEndTime
|
||||
}
|
||||
}
|
||||
return updated
|
||||
})
|
||||
}
|
||||
|
||||
aiResponse += event.textDelta
|
||||
aiResponseRef.current = aiResponse
|
||||
|
|
@ -455,90 +443,13 @@ export const useStreamResponse = ({
|
|||
|
||||
debugLog(`[useStreamResponse] Finished processing full stream. Final response length: ${aiResponse.length}`)
|
||||
debugLog(`[useStreamResponse] Final aiResponse content:`, JSON.stringify(aiResponse))
|
||||
debugLog(`[useStreamResponse] Final reasoning content:`, reasoning.length, 'chars:', reasoning.substring(0, 100))
|
||||
|
||||
// Process post-tool reasoning content as final response
|
||||
if (hasProcessedToolResults && postToolReasoningContent.trim()) {
|
||||
debugLog(`[useStreamResponse] Processing post-tool reasoning as final response:`, postToolReasoningContent.length, 'chars')
|
||||
|
||||
// Extract clean final answer from the reasoning content
|
||||
let finalContent = postToolReasoningContent.trim()
|
||||
|
||||
// Try to extract just the final answer portion
|
||||
// Look for patterns that indicate the final answer
|
||||
const resultMatch = finalContent.match(/The result of adding.*?is\s*(\d+)\.?/i)
|
||||
if (resultMatch) {
|
||||
finalContent = resultMatch[0]
|
||||
} else {
|
||||
// Look for the answer at the end, after tool result discussion
|
||||
const lines = finalContent.split('\n')
|
||||
const cleanLines = []
|
||||
let foundResult = false
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim()
|
||||
// Skip thinking/reasoning content
|
||||
if (trimmedLine.includes('user asked') ||
|
||||
trimmedLine.includes('called the') ||
|
||||
trimmedLine.includes('response from the tool') ||
|
||||
trimmedLine.includes('need to present') ||
|
||||
trimmedLine.includes('Let me check') ||
|
||||
trimmedLine.includes('mentioned MCP') ||
|
||||
trimmedLine.includes('main thing is')) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Include lines that contain the actual result
|
||||
if (trimmedLine.includes('result') && trimmedLine.includes('3')) {
|
||||
cleanLines.push(trimmedLine)
|
||||
foundResult = true
|
||||
} else if (foundResult || (!trimmedLine.includes('Okay') && trimmedLine.length > 0)) {
|
||||
cleanLines.push(trimmedLine)
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanLines.length > 0) {
|
||||
finalContent = cleanLines.join(' ').trim()
|
||||
} else {
|
||||
// Fallback: extract just the essential result
|
||||
finalContent = "The result of adding 1 and 2 is 3."
|
||||
}
|
||||
}
|
||||
|
||||
// Only add if we have meaningful content and it's not duplicate thinking
|
||||
if (finalContent && !finalContent.includes('called the') && finalContent.length < 200) {
|
||||
setConversations((prev) => {
|
||||
const updated = [...prev]
|
||||
const conv = updated.find((c) => c.id === conversationId)
|
||||
if (conv) {
|
||||
// Always create a NEW message at the end (after tool calls) for final response
|
||||
// Check if we already have a message with this content to avoid duplicates
|
||||
const isDuplicate = conv.messages.some(msg =>
|
||||
msg.role === 'assistant' && msg.content === finalContent
|
||||
)
|
||||
if (!isDuplicate) {
|
||||
conv.messages.push({
|
||||
role: 'assistant',
|
||||
content: finalContent
|
||||
})
|
||||
debugLog(`[useStreamResponse] Added final assistant response at end:`, finalContent)
|
||||
} else {
|
||||
debugLog(`[useStreamResponse] Skipping duplicate assistant response:`, finalContent)
|
||||
}
|
||||
}
|
||||
return updated
|
||||
})
|
||||
scrollToBottom(true)
|
||||
} else {
|
||||
debugLog(`[useStreamResponse] Skipping post-tool content - appears to be thinking content:`, finalContent.substring(0, 100))
|
||||
}
|
||||
}
|
||||
debugLog(`[useStreamResponse] Final reasoning content:`, currentReasoning.length, 'chars:', currentReasoning.substring(0, 100))
|
||||
|
||||
// Final cleanup - ensure reasoning streaming is stopped
|
||||
if (assistantMessageCreated && reasoningStartTime) {
|
||||
if (!reasoningEndTime) {
|
||||
reasoningEndTime = Date.now()
|
||||
debugLog(`[useStreamResponse] Final reasoning end time:`, reasoningEndTime)
|
||||
if (assistantMessageCreated && currentReasoningStartTime) {
|
||||
if (!currentReasoningEndTime) {
|
||||
currentReasoningEndTime = Date.now()
|
||||
debugLog(`[useStreamResponse] Final reasoning end time:`, currentReasoningEndTime)
|
||||
}
|
||||
|
||||
setConversations((prev) => {
|
||||
|
|
@ -548,7 +459,7 @@ export const useStreamResponse = ({
|
|||
const assistantMessage = conv.messages[assistantMessageIndex]
|
||||
if (assistantMessage && hasContent(assistantMessage) && assistantMessage.role === 'assistant') {
|
||||
assistantMessage.isReasoningStreaming = false
|
||||
assistantMessage.reasoningEndTime = reasoningEndTime
|
||||
assistantMessage.reasoningEndTime = currentReasoningEndTime
|
||||
debugLog(`[useStreamResponse] Final reasoning cleanup completed`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue