|
| 1 | +import { createLogger } from '@sim/logger' |
| 2 | +import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' |
| 3 | +import { env } from '@/lib/core/config/env' |
| 4 | +import { executeTool } from '@/tools' |
| 5 | + |
| 6 | +interface SearchLibraryDocsParams { |
| 7 | + library_name: string |
| 8 | + query: string |
| 9 | + version?: string |
| 10 | +} |
| 11 | + |
| 12 | +interface SearchLibraryDocsResult { |
| 13 | + results: Array<{ |
| 14 | + title: string |
| 15 | + link: string |
| 16 | + snippet: string |
| 17 | + position?: number |
| 18 | + }> |
| 19 | + query: string |
| 20 | + library: string |
| 21 | + version?: string |
| 22 | + totalResults: number |
| 23 | +} |
| 24 | + |
| 25 | +export const searchLibraryDocsServerTool: BaseServerTool< |
| 26 | + SearchLibraryDocsParams, |
| 27 | + SearchLibraryDocsResult |
| 28 | +> = { |
| 29 | + name: 'search_library_docs', |
| 30 | + async execute(params: SearchLibraryDocsParams): Promise<SearchLibraryDocsResult> { |
| 31 | + const logger = createLogger('SearchLibraryDocsServerTool') |
| 32 | + const { library_name, query, version } = params |
| 33 | + |
| 34 | + if (!library_name || typeof library_name !== 'string') { |
| 35 | + throw new Error('library_name is required') |
| 36 | + } |
| 37 | + if (!query || typeof query !== 'string') { |
| 38 | + throw new Error('query is required') |
| 39 | + } |
| 40 | + |
| 41 | + // Build a search query that targets the library's documentation |
| 42 | + const searchQuery = version |
| 43 | + ? `${library_name} ${version} documentation ${query}` |
| 44 | + : `${library_name} documentation ${query}` |
| 45 | + |
| 46 | + logger.info('Searching library documentation', { |
| 47 | + library: library_name, |
| 48 | + query, |
| 49 | + version, |
| 50 | + fullSearchQuery: searchQuery, |
| 51 | + }) |
| 52 | + |
| 53 | + // Check which API keys are available |
| 54 | + const hasExaApiKey = Boolean(env.EXA_API_KEY && String(env.EXA_API_KEY).length > 0) |
| 55 | + const hasSerperApiKey = Boolean(env.SERPER_API_KEY && String(env.SERPER_API_KEY).length > 0) |
| 56 | + |
| 57 | + // Try Exa first if available (better for documentation searches) |
| 58 | + if (hasExaApiKey) { |
| 59 | + try { |
| 60 | + logger.debug('Attempting exa_search for library docs', { library: library_name }) |
| 61 | + const exaResult = await executeTool('exa_search', { |
| 62 | + query: searchQuery, |
| 63 | + numResults: 10, |
| 64 | + type: 'auto', |
| 65 | + apiKey: env.EXA_API_KEY || '', |
| 66 | + }) |
| 67 | + |
| 68 | + const exaResults = (exaResult as any)?.output?.results || [] |
| 69 | + const count = Array.isArray(exaResults) ? exaResults.length : 0 |
| 70 | + |
| 71 | + logger.info('exa_search for library docs completed', { |
| 72 | + success: exaResult.success, |
| 73 | + resultsCount: count, |
| 74 | + library: library_name, |
| 75 | + }) |
| 76 | + |
| 77 | + if (exaResult.success && count > 0) { |
| 78 | + const transformedResults = exaResults.map((result: any, idx: number) => ({ |
| 79 | + title: result.title || '', |
| 80 | + link: result.url || '', |
| 81 | + snippet: result.text || result.summary || '', |
| 82 | + position: idx + 1, |
| 83 | + })) |
| 84 | + |
| 85 | + return { |
| 86 | + results: transformedResults, |
| 87 | + query, |
| 88 | + library: library_name, |
| 89 | + version, |
| 90 | + totalResults: count, |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + logger.warn('exa_search returned no results for library docs, falling back to Serper', { |
| 95 | + library: library_name, |
| 96 | + }) |
| 97 | + } catch (exaError: any) { |
| 98 | + logger.warn('exa_search failed for library docs, falling back to Serper', { |
| 99 | + error: exaError?.message, |
| 100 | + library: library_name, |
| 101 | + }) |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + // Fall back to Serper if Exa failed or wasn't available |
| 106 | + if (!hasSerperApiKey) { |
| 107 | + throw new Error('No search API keys available (EXA_API_KEY or SERPER_API_KEY required)') |
| 108 | + } |
| 109 | + |
| 110 | + try { |
| 111 | + logger.debug('Calling serper_search for library docs', { library: library_name }) |
| 112 | + const result = await executeTool('serper_search', { |
| 113 | + query: searchQuery, |
| 114 | + num: 10, |
| 115 | + type: 'search', |
| 116 | + apiKey: env.SERPER_API_KEY || '', |
| 117 | + }) |
| 118 | + |
| 119 | + const results = (result as any)?.output?.searchResults || [] |
| 120 | + const count = Array.isArray(results) ? results.length : 0 |
| 121 | + |
| 122 | + logger.info('serper_search for library docs completed', { |
| 123 | + success: result.success, |
| 124 | + resultsCount: count, |
| 125 | + library: library_name, |
| 126 | + }) |
| 127 | + |
| 128 | + if (!result.success) { |
| 129 | + logger.error('serper_search failed for library docs', { error: (result as any)?.error }) |
| 130 | + throw new Error((result as any)?.error || 'Library documentation search failed') |
| 131 | + } |
| 132 | + |
| 133 | + // Transform serper results to match expected format |
| 134 | + const transformedResults = results.map((result: any, idx: number) => ({ |
| 135 | + title: result.title || '', |
| 136 | + link: result.link || '', |
| 137 | + snippet: result.snippet || '', |
| 138 | + position: idx + 1, |
| 139 | + })) |
| 140 | + |
| 141 | + return { |
| 142 | + results: transformedResults, |
| 143 | + query, |
| 144 | + library: library_name, |
| 145 | + version, |
| 146 | + totalResults: count, |
| 147 | + } |
| 148 | + } catch (e: any) { |
| 149 | + logger.error('search_library_docs execution error', { |
| 150 | + message: e?.message, |
| 151 | + library: library_name, |
| 152 | + }) |
| 153 | + throw e |
| 154 | + } |
| 155 | + }, |
| 156 | +} |
0 commit comments