From ccf5c2f6d8045337f33b46db373e631eb141d5c9 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 8 Jul 2025 19:46:06 -0700 Subject: [PATCH] Works? --- apps/sim/app/api/copilot/route.ts | 24 +++- apps/sim/app/api/docs/search/route.ts | 133 +++--------------- .../panel/components/copilot/copilot.tsx | 10 +- apps/sim/lib/copilot-api.ts | 32 +++++ apps/sim/lib/copilot/config.ts | 2 +- apps/sim/lib/copilot/service.ts | 39 +++-- apps/sim/lib/copilot/tools.ts | 116 +++++++++++++++ apps/sim/stores/copilot/store.ts | 38 +++++ apps/sim/tools/docs/search.ts | 42 +++--- 9 files changed, 284 insertions(+), 152 deletions(-) create mode 100644 apps/sim/lib/copilot/tools.ts diff --git a/apps/sim/app/api/copilot/route.ts b/apps/sim/app/api/copilot/route.ts index ced93aa9fc..105301817d 100644 --- a/apps/sim/app/api/copilot/route.ts +++ b/apps/sim/app/api/copilot/route.ts @@ -75,8 +75,28 @@ export async function POST(req: NextRequest) { userId: session.user.id, }) - // Handle streaming response + // Handle streaming response (ReadableStream or StreamingExecution) + let streamToRead: ReadableStream | null = null + + // Debug logging to see what we actually got + logger.info(`[${requestId}] Response type analysis:`, { + responseType: typeof result.response, + isReadableStream: result.response instanceof ReadableStream, + hasStreamProperty: typeof result.response === 'object' && result.response && 'stream' in result.response, + hasExecutionProperty: typeof result.response === 'object' && result.response && 'execution' in result.response, + responseKeys: typeof result.response === 'object' && result.response ? Object.keys(result.response) : [], + }) + if (result.response instanceof ReadableStream) { + logger.info(`[${requestId}] Direct ReadableStream detected`) + streamToRead = result.response + } else if (typeof result.response === 'object' && result.response && 'stream' in result.response && 'execution' in result.response) { + // Handle StreamingExecution (from providers with tool calls) + logger.info(`[${requestId}] StreamingExecution detected`) + streamToRead = (result.response as any).stream + } + + if (streamToRead) { logger.info(`[${requestId}] Returning streaming response`) const encoder = new TextEncoder() @@ -84,7 +104,7 @@ export async function POST(req: NextRequest) { return new Response( new ReadableStream({ async start(controller) { - const reader = (result.response as ReadableStream).getReader() + const reader = streamToRead!.getReader() let accumulatedResponse = '' // Send initial metadata diff --git a/apps/sim/app/api/docs/search/route.ts b/apps/sim/app/api/docs/search/route.ts index 0804a9d040..686f78c09f 100644 --- a/apps/sim/app/api/docs/search/route.ts +++ b/apps/sim/app/api/docs/search/route.ts @@ -1,138 +1,41 @@ -import { sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' +import { searchDocumentation } from '@/lib/copilot/service' import { createLogger } from '@/lib/logs/console-logger' -import { generateEmbeddings } from '@/app/api/knowledge/utils' -import { db } from '@/db' -import { docsEmbeddings } from '@/db/schema' -const logger = createLogger('DocsSearch') +const logger = createLogger('DocsSearchAPI') -const DocsSearchSchema = z.object({ +const SearchSchema = z.object({ query: z.string().min(1, 'Query is required'), - topK: z.number().min(1).max(10).default(5), + topK: z.number().min(1).max(20).default(5), }) -/** - * Generate embedding for search query - */ -async function generateSearchEmbedding(query: string): Promise { - try { - const embeddings = await generateEmbeddings([query]) - return embeddings[0] || [] - } catch (error) { - logger.error('Failed to generate search embedding:', error) - throw new Error('Failed to generate search embedding') - } -} - -/** - * Search docs embeddings using vector similarity - */ -async function searchDocs(queryEmbedding: number[], topK: number) { - try { - const results = await db - .select({ - chunkId: docsEmbeddings.chunkId, - chunkText: docsEmbeddings.chunkText, - sourceDocument: docsEmbeddings.sourceDocument, - sourceLink: docsEmbeddings.sourceLink, - headerText: docsEmbeddings.headerText, - headerLevel: docsEmbeddings.headerLevel, - similarity: sql`1 - (${docsEmbeddings.embedding} <=> ${JSON.stringify(queryEmbedding)}::vector)`, - }) - .from(docsEmbeddings) - .orderBy(sql`${docsEmbeddings.embedding} <=> ${JSON.stringify(queryEmbedding)}::vector`) - .limit(topK) - - return results - } catch (error) { - logger.error('Failed to search docs:', error) - throw new Error('Failed to search docs') - } -} - /** * POST /api/docs/search - * Search Sim Studio documentation using vector similarity + * Search documentation for copilot tools */ export async function POST(req: NextRequest) { const requestId = crypto.randomUUID() try { const body = await req.json() - const { query, topK } = DocsSearchSchema.parse(body) + const { query, topK } = SearchSchema.parse(body) - logger.info(`[${requestId}] 🔍 DOCS SEARCH TOOL CALLED - Query: "${query}"`, { topK }) + logger.info(`[${requestId}] Documentation search request: "${query}"`, { topK }) - // Step 1: Generate embedding for the query - logger.info(`[${requestId}] Generating query embedding...`) - const queryEmbedding = await generateSearchEmbedding(query) + const results = await searchDocumentation(query, { topK }) - if (queryEmbedding.length === 0) { - return NextResponse.json({ error: 'Failed to generate query embedding' }, { status: 500 }) - } - - // Step 2: Search for relevant docs chunks - logger.info(`[${requestId}] Searching docs for top ${topK} chunks...`) - const chunks = await searchDocs(queryEmbedding, topK) - - if (chunks.length === 0) { - return NextResponse.json({ - success: true, - response: "I couldn't find any relevant documentation for that query.", - sources: [], - metadata: { - requestId, - chunksFound: 0, - query, - }, - }) - } - - // 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 || '') - - return `[${index + 1}] ${headerText} -Document: ${sourceDocument} -URL: ${sourceLink} -Content: ${chunkText}` - }) - .join('\n\n') - - // Step 4: Format sources for response - const sources = chunks.map((chunk, index) => ({ - id: index + 1, - title: chunk.headerText, - document: chunk.sourceDocument, - link: chunk.sourceLink, - similarity: Math.round(chunk.similarity * 100) / 100, - })) - - logger.info(`[${requestId}] Found ${chunks.length} relevant chunks`) + logger.info(`[${requestId}] Found ${results.length} documentation results`, { query }) return NextResponse.json({ success: true, - response: context, - sources, + results, + query, + totalResults: results.length, metadata: { requestId, - chunksFound: chunks.length, query, - topSimilarity: sources[0]?.similarity, + topK, }, }) } catch (error) { @@ -143,7 +46,13 @@ Content: ${chunkText}` ) } - logger.error(`[${requestId}] Docs search error:`, error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + logger.error(`[${requestId}] Documentation search error:`, error) + return NextResponse.json( + { + error: 'Failed to search documentation', + details: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ) } } 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 97153f1ea3..4e9eef1dfd 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 @@ -71,7 +71,7 @@ export const Copilot = forwardRef( selectChat, createNewChat, deleteChat, - sendDocsMessage, + sendMessage, clearMessages, clearError, } = useCopilotStore() @@ -138,13 +138,13 @@ export const Copilot = forwardRef( } try { - await sendDocsMessage(query, { stream: true }) - logger.info('Sent docs query:', query) + await sendMessage(query, { stream: true }) + logger.info('Sent message:', query) } catch (error) { - logger.error('Failed to send docs message:', error) + logger.error('Failed to send message:', error) } }, - [isSendingMessage, activeWorkflowId, sendDocsMessage] + [isSendingMessage, activeWorkflowId, sendMessage] ) // Format timestamp for display diff --git a/apps/sim/lib/copilot-api.ts b/apps/sim/lib/copilot-api.ts index 343249ecc2..cdda09889d 100644 --- a/apps/sim/lib/copilot-api.ts +++ b/apps/sim/lib/copilot-api.ts @@ -251,26 +251,44 @@ export async function sendStreamingMessage(request: SendMessageRequest): Promise error?: string }> { try { + console.log('[CopilotAPI] Sending streaming message request:', { + message: request.message, + stream: true, + hasWorkflowId: !!request.workflowId + }) + const response = await fetch('/api/copilot', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...request, stream: true }), }) + console.log('[CopilotAPI] Fetch response received:', { + ok: response.ok, + status: response.status, + statusText: response.statusText, + hasBody: !!response.body, + contentType: response.headers.get('content-type') + }) + if (!response.ok) { const errorData = await response.json() + console.error('[CopilotAPI] Error response:', errorData) throw new Error(errorData.error || 'Failed to send streaming message') } if (!response.body) { + console.error('[CopilotAPI] No response body received') throw new Error('No response body received') } + console.log('[CopilotAPI] Successfully received stream') return { success: true, stream: response.body, } } catch (error) { + console.error('[CopilotAPI] Failed to send streaming message:', error) logger.error('Failed to send streaming message:', error) return { success: false, @@ -332,26 +350,40 @@ export async function sendStreamingDocsMessage(request: DocsQueryRequest): Promi error?: string }> { try { + console.log('[CopilotAPI] sendStreamingDocsMessage called with:', request) + const response = await fetch('/api/copilot/docs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...request, stream: true }), }) + console.log('[CopilotAPI] Fetch response received:', { + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + ok: response.ok, + hasBody: !!response.body + }) + if (!response.ok) { const errorData = await response.json() + console.error('[CopilotAPI] API error response:', errorData) throw new Error(errorData.error || 'Failed to send streaming docs message') } if (!response.body) { + console.error('[CopilotAPI] No response body received') throw new Error('No response body received') } + console.log('[CopilotAPI] Returning successful result with stream') return { success: true, stream: response.body, } } catch (error) { + console.error('[CopilotAPI] Error in sendStreamingDocsMessage:', error) logger.error('Failed to send streaming docs message:', error) return { success: false, diff --git a/apps/sim/lib/copilot/config.ts b/apps/sim/lib/copilot/config.ts index 7594a627ed..b4fc9a749a 100644 --- a/apps/sim/lib/copilot/config.ts +++ b/apps/sim/lib/copilot/config.ts @@ -81,7 +81,7 @@ IMPORTANT: Always provide complete, helpful responses. If you add citations, con maxTokens: 2000, embeddingModel: 'text-embedding-3-small', maxSources: 5, - similarityThreshold: 0.7, + similarityThreshold: 0.5, }, general: { streamingEnabled: true, diff --git a/apps/sim/lib/copilot/service.ts b/apps/sim/lib/copilot/service.ts index e41be64e40..6897096732 100644 --- a/apps/sim/lib/copilot/service.ts +++ b/apps/sim/lib/copilot/service.ts @@ -75,7 +75,7 @@ export async function generateChatTitle(userMessage: string): Promise { try { // Use rotating key directly for hosted providers if (provider === 'openai' || provider === 'anthropic') { - const { getRotatingApiKey } = require('@/lib/utils') + const { getRotatingApiKey } = await import('@/lib/utils') apiKey = getRotatingApiKey(provider) } else { apiKey = getApiKey(provider, model) @@ -125,9 +125,9 @@ export async function searchDocumentation( similarity: number }> > { - const { generateEmbeddings } = require('@/app/api/knowledge/utils') - const { docsEmbeddings } = require('@/db/schema') - const { sql } = require('drizzle-orm') + const { generateEmbeddings } = await import('@/app/api/knowledge/utils') + const { docsEmbeddings } = await import('@/db/schema') + const { sql } = await import('drizzle-orm') const config = getCopilotConfig() const { topK = config.rag.maxSources, threshold = config.rag.similarityThreshold } = options @@ -221,7 +221,7 @@ export async function generateDocsResponse( try { // Use rotating key directly for hosted providers if (selectedProvider === 'openai' || selectedProvider === 'anthropic') { - const { getRotatingApiKey } = require('@/lib/utils') + const { getRotatingApiKey } = await import('@/lib/utils') apiKey = getRotatingApiKey(selectedProvider) } else { apiKey = getApiKey(selectedProvider, selectedModel) @@ -375,7 +375,7 @@ export async function generateChatResponse( try { // Use rotating key directly for hosted providers if (provider === 'openai' || provider === 'anthropic') { - const { getRotatingApiKey } = require('@/lib/utils') + const { getRotatingApiKey } = await import('@/lib/utils') apiKey = getRotatingApiKey(provider) } else { apiKey = getApiKey(provider, model) @@ -444,12 +444,31 @@ export async function generateChatResponse( stream, }) - // Handle tool calls if needed (documentation search) - if (typeof response === 'object' && 'content' in response) { - return response.content || 'Sorry, I could not generate a response.' + // Handle StreamingExecution (from providers with tool calls) + if (typeof response === 'object' && response && 'stream' in response && 'execution' in response) { + logger.info('Detected StreamingExecution from provider') + return (response as any).stream } - // Handle streaming response + // Handle ProviderResponse (non-streaming with tool calls) + if (typeof response === 'object' && 'content' in response) { + const content = response.content || 'Sorry, I could not generate a response.' + + // If streaming was requested, wrap the content in a ReadableStream + if (stream) { + return new ReadableStream({ + start(controller) { + const encoder = new TextEncoder() + controller.enqueue(encoder.encode(content)) + controller.close() + } + }) + } + + return content + } + + // Handle direct ReadableStream response if (response instanceof ReadableStream) { return response } diff --git a/apps/sim/lib/copilot/tools.ts b/apps/sim/lib/copilot/tools.ts new file mode 100644 index 0000000000..4ab92a0c52 --- /dev/null +++ b/apps/sim/lib/copilot/tools.ts @@ -0,0 +1,116 @@ +import { createLogger } from '@/lib/logs/console-logger' +import { searchDocumentation } from './service' + +const logger = createLogger('CopilotTools') + +// Interface for copilot tool execution results +export interface CopilotToolResult { + success: boolean + data?: any + error?: string +} + +// Interface for copilot tool definitions +export interface CopilotTool { + id: string + name: string + description: string + parameters: { + type: 'object' + properties: Record + required: string[] + } + execute: (args: Record) => Promise +} + +// Documentation search tool for copilot +const docsSearchTool: CopilotTool = { + id: 'docs_search_internal', + name: 'Search Documentation', + description: 'Search Sim Studio documentation for information about features, tools, workflows, and functionality', + 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'], + }, + execute: async (args: Record): Promise => { + try { + const { query, topK = 5 } = args + + logger.info('Executing documentation search', { query, topK }) + + const results = await searchDocumentation(query, { topK }) + + logger.info(`Found ${results.length} documentation results`, { query }) + + return { + success: true, + data: { + results, + query, + totalResults: results.length, + }, + } + } catch (error) { + logger.error('Documentation search failed', error) + return { + success: false, + error: `Documentation search failed: ${error instanceof Error ? error.message : 'Unknown error'}`, + } + } + }, +} + +// Copilot tools registry +const copilotTools: Record = { + docs_search_internal: docsSearchTool, +} + +// Get a copilot tool by ID +export function getCopilotTool(toolId: string): CopilotTool | undefined { + return copilotTools[toolId] +} + +// Execute a copilot tool +export async function executeCopilotTool( + toolId: string, + args: Record +): Promise { + const tool = getCopilotTool(toolId) + + if (!tool) { + logger.error(`Copilot tool not found: ${toolId}`) + return { + success: false, + error: `Tool not found: ${toolId}`, + } + } + + try { + logger.info(`Executing copilot tool: ${toolId}`, { args }) + const result = await tool.execute(args) + logger.info(`Copilot tool execution completed: ${toolId}`, { success: result.success }) + return result + } catch (error) { + logger.error(`Copilot tool execution failed: ${toolId}`, error) + return { + success: false, + error: `Tool execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`, + } + } +} + +// Get all available copilot tools (for tool definitions in LLM requests) +export function getAllCopilotTools(): CopilotTool[] { + return Object.values(copilotTools) +} \ No newline at end of file diff --git a/apps/sim/stores/copilot/store.ts b/apps/sim/stores/copilot/store.ts index 1970e9ff9b..ab1e9ef833 100644 --- a/apps/sim/stores/copilot/store.ts +++ b/apps/sim/stores/copilot/store.ts @@ -196,7 +196,15 @@ export const useCopilotStore = create()( const { workflowId, currentChat } = get() const { stream = true } = options + console.log('[CopilotStore] sendMessage called:', { + message, + workflowId, + hasCurrentChat: !!currentChat, + stream + }) + if (!workflowId) { + console.warn('[CopilotStore] No workflow ID set') logger.warn('Cannot send message: no workflow ID set') return } @@ -219,11 +227,17 @@ export const useCopilotStore = create()( timestamp: new Date().toISOString(), } + console.log('[CopilotStore] Adding messages to state:', { + userMessageId: userMessage.id, + streamingMessageId: streamingMessage.id + }) + set((state) => ({ messages: [...state.messages, userMessage, streamingMessage], })) try { + console.log('[CopilotStore] Requesting streaming response') const result = await sendStreamingMessage({ message, chatId: currentChat?.id, @@ -232,9 +246,18 @@ export const useCopilotStore = create()( stream, }) + console.log('[CopilotStore] Streaming result:', { + success: result.success, + hasStream: !!result.stream, + error: result.error + }) + if (result.success && result.stream) { + console.log('[CopilotStore] Starting stream processing') await get().handleStreamingResponse(result.stream, streamingMessage.id) + console.log('[CopilotStore] Stream processing completed') } else { + console.error('[CopilotStore] Stream request failed:', result.error) throw new Error(result.error || 'Failed to send message') } } catch (error) { @@ -330,6 +353,8 @@ export const useCopilotStore = create()( // Handle streaming response (shared by both message types) handleStreamingResponse: async (stream: ReadableStream, messageId: string) => { + console.log('[CopilotStore] handleStreamingResponse started:', { messageId, hasStream: !!stream }) + const reader = stream.getReader() const decoder = new TextDecoder() let accumulatedContent = '' @@ -340,6 +365,7 @@ export const useCopilotStore = create()( try { while (true) { const { done, value } = await reader.read() + if (done || streamComplete) break const chunk = decoder.decode(value, { stream: true }) @@ -367,7 +393,9 @@ export const useCopilotStore = create()( })) } } else if (data.type === 'content') { + console.log('[CopilotStore] Received content chunk:', data.content) accumulatedContent += data.content + console.log('[CopilotStore] Accumulated content length:', accumulatedContent.length) // Update the streaming message set((state) => ({ @@ -382,7 +410,9 @@ export const useCopilotStore = create()( : msg ), })) + console.log('[CopilotStore] Updated message state with content') } else if (data.type === 'done' || data.type === 'complete') { + console.log('[CopilotStore] Received completion marker:', data.type) // Final update set((state) => ({ messages: state.messages.map((msg) => @@ -400,24 +430,32 @@ export const useCopilotStore = create()( // Handle new chat creation if (newChatId && !get().currentChat) { + console.log('[CopilotStore] Reloading chats for new chat:', newChatId) // Reload chats to get the updated list await get().loadChats() } streamComplete = true + console.log('[CopilotStore] Stream marked as complete') break } else if (data.type === 'error') { + console.error('[CopilotStore] Received error from stream:', data.error) throw new Error(data.error || 'Streaming error') } } catch (parseError) { + console.warn('[CopilotStore] Failed to parse SSE data:', parseError, 'Line:', line) logger.warn('Failed to parse SSE data:', parseError) } + } else if (line.trim()) { + console.log('[CopilotStore] Non-SSE line (ignored):', line) } } } + console.log('[CopilotStore] Stream processing completed successfully') logger.info(`Completed streaming response, content length: ${accumulatedContent.length}`) } catch (error) { + console.error('[CopilotStore] Error handling streaming response:', error) logger.error('Error handling streaming response:', error) throw error } diff --git a/apps/sim/tools/docs/search.ts b/apps/sim/tools/docs/search.ts index 166f7592b6..228a05e5e3 100644 --- a/apps/sim/tools/docs/search.ts +++ b/apps/sim/tools/docs/search.ts @@ -6,23 +6,21 @@ export interface DocsSearchParams { } export interface DocsSearchResponse { - success: boolean - output: { - response: string - sources: Array<{ - title: string - document: string - link: string - similarity: number - }> - } - error?: string + results: Array<{ + id: number + title: string + url: string + content: string + similarity: number + }> + query: string + totalResults: number } export const docsSearchTool: ToolConfig = { id: 'docs_search_internal', name: 'Search Documentation', - description: 'Search Sim Studio documentation using vector similarity search', + description: 'Search Sim Studio documentation for information about features, tools, workflows, and functionality', version: '1.0.0', params: { @@ -35,6 +33,7 @@ export const docsSearchTool: ToolConfig = type: 'number', required: false, description: 'Number of results to return (default: 5, max: 10)', + default: 5, }, }, @@ -51,25 +50,24 @@ export const docsSearchTool: ToolConfig = isInternalRoute: true, }, - transformResponse: async (response: Response) => { + transformResponse: async (response: Response): Promise => { const data = await response.json() if (!response.ok) { - throw new Error(data.error || 'Failed to search documentation') + return { + success: false, + output: {}, + error: data.error || 'Failed to search documentation', + } } return { success: true, output: { - response: data.response, - sources: data.sources || [], + results: data.results || [], + query: data.query || '', + totalResults: data.totalResults || 0, }, } }, - - transformError: (error) => { - return error instanceof Error - ? error.message - : 'An error occurred while searching documentation' - }, }