Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions apps/sim/lib/copilot/tools/client/other/search-library-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { createLogger } from '@sim/logger'
import { BookOpen, Loader2, MinusCircle, XCircle } from 'lucide-react'
import {
BaseClientTool,
type BaseClientToolMetadata,
ClientToolCallState,
} from '@/lib/copilot/tools/client/base-tool'
import { ExecuteResponseSuccessSchema } from '@/lib/copilot/tools/shared/schemas'

interface SearchLibraryDocsArgs {
library_name: string
query: string
version?: string
}

export class SearchLibraryDocsClientTool extends BaseClientTool {
static readonly id = 'search_library_docs'

constructor(toolCallId: string) {
super(toolCallId, SearchLibraryDocsClientTool.id, SearchLibraryDocsClientTool.metadata)
}

static readonly metadata: BaseClientToolMetadata = {
displayNames: {
[ClientToolCallState.generating]: { text: 'Reading docs', icon: Loader2 },
[ClientToolCallState.pending]: { text: 'Reading docs', icon: Loader2 },
[ClientToolCallState.executing]: { text: 'Reading docs', icon: Loader2 },
[ClientToolCallState.success]: { text: 'Read docs', icon: BookOpen },
[ClientToolCallState.error]: { text: 'Failed to read docs', icon: XCircle },
[ClientToolCallState.aborted]: { text: 'Aborted reading docs', icon: XCircle },
[ClientToolCallState.rejected]: { text: 'Skipped reading docs', icon: MinusCircle },
},
getDynamicText: (params, state) => {
const libraryName = params?.library_name
if (libraryName && typeof libraryName === 'string') {
switch (state) {
case ClientToolCallState.success:
return `Read ${libraryName} docs`
case ClientToolCallState.executing:
case ClientToolCallState.generating:
case ClientToolCallState.pending:
return `Reading ${libraryName} docs`
case ClientToolCallState.error:
return `Failed to read ${libraryName} docs`
case ClientToolCallState.aborted:
return `Aborted reading ${libraryName} docs`
case ClientToolCallState.rejected:
return `Skipped reading ${libraryName} docs`
}
}
return undefined
},
}

async execute(args?: SearchLibraryDocsArgs): Promise<void> {
const logger = createLogger('SearchLibraryDocsClientTool')
try {
this.setState(ClientToolCallState.executing)
const res = await fetch('/api/copilot/execute-copilot-server-tool', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ toolName: 'search_library_docs', payload: args || {} }),
})
if (!res.ok) {
const txt = await res.text().catch(() => '')
throw new Error(txt || `Server error (${res.status})`)
}
const json = await res.json()
const parsed = ExecuteResponseSuccessSchema.parse(json)
this.setState(ClientToolCallState.success)
await this.markToolComplete(
200,
`Library documentation search complete for ${args?.library_name || 'unknown'}`,
parsed.result
)
this.setState(ClientToolCallState.success)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The state is set to ClientToolCallState.success twice (lines 70 and 76). While this follows the existing pattern in other similar tools like SearchOnlineClientTool and SearchDocumentationClientTool, this redundancy could be removed for cleaner code.

Suggested change
this.setState(ClientToolCallState.success)
await this.markToolComplete(
200,
`Library documentation search complete for ${args?.library_name || 'unknown'}`,
parsed.result
)
this.setState(ClientToolCallState.success)
const json = await res.json()
const parsed = ExecuteResponseSuccessSchema.parse(json)
await this.markToolComplete(
200,
`Library documentation search complete for ${args?.library_name || 'unknown'}`,
parsed.result
)
this.setState(ClientToolCallState.success)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/sim/lib/copilot/tools/client/other/search-library-docs.ts
Line: 70:76

Comment:
The state is set to `ClientToolCallState.success` twice (lines 70 and 76). While this follows the existing pattern in other similar tools like `SearchOnlineClientTool` and `SearchDocumentationClientTool`, this redundancy could be removed for cleaner code.

```suggestion
      const json = await res.json()
      const parsed = ExecuteResponseSuccessSchema.parse(json)
      await this.markToolComplete(
        200,
        `Library documentation search complete for ${args?.library_name || 'unknown'}`,
        parsed.result
      )
      this.setState(ClientToolCallState.success)
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

} catch (e: any) {
logger.error('execute failed', { message: e?.message })
this.setState(ClientToolCallState.error)
await this.markToolComplete(500, e?.message || 'Library documentation search failed')
}
}
}
156 changes: 156 additions & 0 deletions apps/sim/lib/copilot/tools/server/docs/search-library-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { createLogger } from '@sim/logger'
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
import { env } from '@/lib/core/config/env'
import { executeTool } from '@/tools'

interface SearchLibraryDocsParams {
library_name: string
query: string
version?: string
}

interface SearchLibraryDocsResult {
results: Array<{
title: string
link: string
snippet: string
position?: number
}>
query: string
library: string
version?: string
totalResults: number
}

export const searchLibraryDocsServerTool: BaseServerTool<
SearchLibraryDocsParams,
SearchLibraryDocsResult
> = {
name: 'search_library_docs',
async execute(params: SearchLibraryDocsParams): Promise<SearchLibraryDocsResult> {
const logger = createLogger('SearchLibraryDocsServerTool')
const { library_name, query, version } = params

if (!library_name || typeof library_name !== 'string') {
throw new Error('library_name is required')
}
if (!query || typeof query !== 'string') {
throw new Error('query is required')
}

// Build a search query that targets the library's documentation
const searchQuery = version
? `${library_name} ${version} documentation ${query}`
: `${library_name} documentation ${query}`

logger.info('Searching library documentation', {
library: library_name,
query,
version,
fullSearchQuery: searchQuery,
})

// Check which API keys are available
const hasExaApiKey = Boolean(env.EXA_API_KEY && String(env.EXA_API_KEY).length > 0)
const hasSerperApiKey = Boolean(env.SERPER_API_KEY && String(env.SERPER_API_KEY).length > 0)

// Try Exa first if available (better for documentation searches)
if (hasExaApiKey) {
try {
logger.debug('Attempting exa_search for library docs', { library: library_name })
const exaResult = await executeTool('exa_search', {
query: searchQuery,
numResults: 10,
type: 'auto',
apiKey: env.EXA_API_KEY || '',
})

const exaResults = (exaResult as any)?.output?.results || []
const count = Array.isArray(exaResults) ? exaResults.length : 0

logger.info('exa_search for library docs completed', {
success: exaResult.success,
resultsCount: count,
library: library_name,
})

if (exaResult.success && count > 0) {
const transformedResults = exaResults.map((result: any, idx: number) => ({
title: result.title || '',
link: result.url || '',
snippet: result.text || result.summary || '',
position: idx + 1,
}))

return {
results: transformedResults,
query,
library: library_name,
version,
totalResults: count,
}
}

logger.warn('exa_search returned no results for library docs, falling back to Serper', {
library: library_name,
})
} catch (exaError: any) {
logger.warn('exa_search failed for library docs, falling back to Serper', {
error: exaError?.message,
library: library_name,
})
}
}

// Fall back to Serper if Exa failed or wasn't available
if (!hasSerperApiKey) {
throw new Error('No search API keys available (EXA_API_KEY or SERPER_API_KEY required)')
}

try {
logger.debug('Calling serper_search for library docs', { library: library_name })
const result = await executeTool('serper_search', {
query: searchQuery,
num: 10,
type: 'search',
apiKey: env.SERPER_API_KEY || '',
})

const results = (result as any)?.output?.searchResults || []
const count = Array.isArray(results) ? results.length : 0

logger.info('serper_search for library docs completed', {
success: result.success,
resultsCount: count,
library: library_name,
})

if (!result.success) {
logger.error('serper_search failed for library docs', { error: (result as any)?.error })
throw new Error((result as any)?.error || 'Library documentation search failed')
}

// Transform serper results to match expected format
const transformedResults = results.map((result: any, idx: number) => ({
title: result.title || '',
link: result.link || '',
snippet: result.snippet || '',
position: idx + 1,
}))

return {
results: transformedResults,
query,
library: library_name,
version,
totalResults: count,
}
} catch (e: any) {
logger.error('search_library_docs execution error', {
message: e?.message,
library: library_name,
})
throw e
}
},
}
2 changes: 2 additions & 0 deletions apps/sim/lib/copilot/tools/server/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getBlocksAndToolsServerTool } from '@/lib/copilot/tools/server/blocks/g
import { getBlocksMetadataServerTool } from '@/lib/copilot/tools/server/blocks/get-blocks-metadata-tool'
import { getTriggerBlocksServerTool } from '@/lib/copilot/tools/server/blocks/get-trigger-blocks'
import { searchDocumentationServerTool } from '@/lib/copilot/tools/server/docs/search-documentation'
import { searchLibraryDocsServerTool } from '@/lib/copilot/tools/server/docs/search-library-docs'
import {
KnowledgeBaseInput,
knowledgeBaseServerTool,
Expand Down Expand Up @@ -47,6 +48,7 @@ serverToolRegistry[getTriggerBlocksServerTool.name] = getTriggerBlocksServerTool
serverToolRegistry[editWorkflowServerTool.name] = editWorkflowServerTool
serverToolRegistry[getWorkflowConsoleServerTool.name] = getWorkflowConsoleServerTool
serverToolRegistry[searchDocumentationServerTool.name] = searchDocumentationServerTool
serverToolRegistry[searchLibraryDocsServerTool.name] = searchLibraryDocsServerTool
serverToolRegistry[searchOnlineServerTool.name] = searchOnlineServerTool
serverToolRegistry[setEnvironmentVariablesServerTool.name] = setEnvironmentVariablesServerTool
serverToolRegistry[getCredentialsServerTool.name] = getCredentialsServerTool
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/stores/panel/copilot/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { RememberDebugClientTool } from '@/lib/copilot/tools/client/other/rememb
import { ResearchClientTool } from '@/lib/copilot/tools/client/other/research'
import { SearchDocumentationClientTool } from '@/lib/copilot/tools/client/other/search-documentation'
import { SearchErrorsClientTool } from '@/lib/copilot/tools/client/other/search-errors'
import { SearchLibraryDocsClientTool } from '@/lib/copilot/tools/client/other/search-library-docs'
import { SearchOnlineClientTool } from '@/lib/copilot/tools/client/other/search-online'
import { SearchPatternsClientTool } from '@/lib/copilot/tools/client/other/search-patterns'
import { SleepClientTool } from '@/lib/copilot/tools/client/other/sleep'
Expand Down Expand Up @@ -116,6 +117,7 @@ const CLIENT_TOOL_INSTANTIATORS: Record<string, (id: string) => any> = {
get_trigger_blocks: (id) => new GetTriggerBlocksClientTool(id),
search_online: (id) => new SearchOnlineClientTool(id),
search_documentation: (id) => new SearchDocumentationClientTool(id),
search_library_docs: (id) => new SearchLibraryDocsClientTool(id),
search_patterns: (id) => new SearchPatternsClientTool(id),
search_errors: (id) => new SearchErrorsClientTool(id),
remember_debug: (id) => new RememberDebugClientTool(id),
Expand Down Expand Up @@ -174,6 +176,7 @@ export const CLASS_TOOL_METADATA: Record<string, BaseClientToolMetadata | undefi
get_trigger_blocks: (GetTriggerBlocksClientTool as any)?.metadata,
search_online: (SearchOnlineClientTool as any)?.metadata,
search_documentation: (SearchDocumentationClientTool as any)?.metadata,
search_library_docs: (SearchLibraryDocsClientTool as any)?.metadata,
search_patterns: (SearchPatternsClientTool as any)?.metadata,
search_errors: (SearchErrorsClientTool as any)?.metadata,
remember_debug: (RememberDebugClientTool as any)?.metadata,
Expand Down