浏览代码

browser/interface: add websearch + webcrawl (#243)

nicole pardal 1 周之前
父节点
当前提交
5267b5632a
共有 3 个文件被更改,包括 217 次插入0 次删除
  1. 142 0
      examples/websearch/websearch-tools.ts
  2. 38 0
      src/browser.ts
  3. 37 0
      src/interfaces.ts

+ 142 - 0
examples/websearch/websearch-tools.ts

@@ -0,0 +1,142 @@
+import ollama, { Ollama } from 'ollama'
+import type { Message } from 'ollama'
+
+async function main() {
+
+  if (!process.env.OLLAMA_API_KEY) throw new Error('Set OLLAMA_API_KEY to use websearch tools')
+
+  const client = new Ollama({
+    headers: { Authorization: `Bearer ${process.env.OLLAMA_API_KEY}` },
+  })
+
+  // Tool schemas
+  const websearchTool = {
+    type: 'function',
+    function: {
+      name: 'websearch',
+      description: 'Performs a web search for the given queries.',
+      parameters: {
+        type: 'object',
+        properties: {
+          queries: {
+            type: 'array',
+            items: { type: 'string' },
+            description: 'An array of search queries.',
+          },
+          max_results: {
+            type: 'number',
+            description: 'The maximum number of results to return per query (default 5, max 10).',
+          },
+        },
+        required: ['queries'],
+      },
+    },
+  }
+
+  const webcrawlTool = {
+    type: 'function',
+    function: {
+      name: 'webcrawl',
+      description: 'Performs a web crawl for the given URLs.',
+      parameters: {
+        type: 'object',
+        properties: {
+          urls: {
+            type: 'array',
+            items: { type: 'string' },
+            description: 'An array of URLs to crawl.',
+          },
+        },
+        required: ['urls'],
+      },
+    },
+  }
+
+  const availableTools = {
+    websearch: async (args: { queries: string[]; max_results?: number }) => {
+      return await client.search(args)
+    },
+    webcrawl: async (args: { urls: string[] }) => {
+      return await client.crawl(args)
+    },
+  }
+
+  const messages: Message[] = [
+    {
+      role: 'user',
+      content: 'What is Ollama?',
+    },
+  ]
+
+  console.log('----- Prompt:', messages.find((m) => m.role === 'user')?.content, '\n')
+
+  while (true) {
+    const response = await ollama.chat({
+      model: 'gpt-oss',
+      messages: messages,
+      tools: [websearchTool, webcrawlTool],
+      stream: true,
+      think: true,
+    })
+
+    let hadToolCalls = false
+    let startedThinking = false
+    let finishedThinking = false
+    var content = ''
+    var thinking = ''
+    for await (const chunk of response) {
+      if (chunk.message.thinking && !startedThinking) {
+        startedThinking = true
+        process.stdout.write('Thinking:\n========\n\n')
+      } else if (chunk.message.content && startedThinking && !finishedThinking) {
+        finishedThinking = true
+        process.stdout.write('\n\nResponse:\n========\n\n')
+      }
+
+      if (chunk.message.thinking) {
+        thinking += chunk.message.thinking
+        process.stdout.write(chunk.message.thinking)
+      }
+      if (chunk.message.content) {
+        content += chunk.message.content
+        process.stdout.write(chunk.message.content)
+      }
+      if (chunk.message.tool_calls && chunk.message.tool_calls.length > 0) {
+        messages.push({
+          role: 'assistant',
+          content: content,
+          thinking: thinking,
+        })
+        
+        hadToolCalls = true
+        for (const toolCall of chunk.message.tool_calls) {
+          const functionToCall = availableTools[toolCall.function.name]
+          if (functionToCall) {
+            const args = toolCall.function.arguments as any
+            console.log('\nCalling function:', toolCall.function.name, 'with arguments:', args)
+            const output = await functionToCall(args)
+            console.log('Function output:', JSON.stringify(output).slice(0, 200), '\n')
+
+            // message history
+            messages.push(chunk.message)
+            // tool result
+            messages.push({
+              role: 'tool',
+              content: JSON.stringify(output),
+              tool_name: toolCall.function.name,
+            })
+          }
+        }
+      }
+    }
+
+    if (!hadToolCalls) {
+      process.stdout.write('\n')
+      break
+    }
+
+    console.log('----- Sending result back to model \n')
+  }
+}
+
+main().catch(console.error)

+ 38 - 0
src/browser.ts

@@ -24,6 +24,10 @@ import type {
   ShowRequest,
   ShowResponse,
   StatusResponse,
+  SearchRequest,
+  SearchResponse,
+  CrawlRequest,
+  CrawlResponse,
 } from './interfaces.js'
 import { defaultHost } from './constant.js'
 
@@ -320,6 +324,40 @@ async encodeImage(image: Uint8Array | string): Promise<string> {
     })
     return (await response.json()) as ListResponse
   }
+
+  /**
+   * Performs web search using the Ollama web search API
+   * @param request {SearchRequest} - The search request containing queries and options
+   * @returns {Promise<SearchResponse>} - The search results
+   * @throws {Error} - If the request is invalid or the server returns an error
+   */
+  async search(request: SearchRequest): Promise<SearchResponse> {
+    if (!request.queries || request.queries.length === 0) {
+      throw new Error('At least one query is required')
+    }
+
+    const response = await utils.post(this.fetch, `https://ollama.com/api/web_search`, { ...request }, {
+      headers: this.config.headers
+    })
+    return (await response.json()) as SearchResponse
+  }
+
+  /**
+   * Performs web crawl using the Ollama web crawl API
+   * @param request {CrawlRequest} - The crawl request containing URLs and options
+   * @returns {Promise<CrawlResponse>} - The crawl results
+   * @throws {Error} - If the request is invalid or the server returns an error
+   */
+  async crawl(request: CrawlRequest): Promise<CrawlResponse> {
+    if (!request.urls || request.urls.length === 0) {
+      throw new Error('At least one URL is required')
+    }
+
+    const response = await utils.post(this.fetch, `https://ollama.com/api/web_crawl`, { ...request }, {
+      headers: this.config.headers
+    })
+    return (await response.json()) as CrawlResponse
+  }
 }
 
 export default new Ollama()

+ 37 - 0
src/interfaces.ts

@@ -269,3 +269,40 @@ export interface ErrorResponse {
 export interface StatusResponse {
   status: string
 }
+
+// Web Search types
+export interface SearchRequest {
+  queries: string[]
+  max_results?: number
+}
+
+
+export interface SearchResult {
+  title: string
+  url: string
+  content: string
+}
+
+export interface SearchResponse {
+  results: Record<string, SearchResult[]>
+  success: boolean
+  errors?: string[]
+}
+
+// Crawl types - commented out removed fields
+export interface CrawlRequest {
+  urls: string[]
+}
+
+export interface CrawlResult {
+  title: string
+  url: string
+  content: string
+  links: string[]
+}
+
+export interface CrawlResponse {
+  results: Record<string, CrawlResult[]>
+  success: boolean
+  errors?: string[]
+}