Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
850447a
Initial commit
Sg312 Jul 8, 2025
76c0c56
Initial lint
Sg312 Jul 8, 2025
b9fa50b
Add db migration
Sg312 Jul 8, 2025
70a5f4e
Lint fix
Sg312 Jul 8, 2025
17513d7
Initial chatbot ui
Sg312 Jul 8, 2025
70a5100
Better formatting
Sg312 Jul 8, 2025
7bc644a
Better formatting
Sg312 Jul 8, 2025
840a028
Add footer fullscreen option
Sg312 Jul 8, 2025
3af1a6e
Lint
Sg312 Jul 8, 2025
b58d877
Spacing
Sg312 Jul 8, 2025
767b63c
Fix spacing
Sg312 Jul 8, 2025
d75751b
Convo update
Sg312 Jul 9, 2025
3460a7b
Convo update
Sg312 Jul 9, 2025
537fbdb
UI fixes
Sg312 Jul 9, 2025
3c7e794
Lint
Sg312 Jul 9, 2025
caccb61
Tool call version
Sg312 Jul 9, 2025
2354909
Lint
Sg312 Jul 9, 2025
a3159bc
Fix streaming bug
Sg312 Jul 9, 2025
4fffc66
Lint
Sg312 Jul 9, 2025
ee66c15
some fixes
Sg312 Jul 9, 2025
d1fe209
Big refactor
Sg312 Jul 9, 2025
02c4112
Lint
Sg312 Jul 9, 2025
ccf5c2f
Works?
Sg312 Jul 9, 2025
776ae06
Lint
Sg312 Jul 9, 2025
4aaa68d
Better
Sg312 Jul 9, 2025
4b60bba
Lint
Sg312 Jul 9, 2025
1b3b85f
Checkpoint
Sg312 Jul 9, 2025
8828237
Lint
Sg312 Jul 9, 2025
c53e950
Remove dead code
Sg312 Jul 9, 2025
07cd6f9
Better ui
Sg312 Jul 9, 2025
82cb609
Lint
Sg312 Jul 9, 2025
c0b8e1a
Modal fixes
Sg312 Jul 9, 2025
c7b77bd
Yaml language basics
Sg312 Jul 9, 2025
6cb15a6
Lint
Sg312 Jul 9, 2025
2a0224f
Initial yaml
Sg312 Jul 9, 2025
bacb6f3
Lint
Sg312 Jul 9, 2025
684a802
Closer
Sg312 Jul 9, 2025
5dc3ba3
Lint
Sg312 Jul 9, 2025
bb9291a
It works??
Sg312 Jul 9, 2025
e37f362
Lint
Sg312 Jul 9, 2025
f173476
Remove logs
Sg312 Jul 9, 2025
cc249c2
Lint
Sg312 Jul 9, 2025
aa343fb
Checkpoint
Sg312 Jul 9, 2025
f6b25bf
Lint
Sg312 Jul 9, 2025
a588317
Get user workflow tool
Sg312 Jul 9, 2025
0b01d4b
Lint
Sg312 Jul 9, 2025
37c4f83
Read workflow checkpoint
Sg312 Jul 9, 2025
6ca8311
Lint
Sg312 Jul 9, 2025
a2827a5
Checkpoint
Sg312 Jul 9, 2025
218041d
Handle loops/parallel
Sg312 Jul 9, 2025
3c1914c
Fix loop/parallel yaml
Sg312 Jul 9, 2025
610ea0b
Yaml fixes
Sg312 Jul 9, 2025
8176b37
Lint
Sg312 Jul 9, 2025
eade867
Comment instead of ff
Sg312 Jul 9, 2025
763d0de
Lint
Sg312 Jul 9, 2025
cfc261d
Move upload button
Sg312 Jul 9, 2025
c45da7b
Lint
Sg312 Jul 9, 2025
5c487f5
Remove json export
Sg312 Jul 9, 2025
ef681d8
Greptile fixes
Sg312 Jul 9, 2025
3c61bc1
lint
Sg312 Jul 9, 2025
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
281 changes: 281 additions & 0 deletions apps/sim/app/api/copilot/docs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import {
type CopilotChat,
type CopilotMessage,
createChat,
generateChatTitle,
generateDocsResponse,
getChat,
updateChat,
} from '@/lib/copilot/service'
import { createLogger } from '@/lib/logs/console-logger'

const logger = createLogger('CopilotDocsAPI')

// Schema for docs queries
const DocsQuerySchema = z.object({
query: z.string().min(1, 'Query is required'),
topK: z.number().min(1).max(20).default(5),
provider: z.string().optional(),
model: z.string().optional(),
stream: z.boolean().optional().default(false),
chatId: z.string().optional(),
workflowId: z.string().optional(),
createNewChat: z.boolean().optional().default(false),
})

/**
* POST /api/copilot/docs
* Ask questions about documentation using RAG
*/
export async function POST(req: NextRequest) {
const requestId = crypto.randomUUID()

try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const body = await req.json()
const { query, topK, provider, model, stream, chatId, workflowId, createNewChat } =
DocsQuerySchema.parse(body)

logger.info(`[${requestId}] Docs RAG query: "${query}"`, {
provider,
model,
topK,
chatId,
workflowId,
createNewChat,
userId: session.user.id,
})

// Handle chat context
let currentChat: CopilotChat | null = null
let conversationHistory: CopilotMessage[] = []

if (chatId) {
// Load existing chat
currentChat = await getChat(chatId, session.user.id)
if (currentChat) {
conversationHistory = currentChat.messages
}
} else if (createNewChat && workflowId) {
// Create new chat
currentChat = await createChat(session.user.id, workflowId)
}

// Generate docs response
const result = await generateDocsResponse(query, conversationHistory, {
topK,
provider,
model,
stream,
workflowId,
requestId,
})

if (stream && result.response instanceof ReadableStream) {
// Handle streaming response with docs sources
logger.info(`[${requestId}] Returning streaming docs response`)

const encoder = new TextEncoder()

return new Response(
new ReadableStream({
async start(controller) {
const reader = (result.response as ReadableStream).getReader()
let accumulatedResponse = ''

try {
// Send initial metadata including sources
const metadata = {
type: 'metadata',
chatId: currentChat?.id,
sources: result.sources,
citations: result.sources.map((source, index) => ({
id: index + 1,
title: source.title,
url: source.url,
})),
metadata: {
requestId,
chunksFound: result.sources.length,
query,
topSimilarity: result.sources[0]?.similarity,
provider,
model,
},
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(metadata)}\n\n`))

while (true) {
const { done, value } = await reader.read()
if (done) break

const chunk = new TextDecoder().decode(value)
// Clean up any object serialization artifacts in streaming content
const cleanedChunk = chunk.replace(/\[object Object\],?/g, '')
accumulatedResponse += cleanedChunk

const contentChunk = {
type: 'content',
content: cleanedChunk,
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(contentChunk)}\n\n`))
}

// Send completion marker first to unblock the user
controller.enqueue(encoder.encode(`data: {"type":"done"}\n\n`))

// Save conversation to database asynchronously (non-blocking)
if (currentChat) {
// Fire-and-forget database save to avoid blocking stream completion
Promise.resolve()
.then(async () => {
try {
const userMessage: CopilotMessage = {
id: crypto.randomUUID(),
role: 'user',
content: query,
timestamp: new Date().toISOString(),
}

const assistantMessage: CopilotMessage = {
id: crypto.randomUUID(),
role: 'assistant',
content: accumulatedResponse,
timestamp: new Date().toISOString(),
citations: result.sources.map((source, index) => ({
id: index + 1,
title: source.title,
url: source.url,
})),
}

const updatedMessages = [
...conversationHistory,
userMessage,
assistantMessage,
]

// Generate title if this is the first message
let updatedTitle = currentChat.title ?? undefined
if (!updatedTitle && conversationHistory.length === 0) {
updatedTitle = await generateChatTitle(query)
}

// Update the chat in database
await updateChat(currentChat.id, session.user.id, {
title: updatedTitle,
messages: updatedMessages,
})

logger.info(
`[${requestId}] Updated chat ${currentChat.id} with new docs messages`
)
} catch (dbError) {
logger.error(`[${requestId}] Failed to save chat to database:`, dbError)
// Database errors don't affect the user's streaming experience
}
})
.catch((error) => {
logger.error(`[${requestId}] Unexpected error in async database save:`, error)
})
}
} catch (error) {
logger.error(`[${requestId}] Docs streaming error:`, error)
try {
const errorChunk = {
type: 'error',
error: 'Streaming failed',
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorChunk)}\n\n`))
} catch (enqueueError) {
logger.error(`[${requestId}] Failed to enqueue error response:`, enqueueError)
}
} finally {
controller.close()
}
},
}),
{
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
}
)
}

// Handle non-streaming response
logger.info(`[${requestId}] Docs RAG response generated successfully`)

// Save conversation to database if we have a chat
if (currentChat) {
const userMessage: CopilotMessage = {
id: crypto.randomUUID(),
role: 'user',
content: query,
timestamp: new Date().toISOString(),
}

const assistantMessage: CopilotMessage = {
id: crypto.randomUUID(),
role: 'assistant',
content: typeof result.response === 'string' ? result.response : '[Streaming Response]',
timestamp: new Date().toISOString(),
citations: result.sources.map((source, index) => ({
id: index + 1,
title: source.title,
url: source.url,
})),
}

const updatedMessages = [...conversationHistory, userMessage, assistantMessage]

// Generate title if this is the first message
let updatedTitle = currentChat.title ?? undefined
if (!updatedTitle && conversationHistory.length === 0) {
updatedTitle = await generateChatTitle(query)
}

// Update the chat in database
await updateChat(currentChat.id, session.user.id, {
title: updatedTitle,
messages: updatedMessages,
})

logger.info(`[${requestId}] Updated chat ${currentChat.id} with new docs messages`)
}

return NextResponse.json({
success: true,
response: result.response,
sources: result.sources,
chatId: currentChat?.id,
metadata: {
requestId,
chunksFound: result.sources.length,
query,
topSimilarity: result.sources[0]?.similarity,
provider,
model,
},
})
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid request data', details: error.errors },
{ status: 400 }
)
}

logger.error(`[${requestId}] Copilot docs error:`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
Loading