diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts
deleted file mode 100644
index 7595c454c2..0000000000
--- a/apps/sim/app/api/copilot/chat/route.ts
+++ /dev/null
@@ -1,497 +0,0 @@
-import { and, eq } from 'drizzle-orm'
-import { type NextRequest, NextResponse } from 'next/server'
-import { z } from 'zod'
-import { getSession } from '@/lib/auth'
-import { createLogger } from '@/lib/logs/console-logger'
-import { getRotatingApiKey } from '@/lib/utils'
-import { db } from '@/db'
-import { copilotChats } from '@/db/schema'
-import { executeProviderRequest } from '@/providers'
-import type { Message } from '@/providers/types'
-
-const logger = createLogger('CopilotChat')
-
-// Configuration for copilot chat
-const COPILOT_CONFIG = {
- defaultProvider: 'anthropic',
- defaultModel: 'claude-3-7-sonnet-latest',
- temperature: 0.1,
- maxTokens: 4000, // Increased for more comprehensive documentation responses
-} as const
-
-const CopilotChatSchema = z.object({
- message: z.string().min(1, 'Message is required'),
- chatId: z.string().optional(),
- workflowId: z.string().optional(),
- createNewChat: z.boolean().optional().default(false),
- stream: z.boolean().optional().default(false),
-})
-
-/**
- * Generate a chat title using LLM based on the first user message
- */
-async function generateChatTitle(userMessage: string): Promise {
- try {
- const apiKey = getRotatingApiKey('anthropic')
-
- const response = await executeProviderRequest('anthropic', {
- model: 'claude-3-haiku-20240307',
- systemPrompt:
- 'You are a helpful assistant that generates concise, descriptive titles for chat conversations. Create a title that captures the main topic or question being discussed. Keep it under 50 characters and make it specific and clear.',
- context: `Generate a concise title for a conversation that starts with this user message: "${userMessage}"
-
-Return only the title text, nothing else.`,
- temperature: 0.3,
- maxTokens: 50,
- apiKey,
- stream: false,
- })
-
- if (typeof response === 'object' && 'content' in response) {
- return response.content?.trim() || 'New Chat'
- }
-
- return 'New Chat'
- } catch (error) {
- logger.error('Failed to generate chat title:', error)
- return 'New Chat'
- }
-}
-
-/**
- * Generate chat response with tool calling support
- */
-interface StreamingChatResponse {
- stream: ReadableStream
- citations: Array<{
- id: number
- title: string
- url: string
- }>
-}
-
-/**
- * Extract citations from provider response that contains tool results
- */
-function extractCitationsFromResponse(response: any): Array<{
- id: number
- title: string
- url: string
-}> {
- // Handle ReadableStream responses
- if (response instanceof ReadableStream) {
- return []
- }
-
- // Handle string responses
- if (typeof response === 'string') {
- return []
- }
-
- // Handle object responses
- if (typeof response !== 'object' || !response) {
- return []
- }
-
- // Check for tool results
- if (!response.toolResults || !Array.isArray(response.toolResults)) {
- return []
- }
-
- const docsSearchResult = response.toolResults.find(
- (result: any) => result.sources && Array.isArray(result.sources)
- )
-
- if (!docsSearchResult || !docsSearchResult.sources) {
- return []
- }
-
- return docsSearchResult.sources.map((source: any) => ({
- id: source.id,
- title: source.title,
- url: source.link,
- }))
-}
-
-async function generateChatResponse(
- message: string,
- conversationHistory: any[] = [],
- stream = false,
- requestId?: string
-): Promise {
- const apiKey = getRotatingApiKey('anthropic')
-
- // Build conversation context
- const messages: Message[] = []
-
- // Add conversation history
- for (const msg of conversationHistory.slice(-10)) {
- // Keep last 10 messages
- messages.push({
- role: msg.role as 'user' | 'assistant' | 'system',
- content: msg.content,
- })
- }
-
- // Add current user message
- messages.push({
- role: 'user',
- content: message,
- })
-
- const systemPrompt = `You are a helpful AI assistant for Sim Studio, a powerful workflow automation platform. You can help users with questions about:
-
-- Creating and managing workflows
-- Using different tools and blocks
-- Understanding features and capabilities
-- Troubleshooting issues
-- Best practices
-
-You have access to the Sim Studio documentation through a search tool. Use it when users ask about Sim Studio features, tools, or functionality.
-
-WHEN TO SEARCH DOCUMENTATION:
-- User asks about specific Sim Studio features or tools
-- User needs help with workflows or blocks
-- User has technical questions about the platform
-- User asks "How do I..." questions about Sim Studio
-
-WHEN NOT TO SEARCH:
-- Simple greetings or casual conversation
-- General programming questions unrelated to Sim Studio
-- Thank you messages or small talk
-
-CITATION FORMAT:
-When you reference information from documentation sources, use this format:
-- Use [1], [2], [3] etc. to cite sources
-- Place citations at the end of sentences that reference specific information
-- Each source should only be cited once in your response
-- Continue your full response after adding citations - don't stop mid-answer
-
-IMPORTANT: Always provide complete, helpful responses. If you add citations, continue writing your full answer. Do not stop your response after adding a citation.`
-
- // Define the documentation search tool for the LLM
- const tools = [
- {
- id: 'docs_search_internal',
- name: 'Search Documentation',
- description:
- 'Search Sim Studio documentation for information about features, tools, workflows, and functionality',
- params: {},
- parameters: {
- type: 'object',
- properties: {
- query: {
- type: 'string',
- description: 'The search query to find relevant documentation',
- },
- topK: {
- type: 'number',
- description: 'Number of results to return (default: 5, max: 10)',
- default: 5,
- },
- },
- required: ['query'],
- },
- },
- ]
-
- try {
- // For streaming, we always make a non-streaming request first to handle tool calls
- // Then we stream the final response if no tool calls were needed
- const response = await executeProviderRequest('anthropic', {
- model: COPILOT_CONFIG.defaultModel,
- systemPrompt,
- messages,
- tools,
- temperature: COPILOT_CONFIG.temperature,
- maxTokens: COPILOT_CONFIG.maxTokens,
- apiKey,
- stream: false, // Always start with non-streaming to handle tool calls
- })
-
- // If this is a streaming request and we got a regular response,
- // we need to create a streaming response from the content
- if (stream && typeof response === 'object' && 'content' in response) {
- const content = response.content || 'Sorry, I could not generate a response.'
-
- // Extract citations from the provider response for later use
- const responseCitations = extractCitationsFromResponse(response)
-
- // Create a ReadableStream that emits the content in character chunks
- const streamResponse = new ReadableStream({
- start(controller) {
- // Use character-based streaming for more reliable transmission
- const chunkSize = 8 // Stream 8 characters at a time for smooth experience
- let index = 0
-
- const pushNext = () => {
- if (index < content.length) {
- const chunk = content.slice(index, index + chunkSize)
- controller.enqueue(new TextEncoder().encode(chunk))
- index += chunkSize
-
- // Add a small delay to simulate streaming
- setTimeout(pushNext, 25)
- } else {
- controller.close()
- }
- }
-
- pushNext()
- },
- })
-
- // Store citations for later use in the main streaming handler
-
- ;(streamResponse as any)._citations = responseCitations
-
- return streamResponse
- }
-
- // Handle regular response
- if (typeof response === 'object' && 'content' in response) {
- return response.content || 'Sorry, I could not generate a response.'
- }
-
- return 'Sorry, I could not generate a response.'
- } catch (error) {
- logger.error('Failed to generate chat response:', error)
- throw new Error(
- `Failed to generate response: ${error instanceof Error ? error.message : 'Unknown error'}`
- )
- }
-}
-
-/**
- * POST /api/copilot/chat
- * Chat with the copilot using LLM with tool calling
- */
-export async function POST(req: NextRequest) {
- const requestId = crypto.randomUUID()
-
- try {
- const body = await req.json()
- const { message, chatId, workflowId, createNewChat, stream } = CopilotChatSchema.parse(body)
-
- const session = await getSession()
-
- logger.info(`[${requestId}] Copilot chat message: "${message}"`, {
- chatId,
- workflowId,
- createNewChat,
- stream,
- })
-
- // Handle chat context
- let currentChat: any = null
- let conversationHistory: any[] = []
-
- if (chatId && session?.user?.id) {
- // Load existing chat
- const [existingChat] = await db
- .select()
- .from(copilotChats)
- .where(and(eq(copilotChats.id, chatId), eq(copilotChats.userId, session.user.id)))
- .limit(1)
-
- if (existingChat) {
- currentChat = existingChat
- conversationHistory = Array.isArray(existingChat.messages) ? existingChat.messages : []
- }
- } else if (createNewChat && workflowId && session?.user?.id) {
- // Create new chat
- const [newChat] = await db
- .insert(copilotChats)
- .values({
- userId: session.user.id,
- workflowId,
- title: null,
- model: COPILOT_CONFIG.defaultModel,
- messages: [],
- })
- .returning()
-
- if (newChat) {
- currentChat = newChat
- conversationHistory = []
- }
- }
-
- // Generate chat response
- const response = await generateChatResponse(message, conversationHistory, stream, requestId)
-
- // Handle streaming response
- if (response instanceof ReadableStream) {
- logger.info(`[${requestId}] Returning streaming response`)
-
- const encoder = new TextEncoder()
- // Extract citations from the stream object if available
- const citations = (response as any)._citations || []
-
- return new Response(
- new ReadableStream({
- async start(controller) {
- const reader = response.getReader()
- let accumulatedResponse = ''
-
- // Send initial metadata
- const metadata = {
- type: 'metadata',
- chatId: currentChat?.id,
- citations: citations,
- metadata: {
- requestId,
- message,
- },
- }
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(metadata)}\n\n`))
-
- try {
- while (true) {
- const { done, value } = await reader.read()
- if (done) break
-
- const chunkText = new TextDecoder().decode(value)
- accumulatedResponse += chunkText
-
- const contentChunk = {
- type: 'content',
- content: chunkText,
- }
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(contentChunk)}\n\n`))
- }
-
- // Save conversation to database after streaming completes
- if (currentChat && session?.user?.id) {
- const userMessage = {
- id: crypto.randomUUID(),
- role: 'user',
- content: message,
- timestamp: new Date().toISOString(),
- }
-
- const assistantMessage = {
- id: crypto.randomUUID(),
- role: 'assistant',
- content: accumulatedResponse,
- timestamp: new Date().toISOString(),
- citations: citations.length > 0 ? citations : undefined,
- }
-
- const updatedMessages = [...conversationHistory, userMessage, assistantMessage]
-
- // Generate title if this is the first message
- let updatedTitle = currentChat.title
- if (!updatedTitle && conversationHistory.length === 0) {
- updatedTitle = await generateChatTitle(message)
- }
-
- await db
- .update(copilotChats)
- .set({
- title: updatedTitle,
- messages: updatedMessages,
- updatedAt: new Date(),
- })
- .where(eq(copilotChats.id, currentChat.id))
-
- logger.info(`[${requestId}] Updated chat ${currentChat.id} with new messages`)
- }
-
- controller.enqueue(encoder.encode(`data: {"type":"done"}\n\n`))
- } catch (error) {
- logger.error(`[${requestId}] Streaming error:`, error)
- const errorChunk = {
- type: 'error',
- error: 'Streaming failed',
- }
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorChunk)}\n\n`))
- } finally {
- controller.close()
- }
- },
- }),
- {
- headers: {
- 'Content-Type': 'text/event-stream',
- 'Cache-Control': 'no-cache',
- Connection: 'keep-alive',
- },
- }
- )
- }
-
- // Save conversation to database for non-streaming response
- if (currentChat && session?.user?.id) {
- const userMessage = {
- id: crypto.randomUUID(),
- role: 'user',
- content: message,
- timestamp: new Date().toISOString(),
- }
-
- // Extract citations from response if available
- const citations = extractCitationsFromResponse(response)
-
- const assistantMessage = {
- id: crypto.randomUUID(),
- role: 'assistant',
- content:
- typeof response === 'string'
- ? response
- : (typeof response === 'object' && 'content' in response
- ? response.content
- : '[Error generating response]') || '[Error generating response]',
- timestamp: new Date().toISOString(),
- citations: citations.length > 0 ? citations : undefined,
- }
-
- const updatedMessages = [...conversationHistory, userMessage, assistantMessage]
-
- // Generate title if this is the first message
- let updatedTitle = currentChat.title
- if (!updatedTitle && conversationHistory.length === 0) {
- updatedTitle = await generateChatTitle(message)
- }
-
- await db
- .update(copilotChats)
- .set({
- title: updatedTitle,
- messages: updatedMessages,
- updatedAt: new Date(),
- })
- .where(eq(copilotChats.id, currentChat.id))
-
- logger.info(`[${requestId}] Updated chat ${currentChat.id} with new messages`)
- }
-
- logger.info(`[${requestId}] Chat response generated successfully`)
-
- return NextResponse.json({
- success: true,
- response:
- typeof response === 'string'
- ? response
- : (typeof response === 'object' && 'content' in response
- ? response.content
- : '[Error generating response]') || '[Error generating response]',
- chatId: currentChat?.id,
- citations: extractCitationsFromResponse(response),
- metadata: {
- requestId,
- message,
- },
- })
- } catch (error) {
- if (error instanceof z.ZodError) {
- return NextResponse.json(
- { error: 'Invalid request data', details: error.errors },
- { status: 400 }
- )
- }
-
- logger.error(`[${requestId}] Copilot chat error:`, error)
- return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
- }
-}
diff --git a/apps/sim/app/api/copilot/chats/[id]/route.ts b/apps/sim/app/api/copilot/chats/[id]/route.ts
deleted file mode 100644
index f89098fca2..0000000000
--- a/apps/sim/app/api/copilot/chats/[id]/route.ts
+++ /dev/null
@@ -1,217 +0,0 @@
-import { and, eq } from 'drizzle-orm'
-import { type NextRequest, NextResponse } from 'next/server'
-import { z } from 'zod'
-import { getSession } from '@/lib/auth'
-import { createLogger } from '@/lib/logs/console-logger'
-import { getRotatingApiKey } from '@/lib/utils'
-import { db } from '@/db'
-import { copilotChats } from '@/db/schema'
-import { executeProviderRequest } from '@/providers'
-
-const logger = createLogger('CopilotChatAPI')
-
-const UpdateChatSchema = z.object({
- title: z.string().optional(),
- messages: z.array(z.any()).optional(),
- model: z.string().optional(),
-})
-
-const AddMessageSchema = z.object({
- message: z.object({
- role: z.enum(['user', 'assistant']),
- content: z.string(),
- timestamp: z.string().optional(),
- citations: z.array(z.any()).optional(),
- }),
-})
-
-/**
- * GET /api/copilot/chats/[id]
- * Get a specific copilot chat
- */
-export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
- try {
- const session = await getSession()
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const chatId = params.id
-
- logger.info(`Getting chat ${chatId} for user ${session.user.id}`)
-
- const [chat] = await db
- .select()
- .from(copilotChats)
- .where(and(eq(copilotChats.id, chatId), eq(copilotChats.userId, session.user.id)))
- .limit(1)
-
- if (!chat) {
- return NextResponse.json({ error: 'Chat not found' }, { status: 404 })
- }
-
- return NextResponse.json({
- success: true,
- chat: {
- id: chat.id,
- title: chat.title,
- model: chat.model,
- messages: chat.messages,
- createdAt: chat.createdAt,
- updatedAt: chat.updatedAt,
- messageCount: Array.isArray(chat.messages) ? chat.messages.length : 0,
- },
- })
- } catch (error) {
- logger.error('Failed to get copilot chat:', error)
- return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
- }
-}
-
-/**
- * PUT /api/copilot/chats/[id]
- * Update a copilot chat (add messages, update title, etc.)
- */
-export async function PUT(req: NextRequest, { params }: { params: { id: string } }) {
- try {
- const session = await getSession()
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const chatId = params.id
- const body = await req.json()
- const { title, messages, model } = UpdateChatSchema.parse(body)
-
- logger.info(`Updating chat ${chatId} for user ${session.user.id}`)
-
- // First verify the chat exists and belongs to the user
- const [existingChat] = await db
- .select()
- .from(copilotChats)
- .where(and(eq(copilotChats.id, chatId), eq(copilotChats.userId, session.user.id)))
- .limit(1)
-
- if (!existingChat) {
- return NextResponse.json({ error: 'Chat not found' }, { status: 404 })
- }
-
- // Prepare update data
- const updateData: any = {
- updatedAt: new Date(),
- }
-
- if (title !== undefined) updateData.title = title
- if (messages !== undefined) updateData.messages = messages
- if (model !== undefined) updateData.model = model
-
- // Update the chat
- const [updatedChat] = await db
- .update(copilotChats)
- .set(updateData)
- .where(eq(copilotChats.id, chatId))
- .returning()
-
- if (!updatedChat) {
- throw new Error('Failed to update chat')
- }
-
- logger.info(`Updated chat ${chatId} for user ${session.user.id}`)
-
- return NextResponse.json({
- success: true,
- chat: {
- id: updatedChat.id,
- title: updatedChat.title,
- model: updatedChat.model,
- messages: updatedChat.messages,
- createdAt: updatedChat.createdAt,
- updatedAt: updatedChat.updatedAt,
- messageCount: Array.isArray(updatedChat.messages) ? updatedChat.messages.length : 0,
- },
- })
- } catch (error) {
- if (error instanceof z.ZodError) {
- return NextResponse.json(
- { error: 'Invalid request data', details: error.errors },
- { status: 400 }
- )
- }
-
- logger.error('Failed to update copilot chat:', error)
- return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
- }
-}
-
-/**
- * DELETE /api/copilot/chats/[id]
- * Delete a copilot chat
- */
-export async function DELETE(req: NextRequest, { params }: { params: { id: string } }) {
- try {
- const session = await getSession()
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const chatId = params.id
-
- logger.info(`Deleting chat ${chatId} for user ${session.user.id}`)
-
- // First verify the chat exists and belongs to the user
- const [existingChat] = await db
- .select({ id: copilotChats.id })
- .from(copilotChats)
- .where(and(eq(copilotChats.id, chatId), eq(copilotChats.userId, session.user.id)))
- .limit(1)
-
- if (!existingChat) {
- return NextResponse.json({ error: 'Chat not found' }, { status: 404 })
- }
-
- // Delete the chat
- await db.delete(copilotChats).where(eq(copilotChats.id, chatId))
-
- logger.info(`Deleted chat ${chatId} for user ${session.user.id}`)
-
- return NextResponse.json({
- success: true,
- message: 'Chat deleted successfully',
- })
- } catch (error) {
- logger.error('Failed to delete copilot chat:', error)
- return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
- }
-}
-
-/**
- * Generate a chat title using LLM based on the first user message
- */
-export async function generateChatTitle(userMessage: string): Promise {
- try {
- const apiKey = getRotatingApiKey('anthropic')
-
- const response = await executeProviderRequest('anthropic', {
- model: 'claude-3-haiku-20240307', // Use faster, cheaper model for title generation
- systemPrompt:
- 'You are a helpful assistant that generates concise, descriptive titles for chat conversations. Create a title that captures the main topic or question being discussed. Keep it under 50 characters and make it specific and clear.',
- context: `Generate a concise title for a conversation that starts with this user message: "${userMessage}"
-
-Return only the title text, nothing else.`,
- temperature: 0.3,
- maxTokens: 50,
- apiKey,
- stream: false,
- })
-
- // Handle different response types
- if (typeof response === 'object' && 'content' in response) {
- return response.content?.trim() || 'New Chat'
- }
-
- return 'New Chat'
- } catch (error) {
- logger.error('Failed to generate chat title:', error)
- return 'New Chat' // Fallback title
- }
-}
diff --git a/apps/sim/app/api/copilot/chats/route.ts b/apps/sim/app/api/copilot/chats/route.ts
deleted file mode 100644
index 69cbd700f8..0000000000
--- a/apps/sim/app/api/copilot/chats/route.ts
+++ /dev/null
@@ -1,176 +0,0 @@
-import { and, desc, eq } from 'drizzle-orm'
-import { type NextRequest, NextResponse } from 'next/server'
-import { z } from 'zod'
-import { getSession } from '@/lib/auth'
-import { createLogger } from '@/lib/logs/console-logger'
-import { db } from '@/db'
-import { copilotChats } from '@/db/schema'
-
-const logger = createLogger('CopilotChatsAPI')
-
-const CreateChatSchema = z.object({
- workflowId: z.string().min(1, 'Workflow ID is required'),
- title: z.string().optional(),
- model: z.string().optional().default('claude-3-7-sonnet-latest'),
- initialMessage: z.string().optional(), // Optional first user message
-})
-
-const ListChatsSchema = z.object({
- workflowId: z.string().min(1, 'Workflow ID is required'),
- limit: z.number().min(1).max(100).optional().default(50),
- offset: z.number().min(0).optional().default(0),
-})
-
-/**
- * GET /api/copilot/chats
- * List copilot chats for a user and workflow
- */
-export async function GET(req: NextRequest) {
- try {
- const session = await getSession()
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const { searchParams } = new URL(req.url)
- const workflowId = searchParams.get('workflowId')
- const limit = Number.parseInt(searchParams.get('limit') || '50')
- const offset = Number.parseInt(searchParams.get('offset') || '0')
-
- const {
- workflowId: validatedWorkflowId,
- limit: validatedLimit,
- offset: validatedOffset,
- } = ListChatsSchema.parse({ workflowId, limit, offset })
-
- logger.info(`Listing chats for user ${session.user.id}, workflow ${validatedWorkflowId}`)
-
- const chats = await db
- .select({
- id: copilotChats.id,
- title: copilotChats.title,
- model: copilotChats.model,
- createdAt: copilotChats.createdAt,
- updatedAt: copilotChats.updatedAt,
- messageCount: copilotChats.messages, // We'll process this to get count
- })
- .from(copilotChats)
- .where(
- and(
- eq(copilotChats.userId, session.user.id),
- eq(copilotChats.workflowId, validatedWorkflowId)
- )
- )
- .orderBy(desc(copilotChats.updatedAt))
- .limit(validatedLimit)
- .offset(validatedOffset)
-
- // Process the results to add message counts and clean up data
- const processedChats = chats.map((chat) => ({
- id: chat.id,
- title: chat.title,
- model: chat.model,
- createdAt: chat.createdAt,
- updatedAt: chat.updatedAt,
- messageCount: Array.isArray(chat.messageCount) ? chat.messageCount.length : 0,
- }))
-
- return NextResponse.json({
- success: true,
- chats: processedChats,
- pagination: {
- limit: validatedLimit,
- offset: validatedOffset,
- total: processedChats.length,
- },
- })
- } catch (error) {
- if (error instanceof z.ZodError) {
- return NextResponse.json(
- { error: 'Invalid request parameters', details: error.errors },
- { status: 400 }
- )
- }
-
- logger.error('Failed to list copilot chats:', error)
- return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
- }
-}
-
-/**
- * POST /api/copilot/chats
- * Create a new copilot chat
- */
-export async function POST(req: NextRequest) {
- try {
- const session = await getSession()
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const body = await req.json()
- const { workflowId, title, model, initialMessage } = CreateChatSchema.parse(body)
-
- logger.info(`Creating new chat for user ${session.user.id}, workflow ${workflowId}`)
-
- // Prepare initial messages array
- const initialMessages = initialMessage
- ? [
- {
- id: crypto.randomUUID(),
- role: 'user',
- content: initialMessage,
- timestamp: new Date().toISOString(),
- },
- ]
- : []
-
- // Create the chat
- const [newChat] = await db
- .insert(copilotChats)
- .values({
- userId: session.user.id,
- workflowId,
- title: title || null, // Will be generated later if null
- model,
- messages: initialMessages,
- })
- .returning({
- id: copilotChats.id,
- title: copilotChats.title,
- model: copilotChats.model,
- messages: copilotChats.messages,
- createdAt: copilotChats.createdAt,
- updatedAt: copilotChats.updatedAt,
- })
-
- if (!newChat) {
- throw new Error('Failed to create chat')
- }
-
- logger.info(`Created chat ${newChat.id} for user ${session.user.id}`)
-
- return NextResponse.json({
- success: true,
- chat: {
- id: newChat.id,
- title: newChat.title,
- model: newChat.model,
- messages: newChat.messages,
- createdAt: newChat.createdAt,
- updatedAt: newChat.updatedAt,
- messageCount: Array.isArray(newChat.messages) ? newChat.messages.length : 0,
- },
- })
- } catch (error) {
- if (error instanceof z.ZodError) {
- return NextResponse.json(
- { error: 'Invalid request data', details: error.errors },
- { status: 400 }
- )
- }
-
- logger.error('Failed to create copilot chat:', error)
- return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
- }
-}
diff --git a/apps/sim/app/api/copilot/docs/route.ts b/apps/sim/app/api/copilot/docs/route.ts
new file mode 100644
index 0000000000..60f8d3502f
--- /dev/null
+++ b/apps/sim/app/api/copilot/docs/route.ts
@@ -0,0 +1,257 @@
+import { type NextRequest, NextResponse } from 'next/server'
+import { z } from 'zod'
+import { getSession } from '@/lib/auth'
+import { createLogger } from '@/lib/logs/console-logger'
+import {
+ generateDocsResponse,
+ getChat,
+ createChat,
+ updateChat,
+ generateChatTitle,
+} from '@/lib/copilot/service'
+
+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: any = null
+ let conversationHistory: any[] = []
+
+ 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`))
+ }
+
+ // Save conversation to database after streaming completes
+ if (currentChat) {
+ const userMessage = {
+ id: crypto.randomUUID(),
+ role: 'user',
+ content: query,
+ timestamp: new Date().toISOString(),
+ }
+
+ const assistantMessage = {
+ 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
+ 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`)
+ }
+
+ // Send completion marker
+ controller.enqueue(encoder.encode(`data: {"type":"done"}\n\n`))
+ } catch (error) {
+ logger.error(`[${requestId}] Docs streaming error:`, error)
+ const errorChunk = {
+ type: 'error',
+ error: 'Streaming failed',
+ }
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorChunk)}\n\n`))
+ } 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 = {
+ id: crypto.randomUUID(),
+ role: 'user',
+ content: query,
+ timestamp: new Date().toISOString(),
+ }
+
+ const assistantMessage = {
+ 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
+ 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 })
+ }
+}
\ No newline at end of file
diff --git a/apps/sim/app/api/copilot/route.ts b/apps/sim/app/api/copilot/route.ts
index e9c2ad421c..132470aa82 100644
--- a/apps/sim/app/api/copilot/route.ts
+++ b/apps/sim/app/api/copilot/route.ts
@@ -1,214 +1,302 @@
-import { NextResponse } from 'next/server'
-import { OpenAI } from 'openai'
-import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions'
+import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
+import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
+import {
+ sendMessage,
+ createChat,
+ getChat,
+ listChats,
+ deleteChat,
+ generateDocsResponse,
+ type CopilotMessage,
+} from '@/lib/copilot/service'
const logger = createLogger('CopilotAPI')
-const MessageSchema = z.object({
- role: z.enum(['user', 'assistant', 'system']),
- content: z.string(),
+// Schema for sending messages
+const SendMessageSchema = z.object({
+ message: z.string().min(1, 'Message is required'),
+ chatId: z.string().optional(),
+ workflowId: z.string().optional(),
+ createNewChat: z.boolean().optional().default(false),
+ stream: z.boolean().optional().default(false),
})
-const RequestSchema = z.object({
- messages: z.array(MessageSchema),
- workflowState: z.object({
- blocks: z.record(z.any()),
- edges: z.array(z.any()),
- }),
+// 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),
})
-const workflowActions = {
- addBlock: {
- description: 'Add one new block to the workflow',
- parameters: {
- type: 'object',
- required: ['type'],
- properties: {
- type: {
- type: 'string',
- enum: ['agent', 'api', 'condition', 'function', 'router'],
- description: 'The type of block to add',
- },
- name: {
- type: 'string',
- description:
- 'Optional custom name for the block. Do not provide a name unless the user has specified it.',
- },
- position: {
- type: 'object',
- description:
- 'Optional position for the block. Do not provide a position unless the user has specified it.',
- properties: {
- x: { type: 'number' },
- y: { type: 'number' },
- },
- },
- },
- },
- },
- addEdge: {
- description: 'Create a connection (edge) between two blocks',
- parameters: {
- type: 'object',
- required: ['sourceId', 'targetId'],
- properties: {
- sourceId: {
- type: 'string',
- description: 'ID of the source block',
- },
- targetId: {
- type: 'string',
- description: 'ID of the target block',
- },
- sourceHandle: {
- type: 'string',
- description: 'Optional handle identifier for the source connection point',
- },
- targetHandle: {
- type: 'string',
- description: 'Optional handle identifier for the target connection point',
- },
- },
- },
- },
- removeBlock: {
- description: 'Remove a block from the workflow',
- parameters: {
- type: 'object',
- required: ['id'],
- properties: {
- id: { type: 'string', description: 'ID of the block to remove' },
- },
- },
- },
- removeEdge: {
- description: 'Remove a connection (edge) between blocks',
- parameters: {
- type: 'object',
- required: ['id'],
- properties: {
- id: { type: 'string', description: 'ID of the edge to remove' },
- },
- },
- },
-}
+// Schema for creating chats
+const CreateChatSchema = z.object({
+ workflowId: z.string().min(1, 'Workflow ID is required'),
+ title: z.string().optional(),
+ initialMessage: z.string().optional(),
+})
-// System prompt that references workflow state
-const getSystemPrompt = (workflowState: any) => {
- const blockCount = Object.keys(workflowState.blocks).length
- const edgeCount = workflowState.edges.length
+// Schema for listing chats
+const ListChatsSchema = z.object({
+ workflowId: z.string().min(1, 'Workflow ID is required'),
+ limit: z.number().min(1).max(100).optional().default(50),
+ offset: z.number().min(0).optional().default(0),
+})
- // Create a summary of existing blocks
- const blockSummary = Object.values(workflowState.blocks)
- .map((block: any) => `- ${block.type} block named "${block.name}" with id ${block.id}`)
- .join('\n')
-
- // Create a summary of existing edges
- const edgeSummary = workflowState.edges
- .map((edge: any) => `- ${edge.source} -> ${edge.target} with id ${edge.id}`)
- .join('\n')
-
- return `You are a workflow assistant that helps users modify their workflow by adding/removing blocks and connections.
-
-Current Workflow State:
-${
- blockCount === 0
- ? 'The workflow is empty.'
- : `${blockSummary}
-
-Connections:
-${edgeCount === 0 ? 'No connections between blocks.' : edgeSummary}`
-}
-
-When users request changes:
-- Consider existing blocks when suggesting connections
-- Provide clear feedback about what actions you've taken
-
-Use the following functions to modify the workflow:
-1. Use the addBlock function to create a new block
-2. Use the addEdge function to connect one block to another
-3. Use the removeBlock function to remove a block
-4. Use the removeEdge function to remove a connection
-
-Only use the provided functions and respond naturally to the user's requests.`
-}
-
-export async function POST(request: Request) {
- const requestId = crypto.randomUUID().slice(0, 8)
+/**
+ * POST /api/copilot
+ * Send a message to the copilot
+ */
+export async function POST(req: NextRequest) {
+ const requestId = crypto.randomUUID()
try {
- // Validate API key
- const apiKey = request.headers.get('X-OpenAI-Key')
- if (!apiKey) {
- return NextResponse.json({ error: 'OpenAI API key is required' }, { status: 401 })
+ const body = await req.json()
+ const { message, chatId, workflowId, createNewChat, stream } = SendMessageSchema.parse(body)
+
+ const session = await getSession()
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
- // Parse and validate request body
- const body = await request.json()
- const validatedData = RequestSchema.parse(body)
- const { messages, workflowState } = validatedData
-
- // Initialize OpenAI client
- const openai = new OpenAI({ apiKey })
-
- // Create message history with workflow context
- const messageHistory = [
- { role: 'system', content: getSystemPrompt(workflowState) },
- ...messages,
- ]
-
- // Make OpenAI API call with workflow context
- const completion = await openai.chat.completions.create({
- model: 'gpt-4o',
- messages: messageHistory as ChatCompletionMessageParam[],
- tools: Object.entries(workflowActions).map(([name, config]) => ({
- type: 'function',
- function: {
- name,
- description: config.description,
- parameters: config.parameters,
- },
- })),
- tool_choice: 'auto',
+ logger.info(`[${requestId}] Copilot message: "${message}"`, {
+ chatId,
+ workflowId,
+ createNewChat,
+ stream,
+ userId: session.user.id,
})
- const message = completion.choices[0].message
+ // Send message using the service
+ const result = await sendMessage({
+ message,
+ chatId,
+ workflowId,
+ createNewChat,
+ stream,
+ userId: session.user.id,
+ })
- // Process tool calls if present
- if (message.tool_calls) {
- logger.debug(`[${requestId}] Tool calls:`, {
- toolCalls: message.tool_calls,
- })
- const actions = message.tool_calls.map((call) => ({
- name: call.function.name,
- parameters: JSON.parse(call.function.arguments),
- }))
+ // Handle streaming response
+ if (result.response instanceof ReadableStream) {
+ logger.info(`[${requestId}] Returning streaming response`)
- return NextResponse.json({
- message: message.content || "I've updated the workflow based on your request.",
- actions,
- })
+ const encoder = new TextEncoder()
+
+ return new Response(
+ new ReadableStream({
+ async start(controller) {
+ const reader = (result.response as ReadableStream).getReader()
+ let accumulatedResponse = ''
+
+ // Send initial metadata
+ const metadata = {
+ type: 'metadata',
+ chatId: result.chatId,
+ citations: result.citations || [],
+ metadata: {
+ requestId,
+ message,
+ },
+ }
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(metadata)}\n\n`))
+
+ try {
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+
+ const chunkText = new TextDecoder().decode(value)
+ accumulatedResponse += chunkText
+
+ const contentChunk = {
+ type: 'content',
+ content: chunkText,
+ }
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(contentChunk)}\n\n`))
+ }
+
+ // Send completion signal
+ const completion = {
+ type: 'complete',
+ finalContent: accumulatedResponse,
+ }
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(completion)}\n\n`))
+ controller.close()
+ } catch (error) {
+ logger.error(`[${requestId}] Streaming error:`, error)
+ const errorChunk = {
+ type: 'error',
+ error: 'Streaming failed',
+ }
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorChunk)}\n\n`))
+ controller.close()
+ }
+ },
+ }),
+ {
+ headers: {
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache',
+ Connection: 'keep-alive',
+ },
+ }
+ )
}
- // Return response with no actions
+ // Handle non-streaming response
+ logger.info(`[${requestId}] Chat response generated successfully`)
+
return NextResponse.json({
- message:
- message.content ||
- "I'm not sure what changes to make to the workflow. Can you please provide more specific instructions?",
+ success: true,
+ response: result.response,
+ chatId: result.chatId,
+ citations: result.citations || [],
+ metadata: {
+ requestId,
+ message,
+ },
})
} catch (error) {
- logger.error(`[${requestId}] Copilot API error:`, { error })
-
- // Handle specific error types
if (error instanceof z.ZodError) {
return NextResponse.json(
- { error: 'Invalid request format', details: error.errors },
+ { error: 'Invalid request data', details: error.errors },
{ status: 400 }
)
}
- return NextResponse.json({ error: 'Failed to process copilot message' }, { status: 500 })
+ logger.error(`[${requestId}] Copilot error:`, error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
+
+/**
+ * GET /api/copilot
+ * List chats or get a specific chat
+ */
+export async function GET(req: NextRequest) {
+ try {
+ const session = await getSession()
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const { searchParams } = new URL(req.url)
+ const chatId = searchParams.get('chatId')
+
+ // If chatId is provided, get specific chat
+ if (chatId) {
+ const chat = await getChat(chatId, session.user.id)
+ if (!chat) {
+ return NextResponse.json({ error: 'Chat not found' }, { status: 404 })
+ }
+
+ return NextResponse.json({
+ success: true,
+ chat,
+ })
+ }
+
+ // Otherwise, list chats
+ const workflowId = searchParams.get('workflowId')
+ const limit = Number.parseInt(searchParams.get('limit') || '50')
+ const offset = Number.parseInt(searchParams.get('offset') || '0')
+
+ if (!workflowId) {
+ return NextResponse.json(
+ { error: 'workflowId is required for listing chats' },
+ { status: 400 }
+ )
+ }
+
+ const chats = await listChats(session.user.id, workflowId, { limit, offset })
+
+ return NextResponse.json({
+ success: true,
+ chats,
+ })
+ } catch (error) {
+ logger.error('Failed to handle GET request:', error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
+
+/**
+ * PUT /api/copilot
+ * Create a new chat
+ */
+export async function PUT(req: NextRequest) {
+ try {
+ const session = await getSession()
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const body = await req.json()
+ const { workflowId, title, initialMessage } = CreateChatSchema.parse(body)
+
+ logger.info(`Creating new chat for user ${session.user.id}, workflow ${workflowId}`)
+
+ const chat = await createChat(session.user.id, workflowId, {
+ title,
+ initialMessage,
+ })
+
+ logger.info(`Created chat ${chat.id} for user ${session.user.id}`)
+
+ return NextResponse.json({
+ success: true,
+ chat,
+ })
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ { error: 'Invalid request data', details: error.errors },
+ { status: 400 }
+ )
+ }
+
+ logger.error('Failed to create chat:', error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
+
+/**
+ * DELETE /api/copilot
+ * Delete a chat
+ */
+export async function DELETE(req: NextRequest) {
+ try {
+ const session = await getSession()
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const { searchParams } = new URL(req.url)
+ const chatId = searchParams.get('chatId')
+
+ if (!chatId) {
+ return NextResponse.json({ error: 'chatId is required' }, { status: 400 })
+ }
+
+ const success = await deleteChat(chatId, session.user.id)
+
+ if (!success) {
+ return NextResponse.json({ error: 'Chat not found or access denied' }, { status: 404 })
+ }
+
+ return NextResponse.json({
+ success: true,
+ message: 'Chat deleted successfully',
+ })
+ } catch (error) {
+ logger.error('Failed to delete chat:', error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
diff --git a/apps/sim/app/api/docs/ask/route.ts b/apps/sim/app/api/docs/ask/route.ts
index 56245fe50d..7e2c048df8 100644
--- a/apps/sim/app/api/docs/ask/route.ts
+++ b/apps/sim/app/api/docs/ask/route.ts
@@ -2,29 +2,16 @@ import { and, eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
-import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
-import { getRotatingApiKey } from '@/lib/utils'
import { generateEmbeddings } from '@/app/api/knowledge/utils'
import { db } from '@/db'
import { copilotChats, docsEmbeddings } from '@/db/schema'
import { executeProviderRequest } from '@/providers'
-import { getProviderDefaultModel } from '@/providers/models'
+import { getApiKey } from '@/providers/utils'
+import { getCopilotConfig, getCopilotModel } from '@/lib/copilot/config'
const logger = createLogger('DocsRAG')
-// Configuration for docs RAG
-const DOCS_RAG_CONFIG = {
- // Default provider for docs RAG - change this constant to switch providers
- defaultProvider: 'anthropic', // Options: 'openai', 'anthropic', 'deepseek', 'google', 'xai', etc.
- // Default model for docs RAG - will use provider's default if not specified
- defaultModel: 'claude-3-7-sonnet-latest', // e.g., 'gpt-4o-mini', 'claude-3-5-sonnet-latest', 'deepseek-chat'
- // Temperature for response generation
- temperature: 0.1,
- // Max tokens for response
- maxTokens: 1000,
-} as const
-
const DocsQuerySchema = z.object({
query: z.string().min(1, 'Query is required'),
topK: z.number().min(1).max(20).default(10),
@@ -42,10 +29,23 @@ const DocsQuerySchema = z.object({
*/
async function generateChatTitle(userMessage: string): Promise {
try {
- const apiKey = getRotatingApiKey('anthropic')
+ const { provider, model } = getCopilotModel('title')
+ let apiKey: string
+ try {
+ // Use rotating key directly for hosted providers
+ if ((provider === 'openai' || provider === 'anthropic')) {
+ const { getRotatingApiKey } = require('@/lib/utils')
+ apiKey = getRotatingApiKey(provider)
+ } else {
+ apiKey = getApiKey(provider, model)
+ }
+ } catch (error) {
+ logger.error(`Failed to get API key for title generation (${provider} ${model}):`, error)
+ return 'New Chat' // Fallback if API key is not available
+ }
- const response = await executeProviderRequest('anthropic', {
- model: 'claude-3-haiku-20240307', // Use faster, cheaper model for title generation
+ const response = await executeProviderRequest(provider, {
+ model,
systemPrompt:
'You are a helpful assistant that generates concise, descriptive titles for chat conversations. Create a title that captures the main topic or question being discussed. Keep it under 50 characters and make it specific and clear.',
context: `Generate a concise title for a conversation that starts with this user message: "${userMessage}"
@@ -119,29 +119,25 @@ async function generateResponse(
stream = false,
conversationHistory: any[] = []
): Promise {
- // Determine which provider and model to use
- const selectedProvider = provider || DOCS_RAG_CONFIG.defaultProvider
- const selectedModel =
- model || DOCS_RAG_CONFIG.defaultModel || getProviderDefaultModel(selectedProvider)
+ const config = getCopilotConfig()
+
+ // Determine which provider and model to use - allow overrides
+ const selectedProvider = provider || config.rag.defaultProvider
+ const selectedModel = model || config.rag.defaultModel
- // Get API key for the selected provider
+ // Get API key using the provider utils
let apiKey: string
try {
- if (selectedProvider === 'openai' || selectedProvider === 'azure-openai') {
- apiKey = getRotatingApiKey('openai')
- } else if (selectedProvider === 'anthropic') {
- apiKey = getRotatingApiKey('anthropic')
+ // Use rotating key directly for hosted providers
+ if ((selectedProvider === 'openai' || selectedProvider === 'anthropic')) {
+ const { getRotatingApiKey } = require('@/lib/utils')
+ apiKey = getRotatingApiKey(selectedProvider)
} else {
- // For other providers, try to get from environment
- const envKey = `${selectedProvider.toUpperCase().replace('-', '_')}_API_KEY`
- apiKey = process.env[envKey] || ''
- if (!apiKey) {
- throw new Error(`API key not configured for provider: ${selectedProvider}`)
- }
+ apiKey = getApiKey(selectedProvider, selectedModel)
}
} catch (error) {
- logger.error(`Failed to get API key for provider ${selectedProvider}:`, error)
- throw new Error(`API key not configured for provider: ${selectedProvider}`)
+ logger.error(`Failed to get API key for ${selectedProvider} ${selectedModel}:`, error)
+ throw new Error(`API key not configured for ${selectedProvider}. Please set up API keys for this provider or use a different one.`)
}
// Format chunks as context with numbered sources
@@ -172,8 +168,8 @@ Content: ${chunkText}`
let conversationContext = ''
if (conversationHistory.length > 0) {
conversationContext = '\n\nConversation History:\n'
- conversationHistory.slice(-6).forEach((msg: any) => {
- // Include last 6 messages for context
+ conversationHistory.slice(-config.general.maxConversationHistory).forEach((msg: any) => {
+ // Use config for conversation history limit
const role = msg.role === 'user' ? 'Human' : 'Assistant'
conversationContext += `${role}: ${msg.content}\n`
})
@@ -216,15 +212,10 @@ ${context}`
model: selectedModel,
systemPrompt,
context: userPrompt,
- temperature: DOCS_RAG_CONFIG.temperature,
- maxTokens: DOCS_RAG_CONFIG.maxTokens,
+ temperature: config.rag.temperature,
+ maxTokens: config.rag.maxTokens,
apiKey,
stream,
- // Azure OpenAI specific parameters if needed
- ...(selectedProvider === 'azure-openai' && {
- azureEndpoint: env.AZURE_OPENAI_ENDPOINT,
- azureApiVersion: env.AZURE_OPENAI_API_VERSION,
- }),
}
const response = await executeProviderRequest(selectedProvider, providerRequest)
@@ -275,15 +266,15 @@ export async function POST(req: NextRequest) {
const { query, topK, provider, model, stream, chatId, workflowId, createNewChat } =
DocsQuerySchema.parse(body)
+ const config = getCopilotConfig()
+ const ragConfig = getCopilotModel('rag')
+
// Get session for chat functionality
const session = await getSession()
logger.info(`[${requestId}] Docs RAG query: "${query}"`, {
- provider: provider || DOCS_RAG_CONFIG.defaultProvider,
- model:
- model ||
- DOCS_RAG_CONFIG.defaultModel ||
- getProviderDefaultModel(provider || DOCS_RAG_CONFIG.defaultProvider),
+ provider: provider || ragConfig.provider,
+ model: model || ragConfig.model,
topK,
chatId,
workflowId,
@@ -314,7 +305,7 @@ export async function POST(req: NextRequest) {
userId: session.user.id,
workflowId,
title: null, // Will be generated after first response
- model: model || DOCS_RAG_CONFIG.defaultModel,
+ model: model || ragConfig.model,
messages: [],
})
.returning()
@@ -347,11 +338,8 @@ export async function POST(req: NextRequest) {
requestId,
chunksFound: 0,
query,
- provider: provider || DOCS_RAG_CONFIG.defaultProvider,
- model:
- model ||
- DOCS_RAG_CONFIG.defaultModel ||
- getProviderDefaultModel(provider || DOCS_RAG_CONFIG.defaultProvider),
+ provider: provider || ragConfig.provider,
+ model: model || ragConfig.model,
},
})
}
@@ -398,11 +386,8 @@ export async function POST(req: NextRequest) {
chunksFound: chunks.length,
query,
topSimilarity: sources[0]?.similarity,
- provider: provider || DOCS_RAG_CONFIG.defaultProvider,
- model:
- model ||
- DOCS_RAG_CONFIG.defaultModel ||
- getProviderDefaultModel(provider || DOCS_RAG_CONFIG.defaultProvider),
+ provider: provider || ragConfig.provider,
+ model: model || ragConfig.model,
},
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(metadata)}\n\n`))
@@ -549,11 +534,8 @@ export async function POST(req: NextRequest) {
chunksFound: chunks.length,
query,
topSimilarity: sources[0]?.similarity,
- provider: provider || DOCS_RAG_CONFIG.defaultProvider,
- model:
- model ||
- DOCS_RAG_CONFIG.defaultModel ||
- getProviderDefaultModel(provider || DOCS_RAG_CONFIG.defaultProvider),
+ provider: provider || ragConfig.provider,
+ model: model || ragConfig.model,
},
})
} catch (error) {
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/copilot/copilot.tsx
deleted file mode 100644
index 66c473c229..0000000000
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/copilot/copilot.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-'use client'
-
-import { useState } from 'react'
-import { MessageCircle, Send, X } from 'lucide-react'
-import { Button } from '@/components/ui/button'
-import { Input } from '@/components/ui/input'
-import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
-import { useCopilotStore } from '@/stores/copilot/store'
-
-export function Copilot() {
- const { sendMessage } = useCopilotStore()
- const [isOpen, setIsOpen] = useState(false)
- const [message, setMessage] = useState('')
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- if (!message.trim()) return
-
- await sendMessage(message)
- setMessage('')
- }
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === 'Enter') {
- e.preventDefault()
- handleSubmit(e as unknown as React.FormEvent)
- }
- }
-
- if (!isOpen) {
- return (
-
-
-
-
- Open Chat
-
- )
- }
-
- return (
-
-
-
- )
-}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx
index 1676d25559..ed238d72f7 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx
@@ -1,6 +1,6 @@
'use client'
-import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
+import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react'
import {
Bot,
ChevronDown,
@@ -20,16 +20,10 @@ import {
} from '@/components/ui/dropdown-menu'
import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
-import {
- type CopilotChat,
- type CopilotMessage,
- deleteChat,
- getChat,
- listChats,
- sendStreamingMessage,
-} from '@/lib/copilot-api'
import { createLogger } from '@/lib/logs/console-logger'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
+import { useCopilotStore } from '@/stores/copilot/store'
+import type { CopilotMessage } from '@/stores/copilot/types'
import { CopilotModal } from './components/copilot-modal/copilot-modal'
const logger = createLogger('Copilot')
@@ -58,23 +52,36 @@ export const Copilot = forwardRef(
},
ref
) => {
- const [messages, setMessages] = useState([])
- const [input, setInput] = useState('')
- const [isLoading, setIsLoading] = useState(false)
- const [currentChat, setCurrentChat] = useState(null)
- const [chats, setChats] = useState([])
- const [loadingChats, setLoadingChats] = useState(false)
- const scrollAreaRef = useRef(null)
const inputRef = useRef(null)
+ const scrollAreaRef = useRef(null)
const { activeWorkflowId } = useWorkflowRegistry()
+
+ // Use the new copilot store
+ const {
+ currentChat,
+ chats,
+ messages,
+ isLoading,
+ isLoadingChats,
+ isSendingMessage,
+ error,
+ workflowId,
+ setWorkflowId,
+ selectChat,
+ createNewChat,
+ deleteChat,
+ sendDocsMessage,
+ clearMessages,
+ clearError,
+ } = useCopilotStore()
- // Load chats when workflow changes
+ // Sync workflow ID with store
useEffect(() => {
- if (activeWorkflowId) {
- loadChats()
+ if (activeWorkflowId !== workflowId) {
+ setWorkflowId(activeWorkflowId)
}
- }, [activeWorkflowId])
+ }, [activeWorkflowId, workflowId, setWorkflowId])
// Auto-scroll to bottom when new messages are added
useEffect(() => {
@@ -88,232 +95,56 @@ export const Copilot = forwardRef(
}
}, [messages])
- // Load chats for current workflow
- const loadChats = useCallback(async () => {
- if (!activeWorkflowId) return
-
- setLoadingChats(true)
- try {
- const result = await listChats(activeWorkflowId)
- if (result.success) {
- setChats(result.chats)
- // If no current chat and we have chats, select the most recent one
- if (!currentChat && result.chats.length > 0) {
- await selectChat(result.chats[0])
- }
- } else {
- logger.error('Failed to load chats:', result.error)
- }
- } catch (error) {
- logger.error('Error loading chats:', error)
- } finally {
- setLoadingChats(false)
- }
- }, [activeWorkflowId, currentChat])
-
- // Select a specific chat and load its messages
- const selectChat = useCallback(async (chat: CopilotChat) => {
- try {
- const result = await getChat(chat.id)
- if (result.success && result.chat) {
- setCurrentChat(result.chat)
- setMessages(result.chat.messages || [])
- logger.info(`Loaded chat: ${chat.title || 'Untitled'}`)
- } else {
- logger.error('Failed to load chat:', result.error)
- }
- } catch (error) {
- logger.error('Error loading chat:', error)
- }
- }, [])
-
- // Start a new chat
- const startNewChat = useCallback(() => {
- setCurrentChat(null)
- setMessages([])
- logger.info('Started new chat')
- }, [])
-
- // Delete a chat
+ // Handle chat deletion
const handleDeleteChat = useCallback(
async (chatId: string) => {
try {
- const result = await deleteChat(chatId)
- if (result.success) {
- setChats((prev) => prev.filter((chat) => chat.id !== chatId))
- if (currentChat?.id === chatId) {
- startNewChat()
- }
- logger.info('Chat deleted successfully')
- } else {
- logger.error('Failed to delete chat:', result.error)
- }
+ await deleteChat(chatId)
+ logger.info('Chat deleted successfully')
} catch (error) {
logger.error('Error deleting chat:', error)
}
},
- [currentChat, startNewChat]
+ [deleteChat]
)
+ // Handle new chat creation
+ const handleStartNewChat = useCallback(() => {
+ clearMessages()
+ logger.info('Started new chat')
+ }, [clearMessages])
+
// Expose functions to parent
useImperativeHandle(
ref,
() => ({
- clearMessages: startNewChat,
- startNewChat,
+ clearMessages: handleStartNewChat,
+ startNewChat: handleStartNewChat,
}),
- [startNewChat]
+ [handleStartNewChat]
)
// Handle message submission
const handleSubmit = useCallback(
- async (e: React.FormEvent) => {
+ async (e: React.FormEvent, message?: string) => {
e.preventDefault()
- if (!input.trim() || isLoading || !activeWorkflowId) return
+
+ const query = message || (inputRef.current?.value?.trim() || '')
+ if (!query || isSendingMessage || !activeWorkflowId) return
- const query = input.trim()
- setInput('')
- setIsLoading(true)
-
- // Add user message immediately
- const userMessage: CopilotMessage = {
- id: crypto.randomUUID(),
- role: 'user',
- content: query,
- timestamp: new Date().toISOString(),
+ // Clear input if using the form input
+ if (!message && inputRef.current) {
+ inputRef.current.value = ''
}
- // Add streaming placeholder
- const streamingMessage: CopilotMessage = {
- id: crypto.randomUUID(),
- role: 'assistant',
- content: '',
- timestamp: new Date().toISOString(),
- }
-
- setMessages((prev) => [...prev, userMessage, streamingMessage])
-
try {
- logger.info('Sending docs RAG query:', { query, chatId: currentChat?.id })
-
- const result = await sendStreamingMessage({
- message: query,
- chatId: currentChat?.id,
- workflowId: activeWorkflowId,
- createNewChat: !currentChat,
- })
-
- if (result.success && result.stream) {
- const reader = result.stream.getReader()
- const decoder = new TextDecoder()
- let accumulatedContent = ''
- let newChatId: string | undefined
- let responseCitations: Array<{ id: number; title: string; url: string }> = []
- let streamComplete = false
-
- while (true) {
- const { done, value } = await reader.read()
- if (done || streamComplete) break
-
- const chunk = decoder.decode(value, { stream: true })
- const lines = chunk.split('\n')
-
- for (const line of lines) {
- if (line.startsWith('data: ')) {
- try {
- const data = JSON.parse(line.slice(6))
-
- if (data.type === 'metadata') {
- // Get chatId from metadata (for both new and existing chats)
- if (data.chatId) {
- newChatId = data.chatId
- }
- // Get citations from metadata
- if (data.citations) {
- responseCitations = data.citations
- }
- } else if (data.type === 'content') {
- accumulatedContent += data.content
-
- // Update the streaming message with accumulated content and citations
- setMessages((prev) =>
- prev.map((msg) =>
- msg.id === streamingMessage.id
- ? {
- ...msg,
- content: accumulatedContent,
- citations:
- responseCitations.length > 0 ? responseCitations : undefined,
- }
- : msg
- )
- )
- } else if (data.type === 'done') {
- // Final update to ensure citations are applied
- setMessages((prev) =>
- prev.map((msg) =>
- msg.id === streamingMessage.id
- ? {
- ...msg,
- content: accumulatedContent,
- citations:
- responseCitations.length > 0 ? responseCitations : undefined,
- }
- : msg
- )
- )
-
- // Update current chat state with the chatId from response
- if (newChatId && !currentChat) {
- // For new chats, create a temporary chat object and reload the full chat list
- setCurrentChat({
- id: newChatId,
- title: null,
- model: 'claude-3-7-sonnet-latest',
- createdAt: new Date().toISOString(),
- updatedAt: new Date().toISOString(),
- messageCount: 2, // User + assistant message
- })
- // Reload chats in background to get the updated list
- loadChats()
- }
-
- // Mark stream as complete to exit outer loop
- streamComplete = true
- break
- } else if (data.type === 'error') {
- throw new Error(data.error || 'Streaming error')
- }
- } catch (parseError) {
- logger.warn('Failed to parse SSE data:', parseError)
- }
- }
- }
- }
-
- logger.info('Received copilot chat response:', {
- contentLength: accumulatedContent.length,
- })
- } else {
- throw new Error(result.error || 'Failed to send message')
- }
+ await sendDocsMessage(query, { stream: true })
+ logger.info('Sent docs query:', query)
} catch (error) {
- logger.error('Docs RAG error:', error)
-
- const errorMessage: CopilotMessage = {
- id: streamingMessage.id,
- role: 'assistant',
- content:
- 'Sorry, I encountered an error while searching the documentation. Please try again.',
- timestamp: new Date().toISOString(),
- }
-
- setMessages((prev) => prev.slice(0, -1).concat(errorMessage))
- } finally {
- setIsLoading(false)
+ logger.error('Failed to send docs message:', error)
}
},
- [input, isLoading, activeWorkflowId, currentChat, loadChats]
+ [isSendingMessage, activeWorkflowId, sendDocsMessage]
)
// Format timestamp for display
@@ -343,7 +174,6 @@ export const Copilot = forwardRef(
})
// Also replace standalone ↗ symbols with clickable citation links
- // This handles cases where the LLM outputs ↗ directly
if (citations && citations.length > 0) {
let citationIndex = 0
processedContent = processedContent.replace(/↗/g, () => {
@@ -356,34 +186,27 @@ export const Copilot = forwardRef(
})
}
- // Basic markdown processing for better formatting
+ // Basic markdown processing
processedContent = processedContent
- // Handle code blocks
.replace(
/```(\w+)?\n([\s\S]*?)```/g,
'$2
'
)
- // Handle inline code
.replace(
/`([^`]+)`/g,
'$1'
)
- // Handle bold text
.replace(/\*\*(.*?)\*\*/g, '$1')
- // Handle italic text
.replace(/\*(.*?)\*/g, '$1')
- // Handle headers
.replace(/^### (.*$)/gm, '$1
')
.replace(/^## (.*$)/gm, '$1
')
.replace(/^# (.*$)/gm, '$1
')
- // Handle unordered lists
.replace(/^\* (.*$)/gm, '• $1')
.replace(/^- (.*$)/gm, '• $1')
- // Handle line breaks (reduce spacing)
.replace(/\n\n+/g, '
')
.replace(/\n/g, '
')
- // Wrap in paragraph tags if not already wrapped
+ // Wrap in paragraph tags if needed
if (
!processedContent.includes('
') &&
!processedContent.includes('
') &&
@@ -455,10 +278,8 @@ export const Copilot = forwardRef(
// Handle modal message sending
const handleModalSendMessage = useCallback(
async (message: string) => {
- // Create form event and call the main handler
const mockEvent = { preventDefault: () => {} } as React.FormEvent
- setInput(message)
- await handleSubmit(mockEvent)
+ await handleSubmit(mockEvent, message)
},
[handleSubmit]
)
@@ -519,64 +340,81 @@ export const Copilot = forwardRef(
+
+ {/* Error display */}
+ {error && (
+
+ {error}
+
+
+ )}
- {/* Messages */}
-
+ {/* Messages area */}
+
{messages.length === 0 ? (
-
-
-
Welcome to Documentation Copilot
-
- Ask me anything about Sim Studio features, workflows, tools, or how to get
- started.
-
-
-
Try asking:
-
-
- "How do I create a workflow?"
-
-
- "What tools are available?"
-
-
- "How do I deploy my workflow?"
+
+
+
+
+
Welcome to Documentation Copilot
+
+ Ask me anything about Sim Studio features, workflows, tools, or how to get
+ started.
+
+
+
+
Try asking:
+
+
+ "How do I create a workflow?"
+
+
+ "What tools are available?"
+
+
+ "How do I deploy my workflow?"
+
) : (
-
{messages.map(renderMessage)}
+ messages.map(renderMessage)
)}
- {/* Input */}
+ {/* Input area */}