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
102 changes: 89 additions & 13 deletions apps/sim/app/api/copilot/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,25 @@ const ChatMessageSchema = z.object({
contexts: z
.array(
z.object({
kind: z.enum(['past_chat', 'workflow', 'blocks', 'logs', 'knowledge', 'templates']),
kind: z.enum([
'past_chat',
'workflow',
'current_workflow',
'blocks',
'logs',
'workflow_block',
'knowledge',
'templates',
'docs',
]),
label: z.string(),
chatId: z.string().optional(),
workflowId: z.string().optional(),
knowledgeId: z.string().optional(),
blockId: z.string().optional(),
templateId: z.string().optional(),
executionId: z.string().optional(),
// For workflow_block, provide both workflowId and blockId
})
)
.optional(),
Expand Down Expand Up @@ -105,6 +117,7 @@ export async function POST(req: NextRequest) {
kind: c?.kind,
chatId: c?.chatId,
workflowId: c?.workflowId,
executionId: (c as any)?.executionId,
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Using (c as any)?.executionId bypasses TypeScript safety. Consider updating the ChatContext type to include executionId properly.

Context Used: Context - Avoid using type assertions to 'any' in TypeScript. Instead, ensure proper type definitions are used to maintain type safety. (link)

label: c?.label,
}))
: undefined,
Expand All @@ -115,13 +128,18 @@ export async function POST(req: NextRequest) {
if (Array.isArray(contexts) && contexts.length > 0) {
try {
const { processContextsServer } = await import('@/lib/copilot/process-contents')
const processed = await processContextsServer(contexts as any, authenticatedUserId)
const processed = await processContextsServer(contexts as any, authenticatedUserId, message)
agentContexts = processed
logger.info(`[${tracker.requestId}] Contexts processed for request`, {
processedCount: agentContexts.length,
kinds: agentContexts.map((c) => c.type),
lengthPreview: agentContexts.map((c) => c.content?.length ?? 0),
})
if (Array.isArray(contexts) && contexts.length > 0 && agentContexts.length === 0) {
logger.warn(
`[${tracker.requestId}] Contexts provided but none processed. Check executionId for logs contexts.`
)
}
} catch (e) {
logger.error(`[${tracker.requestId}] Failed to process contexts`, e)
}
Expand Down Expand Up @@ -474,16 +492,6 @@ export async function POST(req: NextRequest) {
break
}

// Check if client disconnected before processing chunk
try {
// Forward the chunk to client immediately
controller.enqueue(value)
} catch (error) {
// Client disconnected - stop reading from sim agent
reader.cancel() // Stop reading from sim agent
break
}

// Decode and parse SSE events for logging and capturing content
const decodedChunk = decoder.decode(value, { stream: true })
buffer += decodedChunk
Expand Down Expand Up @@ -583,6 +591,47 @@ export async function POST(req: NextRequest) {

default:
}

// Emit to client: rewrite 'error' events into user-friendly assistant message
if (event?.type === 'error') {
try {
const displayMessage: string =
(event?.data && (event.data.displayMessage as string)) ||
'Sorry, I encountered an error. Please try again.'
const formatted = `_${displayMessage}_`
// Accumulate so it persists to DB as assistant content
assistantContent += formatted
// Send as content chunk
try {
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify({ type: 'content', data: formatted })}\n\n`
)
)
} catch (enqueueErr) {
reader.cancel()
break
}
// Then close this response cleanly for the client
try {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: 'done' })}\n\n`)
)
} catch (enqueueErr) {
reader.cancel()
break
}
} catch {}
// Do not forward the original error event
} else {
// Forward original event to client
try {
controller.enqueue(encoder.encode(`data: ${jsonStr}\n\n`))
} catch (enqueueErr) {
reader.cancel()
break
}
}
} catch (e) {
// Enhanced error handling for large payloads and parsing issues
const lineLength = line.length
Expand Down Expand Up @@ -615,10 +664,37 @@ export async function POST(req: NextRequest) {
logger.debug(`[${tracker.requestId}] Processing remaining buffer: "${buffer}"`)
if (buffer.startsWith('data: ')) {
try {
const event = JSON.parse(buffer.slice(6))
const jsonStr = buffer.slice(6)
const event = JSON.parse(jsonStr)
if (event.type === 'content' && event.data) {
assistantContent += event.data
}
// Forward remaining event, applying same error rewrite behavior
if (event?.type === 'error') {
const displayMessage: string =
(event?.data && (event.data.displayMessage as string)) ||
'Sorry, I encountered an error. Please try again.'
const formatted = `_${displayMessage}_`
assistantContent += formatted
try {
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify({ type: 'content', data: formatted })}\n\n`
)
)
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: 'done' })}\n\n`)
)
} catch (enqueueErr) {
reader.cancel()
}
} else {
try {
controller.enqueue(encoder.encode(`data: ${jsonStr}\n\n`))
} catch (enqueueErr) {
reader.cancel()
}
}
} catch (e) {
logger.warn(`[${tracker.requestId}] Failed to parse final buffer: "${buffer}"`)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
import { type FC, memo, useEffect, useMemo, useState } from 'react'
import {
Blocks,
BookOpen,
Bot,
Box,
Check,
Clipboard,
Info,
LibraryBig,
Loader2,
RotateCcw,
Shapes,
SquareChevronRight,
ThumbsDown,
ThumbsUp,
Workflow,
Expand Down Expand Up @@ -389,7 +392,9 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
const fromBlock = Array.isArray((block as any)?.contexts)
? ((block as any).contexts as any[])
: []
const allContexts = direct.length > 0 ? direct : fromBlock
const allContexts = (direct.length > 0 ? direct : fromBlock).filter(
(c: any) => c?.kind !== 'current_workflow'
)
const MAX_VISIBLE = 4
const visible = showAllContexts
? allContexts
Expand All @@ -404,14 +409,20 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
>
{ctx?.kind === 'past_chat' ? (
<Bot className='h-3 w-3 text-muted-foreground' />
) : ctx?.kind === 'workflow' ? (
) : ctx?.kind === 'workflow' || ctx?.kind === 'current_workflow' ? (
<Workflow className='h-3 w-3 text-muted-foreground' />
) : ctx?.kind === 'blocks' ? (
<Blocks className='h-3 w-3 text-muted-foreground' />
) : ctx?.kind === 'workflow_block' ? (
<Box className='h-3 w-3 text-muted-foreground' />
) : ctx?.kind === 'knowledge' ? (
<LibraryBig className='h-3 w-3 text-muted-foreground' />
) : ctx?.kind === 'templates' ? (
<Shapes className='h-3 w-3 text-muted-foreground' />
) : ctx?.kind === 'docs' ? (
<BookOpen className='h-3 w-3 text-muted-foreground' />
) : ctx?.kind === 'logs' ? (
<SquareChevronRight className='h-3 w-3 text-muted-foreground' />
) : (
<Info className='h-3 w-3 text-muted-foreground' />
)}
Expand Down Expand Up @@ -500,7 +511,10 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
const contexts: any[] = Array.isArray((message as any).contexts)
? ((message as any).contexts as any[])
: []
const labels = contexts.map((c) => c?.label).filter(Boolean) as string[]
const labels = contexts
.filter((c) => c?.kind !== 'current_workflow')
.map((c) => c?.label)
.filter(Boolean) as string[]
if (!labels.length) return <WordWrap text={text} />

const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
Expand Down
Loading