diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index 109ca6eb17..aeac8af9da 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -1,14 +1,13 @@ -import { eq, and } from 'drizzle-orm' +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 { getSession } from '@/lib/auth' import { db } from '@/db' import { copilotChats } from '@/db/schema' import { executeProviderRequest } from '@/providers' import type { Message } from '@/providers/types' -import { executeTool } from '@/tools' const logger = createLogger('CopilotChat') @@ -34,10 +33,11 @@ const CopilotChatSchema = z.object({ 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.', + 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.`, @@ -58,8 +58,6 @@ Return only the title text, nothing else.`, } } - - /** * Generate chat response with tool calling support */ @@ -84,8 +82,8 @@ function extractCitationsFromResponse(response: any): Array<{ return [] } - const docsSearchResult = response.toolResults.find((result: any) => - result.sources && Array.isArray(result.sources) + const docsSearchResult = response.toolResults.find( + (result: any) => result.sources && Array.isArray(result.sources) ) if (!docsSearchResult || !docsSearchResult.sources) { @@ -109,15 +107,16 @@ async function generateChatResponse( // Build conversation context const messages: Message[] = [] - + // Add conversation history - for (const msg of conversationHistory.slice(-10)) { // Keep last 10 messages + 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', @@ -170,7 +169,8 @@ MAKE SURE YOU FULLY ANSWER THE USER'S QUESTION. { id: 'docs_search_internal', name: 'Search Documentation', - description: 'Search Sim Studio documentation for information about features, tools, workflows, and functionality', + description: + 'Search Sim Studio documentation for information about features, tools, workflows, and functionality', params: {}, parameters: { type: 'object', @@ -204,43 +204,42 @@ MAKE SURE YOU FULLY ANSWER THE USER'S QUESTION. stream: false, // Always start with non-streaming to handle tool calls }) - - - // If this is a streaming request and we got a regular response, + // 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 } @@ -252,7 +251,9 @@ MAKE SURE YOU FULLY ANSWER THE USER'S QUESTION. 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'}`) + throw new Error( + `Failed to generate response: ${error instanceof Error ? error.message : 'Unknown error'}` + ) } } @@ -268,7 +269,7 @@ export async function POST(req: NextRequest) { const { message, chatId, workflowId, createNewChat, stream } = CopilotChatSchema.parse(body) const session = await getSession() - + logger.info(`[${requestId}] Copilot chat message: "${message}"`, { chatId, workflowId, @@ -285,12 +286,7 @@ export async function POST(req: NextRequest) { const [existingChat] = await db .select() .from(copilotChats) - .where( - and( - eq(copilotChats.id, chatId), - eq(copilotChats.userId, session.user.id) - ) - ) + .where(and(eq(copilotChats.id, chatId), eq(copilotChats.userId, session.user.id))) .limit(1) if (existingChat) { @@ -326,11 +322,11 @@ export async function POST(req: NextRequest) { 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() + + return new Response( + new ReadableStream({ + async start(controller) { + const reader = response.getReader() let accumulatedResponse = '' // Send initial metadata @@ -352,7 +348,7 @@ export async function POST(req: NextRequest) { const chunkText = new TextDecoder().decode(value) accumulatedResponse += chunkText - + const contentChunk = { type: 'content', content: chunkText, @@ -430,14 +426,22 @@ export async function POST(req: NextRequest) { } // Extract citations from response if available - const citations = typeof response === 'object' && 'citations' in response ? response.citations : - typeof response === 'object' && 'toolResults' in response ? extractCitationsFromResponse(response) : [] + const citations = + typeof response === 'object' && 'citations' in response + ? response.citations + : typeof response === 'object' && 'toolResults' in response + ? extractCitationsFromResponse(response) + : [] const assistantMessage = { id: crypto.randomUUID(), role: 'assistant', - content: typeof response === 'string' ? response : - 'content' in response ? response.content : '[Error generating response]', + content: + typeof response === 'string' + ? response + : 'content' in response + ? response.content + : '[Error generating response]', timestamp: new Date().toISOString(), citations: citations.length > 0 ? citations : undefined, } @@ -466,8 +470,12 @@ export async function POST(req: NextRequest) { return NextResponse.json({ success: true, - response: typeof response === 'string' ? response : - 'content' in response ? response.content : '[Error generating response]', + response: + typeof response === 'string' + ? response + : 'content' in response + ? response.content + : '[Error generating response]', chatId: currentChat?.id, metadata: { requestId, @@ -485,4 +493,4 @@ export async function POST(req: NextRequest) { logger.error(`[${requestId}] Copilot chat error:`, error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } -} \ No newline at end of file +} diff --git a/apps/sim/app/api/docs/search/route.ts b/apps/sim/app/api/docs/search/route.ts index 61593ff92b..0804a9d040 100644 --- a/apps/sim/app/api/docs/search/route.ts +++ b/apps/sim/app/api/docs/search/route.ts @@ -93,10 +93,18 @@ export async function POST(req: NextRequest) { // Step 3: Format the response with context and sources const context = chunks .map((chunk, index) => { - const headerText = typeof chunk.headerText === 'string' ? chunk.headerText : String(chunk.headerText || 'Untitled Section') - const sourceDocument = typeof chunk.sourceDocument === 'string' ? chunk.sourceDocument : String(chunk.sourceDocument || 'Unknown Document') - const sourceLink = typeof chunk.sourceLink === 'string' ? chunk.sourceLink : String(chunk.sourceLink || '#') - const chunkText = typeof chunk.chunkText === 'string' ? chunk.chunkText : String(chunk.chunkText || '') + const headerText = + typeof chunk.headerText === 'string' + ? chunk.headerText + : String(chunk.headerText || 'Untitled Section') + const sourceDocument = + typeof chunk.sourceDocument === 'string' + ? chunk.sourceDocument + : String(chunk.sourceDocument || 'Unknown Document') + const sourceLink = + typeof chunk.sourceLink === 'string' ? chunk.sourceLink : String(chunk.sourceLink || '#') + const chunkText = + typeof chunk.chunkText === 'string' ? chunk.chunkText : String(chunk.chunkText || '') return `[${index + 1}] ${headerText} Document: ${sourceDocument} @@ -138,4 +146,4 @@ Content: ${chunkText}` logger.error(`[${requestId}] Docs search error:`, error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } -} \ No newline at end of file +} 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 30bc33127e..28ad81d4aa 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 @@ -208,7 +208,7 @@ export const Copilot = forwardRef( const decoder = new TextDecoder() let accumulatedContent = '' let newChatId: string | undefined - let responseCitations: Array<{id: number, title: string, url: string}> = [] + let responseCitations: Array<{ id: number; title: string; url: string }> = [] while (true) { const { done, value } = await reader.read() @@ -241,7 +241,8 @@ export const Copilot = forwardRef( ? { ...msg, content: accumulatedContent, - citations: responseCitations.length > 0 ? responseCitations : undefined, + citations: + responseCitations.length > 0 ? responseCitations : undefined, } : msg ) @@ -254,12 +255,13 @@ export const Copilot = forwardRef( ? { ...msg, content: accumulatedContent, - citations: responseCitations.length > 0 ? responseCitations : undefined, + 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 diff --git a/apps/sim/tools/docs/search.ts b/apps/sim/tools/docs/search.ts index 33f4c618e7..166f7592b6 100644 --- a/apps/sim/tools/docs/search.ts +++ b/apps/sim/tools/docs/search.ts @@ -68,6 +68,8 @@ export const docsSearchTool: ToolConfig = }, transformError: (error) => { - return error instanceof Error ? error.message : 'An error occurred while searching documentation' + return error instanceof Error + ? error.message + : 'An error occurred while searching documentation' }, -} \ No newline at end of file +} diff --git a/apps/sim/tools/utils.ts b/apps/sim/tools/utils.ts index 989aab60fd..34406801d3 100644 --- a/apps/sim/tools/utils.ts +++ b/apps/sim/tools/utils.ts @@ -2,8 +2,8 @@ import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console-logger' import { useCustomToolsStore } from '@/stores/custom-tools/store' import { useEnvironmentStore } from '@/stores/settings/environment/store' -import { tools } from './registry' import { docsSearchTool } from './docs/search' +import { tools } from './registry' import type { TableRow, ToolConfig, ToolResponse } from './types' const logger = createLogger('ToolsUtils')