/ src / server / tools / web-tool-execution.ts
web-tool-execution.ts
  1  import type {
  2    BrowserFetchResponse,
  3    DuckDuckGoSearchType,
  4    WebFetchResponse,
  5    WebSearchResponse,
  6  } from '@/lib/shared/chat'
  7  import { performBrowserFetch } from '@/server/tools/browser-fetch'
  8  import { performWebFetch } from '@/server/tools/web-fetch'
  9  import { performWebSearch } from '@/server/tools/web-search'
 10  
 11  const MAX_WEB_TOOL_SEARCH_RESULTS = 10
 12  const MAX_WEB_TOOL_FETCH_BYTES = 500_000
 13  
 14  export type WebToolSearchExecutionInput = {
 15    query: string
 16    searchType?: DuckDuckGoSearchType
 17    maxResults?: number
 18  }
 19  
 20  export type WebToolFetchExecutionInput = {
 21    url: string
 22    maxBytes?: number
 23  }
 24  
 25  export type BrowserToolFetchExecutionInput = {
 26    url: string
 27    maxBytes?: number
 28    preserveLinks?: boolean
 29  }
 30  
 31  export type WebToolSearchExecutionResult =
 32    | {
 33      ok: true
 34      response: WebSearchResponse
 35      results: WebSearchResponse['results']
 36    }
 37    | { ok: false; error: string }
 38  
 39  export type WebToolFetchExecutionResult =
 40    | {
 41      ok: true
 42      response: WebFetchResponse
 43    }
 44    | { ok: false; error: string }
 45  
 46  export type BrowserToolFetchExecutionResult =
 47    | {
 48      ok: true
 49      response: BrowserFetchResponse
 50    }
 51    | { ok: false; error: string }
 52  
 53  export async function executeWebSearchTool(
 54    input: WebToolSearchExecutionInput,
 55  ): Promise<WebToolSearchExecutionResult> {
 56    const normalizedMaxResults = normalizeWebSearchMaxResults(input.maxResults)
 57  
 58    try {
 59      const response = await performWebSearch({
 60        query: input.query,
 61        searchType: input.searchType,
 62        maxResults: normalizedMaxResults,
 63      })
 64  
 65      return {
 66        ok: true,
 67        response,
 68        results: normalizedMaxResults
 69          ? response.results.slice(0, normalizedMaxResults)
 70          : response.results,
 71      }
 72    } catch (error) {
 73      return {
 74        ok: false,
 75        error: error instanceof Error ? error.message : 'Web search failed',
 76      }
 77    }
 78  }
 79  
 80  export async function executeWebFetchTool(
 81    input: WebToolFetchExecutionInput,
 82  ): Promise<WebToolFetchExecutionResult> {
 83    const normalizedMaxBytes = normalizeWebFetchMaxBytes(input.maxBytes)
 84  
 85    try {
 86      const response = await performWebFetch({
 87        url: input.url,
 88        maxBytes: normalizedMaxBytes,
 89      })
 90      return {
 91        ok: true,
 92        response,
 93      }
 94    } catch (error) {
 95      return {
 96        ok: false,
 97        error: error instanceof Error ? error.message : 'Web fetch failed',
 98      }
 99    }
100  }
101  
102  export async function executeBrowserFetchTool(
103    input: BrowserToolFetchExecutionInput,
104  ): Promise<BrowserToolFetchExecutionResult> {
105    const normalizedMaxBytes = normalizeWebFetchMaxBytes(input.maxBytes)
106    const preserveLinks = input.preserveLinks === true
107  
108    try {
109      const response = await performBrowserFetch({
110        url: input.url,
111        maxBytes: normalizedMaxBytes,
112        ...(preserveLinks ? { preserveLinks: true } : {}),
113      })
114      return {
115        ok: true,
116        response,
117      }
118    } catch (error) {
119      return {
120        ok: false,
121        error: error instanceof Error ? error.message : 'Browser fetch failed',
122      }
123    }
124  }
125  
126  function normalizeWebSearchMaxResults(value: number | undefined): number | undefined {
127    if (typeof value !== 'number' || value <= 0) return undefined
128    return Math.min(value, MAX_WEB_TOOL_SEARCH_RESULTS)
129  }
130  
131  function normalizeWebFetchMaxBytes(value: number | undefined): number | undefined {
132    if (typeof value !== 'number' || value <= 0) return undefined
133    return Math.min(value, MAX_WEB_TOOL_FETCH_BYTES)
134  }