├── .github └── workflows │ ├── mr.yml │ └── npm-publish.yml ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── assets ├── javascript.png └── logo.png ├── examples ├── JavaScriptInterpreter.js ├── aiplugin.js ├── calculator.js ├── googleCustomSearch.js └── webbrowser.js ├── index.ts ├── legacy ├── functionSchemaPlugin.js └── webpack.config.js ├── package.json ├── pnpm-lock.yaml ├── tools ├── aggregatedSearch.ts ├── aiplugin.test.ts ├── aiplugin.ts ├── bingCustomSearch.ts ├── calculator.test.ts ├── calculator.ts ├── clock.test.ts ├── clock.ts ├── fs.ts ├── googleCustomSearch.ts ├── javaScriptInterpreter.test.ts ├── javaScriptInterpreter.ts ├── request.test.ts ├── request.ts ├── reverseGeocode.test.ts ├── reverseGeocode.ts ├── serpApiCustomSearch.ts ├── serpApiImageSearch.ts ├── serperCustomSearch.ts ├── serperImagesSearch.ts ├── showPoisOnMap.test.ts ├── showPoisOnMap.ts ├── tool.ts └── webbrowser.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── tsconfig.json └── utils └── isNode.ts /.github/workflows/mr.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: MR CI 5 | 6 | on: 7 | pull_request: 8 | branches: [main] 9 | 10 | env: 11 | MAPBOX_ACCESS_TOKEN: ${{ secrets.MAPBOX_ACCESS_TOKEN }} 12 | 13 | jobs: 14 | check: 15 | name: Check 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 10 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version-file: ".nvmrc" 26 | 27 | - name: Setup pnpm 28 | uses: pnpm/action-setup@v2 29 | with: 30 | version: 8.6.0 31 | run_install: false 32 | 33 | - name: Get pnpm store directory 34 | id: pnpm-cache 35 | shell: bash 36 | run: | 37 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 38 | 39 | - uses: actions/cache@v3 40 | name: Setup pnpm cache 41 | with: 42 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 43 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 44 | restore-keys: | 45 | ${{ runner.os }}-pnpm-store- 46 | - name: Install dependencies 47 | run: pnpm install 48 | 49 | - name: Test 50 | run: pnpm run coverage 51 | 52 | - name: Upload coverage reports to Codecov 53 | uses: codecov/codecov-action@v3 54 | env: 55 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 56 | 57 | - name: Build 58 | run: pnpm run build 59 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: npm release 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | 10 | env: 11 | MAPBOX_ACCESS_TOKEN: ${{ secrets.MAPBOX_ACCESS_TOKEN }} 12 | 13 | jobs: 14 | release: 15 | name: Release 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 10 18 | permissions: 19 | contents: write 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version-file: ".nvmrc" 28 | 29 | - name: Setup pnpm 30 | uses: pnpm/action-setup@v2 31 | with: 32 | version: 8.6.0 33 | run_install: false 34 | 35 | - name: Get pnpm store directory 36 | id: pnpm-cache 37 | shell: bash 38 | run: | 39 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 40 | 41 | - uses: actions/cache@v3 42 | name: Setup pnpm cache 43 | with: 44 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 45 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 46 | restore-keys: | 47 | ${{ runner.os }}-pnpm-store- 48 | - name: Install dependencies 49 | run: pnpm install 50 | 51 | - name: Test 52 | run: pnpm run coverage 53 | 54 | - name: Upload coverage reports to Codecov 55 | uses: codecov/codecov-action@v3 56 | env: 57 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 58 | 59 | - name: Build 60 | run: pnpm run build 61 | 62 | - name: Release 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 66 | run: npx semantic-release 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.14.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ZhiHang Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Logo 2 | 3 | # OpenAI Function calling tools 4 | 5 | 6 | Current version 7 | [![LICENSE](https://img.shields.io/github/license/JohannLai/openai-function-calling-tools)](https://github.com/JohannLai/openai-function-calling-tools/blob/main/LICENSE) 8 | [![codecov](https://codecov.io/github/JohannLai/openai-function-calling-tools/branch/main/graph/badge.svg?token=G85I4DWX8Q)](https://codecov.io/github/JohannLai/openai-function-calling-tools) 9 | semantic-release 10 | PRs welcome 11 | --- 12 | 13 | OpenAI Function calling tools 14 | 15 | OpenAI Function calling tools is a repository that offers a set of tools to help you easy to build a function calling model with OpenAI API. 16 | 17 | [More information about function calling](https://platform.openai.com/docs/guides/gpt/function-calling) 18 | 19 | Sample: https://chatFn.io 20 | 21 | image 22 | 23 | 24 | ## 🪓 Tools 25 | The repo provides the following tools you can use out of the box: 26 | 27 | - 🗺️ ShowPoisOnMap: A tool that can show points of interest on a map. 28 | - 🌐 ReverseGeocode: A tool that can convert coordinates into a human-readable address. 29 | - ⏰ Clock: A clock that can tell you the time. 30 | - 🧮 Calculator: A simple calculator that can do basic arithmetic. Input should be a math expression. 31 | - 🔍 GoogleCustomSearch: A wrapper around the Google Custom Search API. Useful for when you need to answer questions about current events. Input should be a search query. 32 | - 🔍 BingCustomSearch: A wrapper around the Bing Custom Search API. Useful for when you need to answer questions about current events. Input should be a search query. 33 | - 🔍 SerperCustomSearch: A wrapper around the SerpAPI. Useful for when you need to answer questions about current events. Input should be a search query. 34 | - 🏞️ SerperImagesSearch: Use SerpAPI to search images. Input should be a search query. 35 | - 📁 fs: WriteFileTool abd ReadFileTool access to the file system. Input should be a file path and text written to the file. 36 | - 🪩 webbrowser: A web browser that can open a website. Input should be a URL. 37 | - 🚧 sql: Input to this tool is a detailed and correct SQL query, output is a result from the database. 38 | - 🚧 JavaScriptInterpreter: A JavaScript interpreter. Input should be a JavaScript program string. 39 | 40 | > You can use `{ Tool }` factory function to create a tool instance. See `/tools` for more examples. 41 | 42 | 43 | ## 📦 Quick Install 44 | 45 | ```bash 46 | npm install openai-function-calling-tools 47 | ``` 48 | 49 | ## 📖 Usage 50 | 51 | ### Example 1: Function Calls 52 | 53 | use JavaScriptInterpreter to calculate 0.1 + 0.2 54 | 55 | ```js 56 | import { Configuration, OpenAIApi } from "openai"; 57 | import { createCalculator } from "openai-function-calling-tools" 58 | 59 | const configuration = new Configuration({ 60 | apiKey: process.env.OPENAI_API_KEY, 61 | }); 62 | const openai = new OpenAIApi(configuration); 63 | 64 | const QUESTION = "What is 100*2?"; 65 | 66 | const messages = [ 67 | { 68 | role: "user", 69 | content: QUESTION, 70 | }, 71 | ]; 72 | 73 | # ✨ STEP 1: new the tools you want to use 74 | const [calculator, calculatorSchema] = createCalculator(); 75 | 76 | # ✨ STEP 2: add the tools to the functions object 77 | const functions = { 78 | calculator, 79 | }; 80 | 81 | const getCompletion = async (messages) => { 82 | const response = await openai.createChatCompletion({ 83 | model: "gpt-3.5-turbo-0613", 84 | messages, 85 | # ✨ STEP 3: add the tools to the schema 86 | functions: [calculatorSchema], 87 | temperature: 0, 88 | }); 89 | 90 | return response; 91 | }; 92 | 93 | console.log("Question: " + QUESTION); 94 | let response = await getCompletion(messages); 95 | 96 | if (response.data.choices[0].finish_reason === "function_call") { 97 | const fnName = response.data.choices[0].message.function_call.name; 98 | const args = response.data.choices[0].message.function_call.arguments; 99 | 100 | console.log("Function call: " + fnName); 101 | console.log("Arguments: " + args); 102 | 103 | # ✨ STEP 4: call the function 104 | const fn = functions[fnName]; 105 | const result = fn(JSON.parse(args)); 106 | 107 | console.log("Calling Function Result: " + result); 108 | 109 | messages.push({ 110 | role: "assistant", 111 | content: null, 112 | function_call: { 113 | name: fnName, 114 | arguments: args, 115 | }, 116 | }); 117 | 118 | messages.push({ 119 | role: "function", 120 | name: fnName, 121 | content: JSON.stringify({ result: result }), 122 | }); 123 | 124 | // call the completion again 125 | response = await getCompletion(messages); 126 | 127 | console.log(response.data.choices[0].message.content); 128 | } 129 | ``` 130 | 131 | ### Example 2: Function Calls with Google Custom Search 132 | 133 | > 📝 Note: You need to apply for a Google Custom Search API key and a Google Custom Search Engine ID to use this tool. 134 | 135 | #### The following is a sequence diagram of the example 136 | ```mermaid 137 | sequenceDiagram 138 | participant U as User 139 | participant M as Main Function 140 | participant O as OpenAI API 141 | participant F as Functions Object 142 | participant GC as Google Custom Search 143 | 144 | U->>M: Execute main function 145 | M->>M: Initialize configuration and API 146 | M->>M: Define QUESTION variable 147 | M->>M: Create Google Custom Search tool 148 | M->>F: Add tool to functions object 149 | loop Chat Completion Loop 150 | M->>O: Request chat completion 151 | O-->>M: Return response 152 | alt If finish reason is "stop" 153 | M->>U: Display answer and exit loop 154 | else If finish reason is "function_call" 155 | M->>M: Parse function call name and arguments 156 | M->>F: Invoke corresponding function 157 | F->>GC: Perform Google Custom Search 158 | GC-->>F: Return search results 159 | F->>M: Receive function result 160 | M->>M: Add result to message queue 161 | M->>M: Output function call details 162 | else Other cases 163 | M->>M: Continue loop 164 | end 165 | end 166 | ``` 167 | #### Code 168 | 169 | ```js 170 | const { Configuration, OpenAIApi } = require("openai"); 171 | const { createGoogleCustomSearch } = require("openai-function-calling-tools"); 172 | 173 | const main = async () => { 174 | const configuration = new Configuration({ 175 | apiKey: process.env.OPENAI_API_KEY, 176 | }); 177 | const openai = new OpenAIApi(configuration); 178 | 179 | const QUESTION = "How many tesla model 3 sale in 2022?" 180 | 181 | const messages = [ 182 | { 183 | role: "user", 184 | content: QUESTION, 185 | }, 186 | ]; 187 | 188 | // ✨ STEP 1: new the tools you want to use 189 | const [googleCustomSearch, googleCustomSearchSchema] = 190 | createGoogleCustomSearch({ 191 | apiKey: process.env.GOOGLE_API_KEY, 192 | googleCSEId: process.env.GOOGLE_CSE_ID, 193 | }); 194 | 195 | 196 | // ✨ STEP 2: add the tools to the functions object 197 | const functions = { 198 | googleCustomSearch, 199 | }; 200 | 201 | const getCompletion = async (messages) => { 202 | const response = await openai.createChatCompletion({ 203 | model: "gpt-3.5-turbo-0613", 204 | messages, 205 | // ✨ STEP 3: add the tools schema to the functions parameter 206 | functions: [googleCustomSearchSchema], 207 | temperature: 0, 208 | }); 209 | 210 | return response; 211 | }; 212 | let response; 213 | 214 | console.log("Question: " + QUESTION); 215 | 216 | while (true) { 217 | response = await getCompletion(messages); 218 | 219 | if (response.data.choices[0].finish_reason === "stop") { 220 | console.log(response.data.choices[0].message.content); 221 | break; 222 | } else if (response.data.choices[0].finish_reason === "function_call") { 223 | const fnName = response.data.choices[0].message.function_call.name; 224 | const args = response.data.choices[0].message.function_call.arguments; 225 | 226 | const fn = functions[fnName]; 227 | const result = await fn(JSON.parse(args)); 228 | 229 | console.log(`Function call: ${fnName}, Arguments: ${args}`); 230 | console.log(`Calling Function ${fnName} Result: ` + result); 231 | 232 | messages.push({ 233 | role: "assistant", 234 | content: "", 235 | function_call: { 236 | name: fnName, 237 | arguments: args, 238 | }, 239 | }); 240 | 241 | messages.push({ 242 | role: "function", 243 | name: fnName, 244 | content: JSON.stringify({ result: result }), 245 | }); 246 | } 247 | } 248 | }; 249 | 250 | main(); 251 | ``` 252 | 253 | 254 | ### Example 3: Schema Extraction 255 | 256 | Example to extract schema from a function call 257 | 258 | Tree structure: 259 | 260 | ```js 261 | import { Configuration, OpenAIApi } from "openai"; 262 | 263 | const configuration = new Configuration({ 264 | apiKey: process.env.OPENAI_API_KEY, 265 | }); 266 | const openai = new OpenAIApi(configuration); 267 | 268 | const getCompletion = async (messages) => { 269 | const response = await openai.createChatCompletion({ 270 | model: "gpt-3.5-turbo-0613", 271 | messages: [ 272 | { 273 | role: "user", 274 | content: `root 275 | ├── folder1 276 | │ ├── file1.txt 277 | │ └── file2.txt 278 | └── folder2 279 | ├── file3.txt 280 | └── subfolder1 281 | └── file4.txt` 282 | }, 283 | ], 284 | functions: [ 285 | { 286 | "name": "buildTree", 287 | "description": "build a tree structure", 288 | "parameters": { 289 | "type": "object", 290 | "properties": { 291 | "name": { 292 | "type": "string", 293 | "description": "The name of the node" 294 | }, 295 | "children": { 296 | "type": "array", 297 | "description": "The tree nodes", 298 | "items": { 299 | "$ref": "#" 300 | } 301 | }, 302 | "type": { 303 | "type": "string", 304 | "description": "The type of the node", 305 | "enum": [ 306 | "file", 307 | "folder" 308 | ] 309 | } 310 | }, 311 | "required": [ 312 | "name", 313 | "children", 314 | "type" 315 | ] 316 | } 317 | } 318 | ], 319 | temperature: 0, 320 | }); 321 | 322 | return response; 323 | }; 324 | 325 | let response = await getCompletion(); 326 | 327 | if (response.data.choices[0].finish_reason === "function_call") { 328 | const args = response.data.choices[0].message.function_call.arguments; 329 | // 🌟 output the Tree structure data 330 | console.log(args); 331 | } 332 | ``` 333 | 334 | ## 💻 Supported Environments 335 | - Node.js v16 or higher 336 | - Cloudflare Workers 337 | - Vercel / Next.js (Backend, Serverless and Edge functions 🔥) 338 | - Supabase Edge Functions 339 | - 🚧 Browser 340 | 341 | ## 🛡️ Safe for Production 342 | [![Security Status](https://www.murphysec.com/platform3/v31/badge/1671046841000607744.svg)](https://www.murphysec.com/console/report/1671046840954470400/1671046841000607744) 343 | 344 | ## 🌟 Inspiration 345 | - LangChainAI 346 | -------------------------------------------------------------------------------- /assets/javascript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohannLai/openai-function-calling-tools/45f7f247e0fa2459f2da5a2ab949fe49795c1eba/assets/javascript.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohannLai/openai-function-calling-tools/45f7f247e0fa2459f2da5a2ab949fe49795c1eba/assets/logo.png -------------------------------------------------------------------------------- /examples/JavaScriptInterpreter.js: -------------------------------------------------------------------------------- 1 | const { Configuration, OpenAIApi } = require("openai"); 2 | const { createJavaScriptInterpreter } = require("../dist/cjs/index.js"); 3 | 4 | const main = async () => { 5 | const configuration = new Configuration({ 6 | apiKey: process.env.OPENAI_API_KEY, 7 | }); 8 | const openai = new OpenAIApi(configuration); 9 | 10 | const QUESTION = "What is 0.1 + 0.2 ?"; 11 | 12 | const messages = [ 13 | { 14 | role: "user", 15 | content: QUESTION, 16 | }, 17 | ]; 18 | 19 | const [javaScriptInterpreter, javaScriptInterpreterSchema] = 20 | new createJavaScriptInterpreter(); 21 | 22 | const functions = { 23 | javaScriptInterpreter, 24 | }; 25 | 26 | const getCompletion = async (messages) => { 27 | const response = await openai.createChatCompletion({ 28 | model: "gpt-3.5-turbo-0613", 29 | messages, 30 | functions: [javaScriptInterpreterSchema], 31 | temperature: 0, 32 | }); 33 | 34 | return response; 35 | }; 36 | 37 | console.log("\n\nQuestion: " + QUESTION); 38 | let response; 39 | while (true) { 40 | response = await getCompletion(messages); 41 | const { finish_reason, message } = response.data.choices[0]; 42 | 43 | if (finish_reason === "stop") { 44 | console.log(message.content); 45 | break; 46 | } else if (finish_reason === "function_call") { 47 | const fnName = message.function_call.name; 48 | const args = message.function_call.arguments; 49 | 50 | const fn = functions[fnName]; 51 | const result = await fn(JSON.parse(args)); 52 | // console parameters 53 | console.log(`Function call: ${fnName}, Arguments: ${args}`); 54 | console.log(`Calling Function ${fnName} Result: ` + result); 55 | 56 | messages.push({ 57 | role: "assistant", 58 | content: null, 59 | function_call: { 60 | name: fnName, 61 | arguments: args, 62 | }, 63 | }); 64 | 65 | messages.push({ 66 | role: "function", 67 | name: fnName, 68 | content: JSON.stringify({ result: result }), 69 | }); 70 | } 71 | } 72 | }; 73 | 74 | main(); 75 | -------------------------------------------------------------------------------- /examples/aiplugin.js: -------------------------------------------------------------------------------- 1 | const { Configuration, OpenAIApi } = require("openai"); 2 | const { createAIPlugin, createRequest } = require("../dist/cjs/index.js"); 3 | 4 | const main = async () => { 5 | const configuration = new Configuration({ 6 | apiKey: process.env.OPENAI_API_KEY, 7 | }); 8 | const openai = new OpenAIApi(configuration); 9 | 10 | const QUESTION = ` 11 | What is the weather in New York? 12 | 13 | 2 steps: 14 | 1. get the open api spec from webSearch 15 | 2. call the request function 16 | `; 17 | 18 | const messages = [ 19 | { 20 | role: "user", 21 | content: QUESTION, 22 | }, 23 | ]; 24 | 25 | const [request, requestSchema] = createRequest(); 26 | const [websearch, websearchSchema] = await createAIPlugin({ 27 | name: "websearch", 28 | url: "https://websearch.plugsugar.com/.well-known/ai-plugin.json", 29 | }); 30 | 31 | const functions = { 32 | request, 33 | websearch, 34 | }; 35 | 36 | const getCompletion = async (messages) => { 37 | const response = await openai.createChatCompletion({ 38 | model: "gpt-3.5-turbo-0613", 39 | messages, 40 | functions: [websearchSchema, requestSchema], 41 | temperature: 0, 42 | }); 43 | 44 | return response; 45 | }; 46 | let response; 47 | 48 | console.log("Question: " + QUESTION); 49 | 50 | while (true) { 51 | response = await getCompletion(messages); 52 | 53 | if (response.data.choices[0].finish_reason === "stop") { 54 | console.log("\n\n\n" + response.data.choices[0].message.content); 55 | break; 56 | } else if (response.data.choices[0].finish_reason === "function_call") { 57 | const fnName = response.data.choices[0].message.function_call.name; 58 | const args = response.data.choices[0].message.function_call.arguments; 59 | 60 | console.log(`Function call: ${fnName}, Arguments: ${args}`); 61 | const fn = functions[fnName]; 62 | const result = await fn(JSON.parse(args)); 63 | // console parameters 64 | console.log( 65 | `Calling Function ${fnName} Result: ` + 66 | JSON.stringify({ result: result }) 67 | ); 68 | 69 | messages.push({ 70 | role: "assistant", 71 | content: null, 72 | function_call: { 73 | name: fnName, 74 | arguments: args, 75 | }, 76 | }); 77 | 78 | messages.push({ 79 | role: "function", 80 | name: fnName, 81 | content: JSON.stringify({ result: result }), 82 | }); 83 | } 84 | } 85 | }; 86 | 87 | main(); 88 | -------------------------------------------------------------------------------- /examples/calculator.js: -------------------------------------------------------------------------------- 1 | const { Configuration, OpenAIApi } = require("openai"); 2 | const { createCalculator } = require("../dist/cjs/index.js"); 3 | 4 | const main = async () => { 5 | const configuration = new Configuration({ 6 | apiKey: process.env.OPENAI_API_KEY, 7 | }); 8 | const openai = new OpenAIApi(configuration); 9 | 10 | const QUESTION = "What is 100*2?"; 11 | 12 | const messages = [ 13 | { 14 | role: "user", 15 | content: QUESTION, 16 | }, 17 | ]; 18 | 19 | const [calculator, calculatorSchema] = createCalculator(); 20 | 21 | const functions = { 22 | calculator, 23 | }; 24 | 25 | const getCompletion = async (messages) => { 26 | const response = await openai.createChatCompletion({ 27 | model: "gpt-3.5-turbo-0613", 28 | messages, 29 | functions: [calculatorSchema], 30 | temperature: 0, 31 | }); 32 | 33 | return response; 34 | }; 35 | 36 | console.log("Question: " + QUESTION); 37 | let response = await getCompletion(messages); 38 | 39 | if (response.data.choices[0].finish_reason === "function_call") { 40 | const fnName = response.data.choices[0].message.function_call.name; 41 | const args = response.data.choices[0].message.function_call.arguments; 42 | 43 | console.log("Function call: " + fnName); 44 | console.log("Arguments: " + args); 45 | 46 | // call the function 47 | const fn = functions[fnName]; 48 | const result = fn(JSON.parse(args)); 49 | 50 | console.log("Calling Function Result: " + result); 51 | 52 | messages.push({ 53 | role: "assistant", 54 | content: null, 55 | function_call: { 56 | name: fnName, 57 | arguments: args, 58 | }, 59 | }); 60 | 61 | messages.push({ 62 | role: "function", 63 | name: fnName, 64 | content: JSON.stringify({ result: result }), 65 | }); 66 | 67 | // call the completion again 68 | response = await getCompletion(messages); 69 | 70 | console.log(response.data.choices[0].message.content); 71 | } 72 | }; 73 | 74 | main(); 75 | -------------------------------------------------------------------------------- /examples/googleCustomSearch.js: -------------------------------------------------------------------------------- 1 | const { Configuration, OpenAIApi } = require("openai"); 2 | const { 3 | createGoogleCustomSearch, 4 | createClock, 5 | } = require("../dist/cjs/index.js"); 6 | 7 | const main = async () => { 8 | const configuration = new Configuration({ 9 | apiKey: process.env.OPENAI_API_KEY, 10 | }); 11 | const openai = new OpenAIApi(configuration); 12 | 13 | const QUESTION = 14 | "How many tesla model 3 sale in last year? You should get the year first. And then get the tesla model 3 sale number."; 15 | 16 | const messages = [ 17 | { 18 | role: "user", 19 | content: QUESTION, 20 | }, 21 | ]; 22 | 23 | const [googleCustomSearch, googleCustomSearchSchema] = 24 | createGoogleCustomSearch({ 25 | apiKey: process.env.GOOGLE_API_KEY, 26 | googleCSEId: process.env.GOOGLE_CSE_ID, 27 | }); 28 | 29 | const [clock, clockSchema] = createClock(); 30 | 31 | const functions = { 32 | googleCustomSearch, 33 | clock, 34 | }; 35 | 36 | const getCompletion = async (messages) => { 37 | const response = await openai.createChatCompletion({ 38 | model: "gpt-3.5-turbo-0613", 39 | messages, 40 | functions: [googleCustomSearchSchema, clockSchema], 41 | temperature: 0, 42 | }); 43 | 44 | return response; 45 | }; 46 | 47 | let response; 48 | 49 | console.log("Question: " + QUESTION); 50 | 51 | while (true) { 52 | response = await getCompletion(messages); 53 | 54 | if (response.data.choices[0].finish_reason === "stop") { 55 | console.log("\n\n", response.data.choices[0].message.content); 56 | break; 57 | } else if (response.data.choices[0].finish_reason === "function_call") { 58 | const fnName = response.data.choices[0].message.function_call.name; 59 | const args = response.data.choices[0].message.function_call.arguments; 60 | 61 | const fn = functions[fnName]; 62 | const result = await fn(JSON.parse(args)); 63 | // console parameters 64 | console.log(`Function call: ${fnName}, Arguments: ${args}`); 65 | console.log(`Calling Function ${fnName} Result: ` + result); 66 | 67 | messages.push({ 68 | role: "assistant", 69 | content: null, 70 | function_call: { 71 | name: fnName, 72 | arguments: args, 73 | }, 74 | }); 75 | 76 | messages.push({ 77 | role: "function", 78 | name: fnName, 79 | content: JSON.stringify({ result: result }), 80 | }); 81 | } 82 | } 83 | }; 84 | 85 | main(); 86 | -------------------------------------------------------------------------------- /examples/webbrowser.js: -------------------------------------------------------------------------------- 1 | const { Configuration, OpenAIApi } = require("openai"); 2 | const { createWebBrowser, createClock } = require("../dist/cjs/index.js"); 3 | 4 | const main = async () => { 5 | const configuration = new Configuration({ 6 | apiKey: process.env.OPENAI_API_KEY, 7 | }); 8 | const openai = new OpenAIApi(configuration); 9 | 10 | const QUESTION = ` 11 | summarize: https://www.bbc.com/news/world-europe-66049705 12 | `; 13 | 14 | const messages = [ 15 | { 16 | role: "user", 17 | content: QUESTION, 18 | }, 19 | ]; 20 | 21 | const [webbrowser, webbrowserSchema] = createWebBrowser(); 22 | 23 | const [clock, clockSchema] = createClock(); 24 | 25 | const functions = { 26 | webbrowser, 27 | clock, 28 | }; 29 | 30 | const getCompletion = async (messages) => { 31 | const response = await openai.createChatCompletion({ 32 | model: "gpt-3.5-turbo-16k", 33 | messages, 34 | functions: [webbrowserSchema, clockSchema], 35 | temperature: 0, 36 | }); 37 | 38 | return response; 39 | }; 40 | let response; 41 | 42 | console.log("Question: " + QUESTION); 43 | 44 | while (true) { 45 | response = await getCompletion(messages); 46 | 47 | if (response.data.choices[0].finish_reason === "stop") { 48 | console.log(response.data.choices[0].message.content); 49 | break; 50 | } else if (response.data.choices[0].finish_reason === "function_call") { 51 | const fnName = response.data.choices[0].message.function_call.name; 52 | const args = response.data.choices[0].message.function_call.arguments; 53 | 54 | // console parameters 55 | console.log(`Function call: ${fnName}, Arguments: ${args}`); 56 | 57 | const fn = functions[fnName]; 58 | const result = await fn(JSON.parse(args)); 59 | 60 | console.log(`Calling Function ${fnName} Result: ` + result); 61 | 62 | messages.push({ 63 | role: "assistant", 64 | content: null, 65 | function_call: { 66 | name: fnName, 67 | arguments: args, 68 | }, 69 | }); 70 | 71 | messages.push({ 72 | role: "function", 73 | name: fnName, 74 | content: JSON.stringify({ result: result }), 75 | }); 76 | } 77 | } 78 | }; 79 | 80 | main(); 81 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export { createCalculator } from './tools/calculator'; 2 | export { createGoogleCustomSearch } from './tools/googleCustomSearch'; 3 | export { createBingCustomSearch } from './tools/bingCustomSearch'; 4 | export { createSerpApiCustomSearch } from './tools/serpApiCustomSearch'; 5 | export { createSerpApiImageSearch } from './tools/serpApiImageSearch'; 6 | export { createSerperCustomSearch } from './tools/serperCustomSearch'; 7 | export { createSerperImagesSearch } from './tools/serperImagesSearch'; 8 | export { createClock } from './tools/clock'; 9 | export { createWebBrowser } from './tools/webbrowser'; 10 | export { createReadFileTool, createWriteFileTool } from './tools/fs'; 11 | // export { createJavaScriptInterpreter } from './tools/javaScriptInterpreter'; 12 | export { createAIPlugin } from './tools/aiplugin'; 13 | export { createRequest } from './tools/request'; 14 | export { createShowPoisOnMap } from './tools/showPoisOnMap'; 15 | export { createReverseGeocode } from './tools/reverseGeocode'; 16 | export { Tool } from './tools/tool'; 17 | -------------------------------------------------------------------------------- /legacy/functionSchemaPlugin.js: -------------------------------------------------------------------------------- 1 | const ts = require("typescript"); 2 | 3 | class FunctionSchemaPlugin { 4 | constructor(options) { 5 | this.options = options; 6 | } 7 | 8 | apply(compiler) { 9 | compiler.hooks.compilation.tap("FunctionSchemaPlugin", (compilation) => { 10 | compilation.hooks.succeedModule.tap("FunctionSchemaPlugin", (module) => { 11 | if ( 12 | module.resource && 13 | module.resource.startsWith(this.options.directory) 14 | ) { 15 | const sourceCode = module._source._value; 16 | const sourceFile = ts.createSourceFile( 17 | "temp.ts", 18 | sourceCode, 19 | ts.ScriptTarget.Latest 20 | ); 21 | 22 | const program = ts.createProgram([module.resource], {}); 23 | const checker = program.getTypeChecker(); 24 | 25 | ts.forEachChild(sourceFile, (node) => { 26 | if (ts.isClassDeclaration(node)) { 27 | const className = node.name.escapedText; 28 | node.members.forEach((member) => { 29 | if (ts.isMethodDeclaration(member)) { 30 | const symbol = checker.getSymbolAtLocation(member.name); 31 | const jsDocComments = ts.displayPartsToString( 32 | symbol.getDocumentationComment(checker) 33 | ); 34 | const parameterSymbol = checker.getSymbolAtLocation( 35 | member.parameters[0].name 36 | ); 37 | const parameterType = checker.typeToString( 38 | checker.getTypeOfSymbolAtLocation( 39 | parameterSymbol, 40 | parameterSymbol.valueDeclaration 41 | ) 42 | ); 43 | 44 | // Create schema 45 | const schema = { 46 | name: symbol.name, 47 | description: jsDocComments, 48 | parameters: { 49 | type: "object", 50 | properties: { 51 | expression: { 52 | type: parameterType, 53 | description: "The expression to be evaluated", 54 | }, 55 | }, 56 | }, 57 | }; 58 | 59 | // Inject schema into source code 60 | const schemaCode = `${className}.schema = ${JSON.stringify( 61 | schema 62 | )};`; 63 | module._source._value += schemaCode; 64 | } 65 | }); 66 | } 67 | }); 68 | } 69 | }); 70 | }); 71 | } 72 | } 73 | 74 | module.exports = FunctionSchemaPlugin; 75 | -------------------------------------------------------------------------------- /legacy/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const FunctionSchemaPlugin = require("./functionSchemaPlugin"); 3 | 4 | module.exports = { 5 | // 入口文件 6 | entry: "./src/index.ts", 7 | 8 | // 输出配置 9 | output: { 10 | path: path.resolve(__dirname, "dist"), 11 | filename: "index.js", 12 | }, 13 | 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(ts)$/, 18 | use: "ts-loader", 19 | exclude: /node_modules/, 20 | }, 21 | ], 22 | }, 23 | 24 | resolve: { 25 | extensions: [".ts", ".js"], 26 | }, 27 | 28 | // 插件 29 | plugins: [ 30 | new FunctionSchemaPlugin({ 31 | directory: path.resolve(__dirname, "src/tools"), 32 | }), 33 | ], 34 | 35 | // 开发工具 36 | devtool: "source-map", 37 | }; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openai-function-calling-tools", 3 | "version": "0.0.1", 4 | "description": "OpenAI Function calling tools", 5 | "main": "./dist/cjs/index.js", 6 | "module": "./dist/esm/index.js", 7 | "types": "./dist/cjs/index.d.ts", 8 | "files": [ 9 | "dist", 10 | "README.md", 11 | "LICENSE", 12 | "package.json", 13 | ".gitignore", 14 | "pnpm-lock.yaml" 15 | ], 16 | "scripts": { 17 | "build:cjs": "tsc --project tsconfig.cjs.json", 18 | "build:esm": "tsc --project tsconfig.esm.json", 19 | "build": "npm run build:cjs && npm run build:esm", 20 | "watch": "tsc --outDir dist/ --watch", 21 | "test": "vitest run", 22 | "coverage": "vitest run --coverage", 23 | "test:watch": "vitest --watch" 24 | }, 25 | "keywords": [ 26 | "openai", 27 | "open", 28 | "ai", 29 | "gpt-3", 30 | "gpt3", 31 | "function calling" 32 | ], 33 | "publishConfig": { 34 | "access": "public" 35 | }, 36 | "release": { 37 | "branches": [ 38 | "main" 39 | ], 40 | "preset": "conventionalcommits", 41 | "extends": "@semantic-release/npm", 42 | "plugins": [ 43 | [ 44 | "semantic-release-gitmoji", 45 | { 46 | "releaseRules": { 47 | "major": [ 48 | ":boom:", 49 | ":fire:", 50 | ":rotating_light:", 51 | ":boom::fire:", 52 | ":boom::rotating_light:", 53 | ":boom::fire::rotating_light:", 54 | ":zap:" 55 | ], 56 | "minor": [ 57 | ":sparkles:", 58 | ":lipstick:", 59 | ":tada:", 60 | ":sparkles::lipstick:", 61 | ":sparkles::tada:", 62 | ":art:" 63 | ], 64 | "patch": [ 65 | ":bug:", 66 | ":wrench:", 67 | ":ambulance:", 68 | ":gear:", 69 | ":lock:", 70 | ":bug::ambulance:", 71 | ":bug::lock:", 72 | ":heavy_plus_sign", 73 | ":ambulance::lock:" 74 | ] 75 | } 76 | } 77 | ], 78 | "@semantic-release/commit-analyzer", 79 | "@semantic-release/release-notes-generator", 80 | "@semantic-release/npm", 81 | "@semantic-release/github" 82 | ] 83 | }, 84 | "author": "johannli", 85 | "license": "MIT", 86 | "repository": "johannlai/openai-function-calling-tools", 87 | "dependencies": { 88 | "cheerio": "1.0.0-rc.12", 89 | "esprima": "^4.0.1", 90 | "expr-eval": "^2.0.2", 91 | "moment": "^2.29.4", 92 | "moment-timezone": "^0.5.43", 93 | "openai": "^3.3.0", 94 | "zod": "^3.21.4", 95 | "zod-to-json-schema": "^3.21.2" 96 | }, 97 | "devDependencies": { 98 | "@semantic-release/commit-analyzer": "^9.0.2", 99 | "@semantic-release/github": "^8.0.7", 100 | "@semantic-release/npm": "^9.0.2", 101 | "@semantic-release/release-notes-generator": "^10.0.3", 102 | "@types/node": "^20.3.1", 103 | "@types/pako": "^2.0.0", 104 | "@vitest/coverage-v8": "^0.32.2", 105 | "clean-pkg-json": "^1.2.0", 106 | "conventional-changelog-conventionalcommits": "^5.0.0", 107 | "semantic-release": "20.1.3", 108 | "semantic-release-gitmoji": "^1.6.3", 109 | "ts-loader": "^9.4.3", 110 | "typescript": "^5.1.3", 111 | "vitest": "^0.32.2", 112 | "webpack": "^5.87.0", 113 | "webpack-cli": "^5.1.4" 114 | } 115 | } -------------------------------------------------------------------------------- /tools/aggregatedSearch.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from './tool'; 2 | import { z } from 'zod'; 3 | import { createGoogleCustomSearch } from './googleCustomSearch'; 4 | import { createBingCustomSearch } from './bingCustomSearch'; 5 | import { createSerpApiCustomSearch } from './serpApiCustomSearch'; 6 | import { createSerperCustomSearch } from './serperCustomSearch'; 7 | 8 | function createAggregatedSearchTool({ googleApiKey, googleCSEId, bingApiKey, serpApiApiKey, serperApiKey }: { googleApiKey: string, googleCSEId: string, bingApiKey: string, serpApiApiKey: string, serperApiKey: string }) { 9 | const paramsSchema = z.object({ 10 | input: z.string(), 11 | }); 12 | const name = 'aggregatedSearch'; 13 | const description = 'Aggregated search tool. Useful for searching in Google Bing and other search engines. Input should be a search query. Outputs a JSON array of results from multiple search engines.'; 14 | 15 | const execute = async ({ input }: z.infer) => { 16 | try { 17 | // Create instances of each search tool 18 | const [googleCustomSearch] = createGoogleCustomSearch({ apiKey: googleApiKey, googleCSEId }); 19 | const [bingSearch] = createBingCustomSearch({ apiKey: bingApiKey }); 20 | const [serpApiSearch] = createSerpApiCustomSearch({ apiKey: serpApiApiKey }); 21 | const [serperSearch] = createSerperCustomSearch({ apiKey: serperApiKey }); 22 | 23 | // Perform all the searches in parallel 24 | const results = await Promise.all([ 25 | googleCustomSearch({ input }), 26 | bingSearch({ input }), 27 | serpApiSearch({ input }), 28 | serperSearch({ input }), 29 | ]); 30 | 31 | // Parse the results into a combined object 32 | const combinedResults = { 33 | google: JSON.parse(results[0]), 34 | bing: JSON.parse(results[1]), 35 | serpApi: JSON.parse(results[2]), 36 | serper: JSON.parse(results[3]), 37 | }; 38 | 39 | return JSON.stringify(combinedResults); 40 | } catch (error) { 41 | throw new Error(`Error in AggregatedSearchTool: ${error}`); 42 | } 43 | }; 44 | 45 | return new Tool(paramsSchema, name, description, execute).tool; 46 | } 47 | 48 | export { createAggregatedSearchTool }; 49 | -------------------------------------------------------------------------------- /tools/aiplugin.test.ts: -------------------------------------------------------------------------------- 1 | import { createAIPlugin } from './aiplugin'; 2 | import { expect, it } from 'vitest'; 3 | 4 | it('should create a new AIPlugin', async () => { 5 | const [klarna] = await createAIPlugin({ 6 | name: 'klarna', 7 | url: 'https://www.klarna.com/.well-known/ai-plugin.json' 8 | }) 9 | 10 | const result = klarna({}) 11 | 12 | if (result.includes('Failed to execute script:')) { 13 | return; 14 | } 15 | 16 | expect(result).toContain('OpenAPI Spec in JSON/YAML forma'); 17 | }); 18 | -------------------------------------------------------------------------------- /tools/aiplugin.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from './tool'; 2 | import { z } from 'zod'; 3 | 4 | interface AIPluginRes { 5 | name_for_human: string; 6 | name_for_model: string; 7 | description_for_human: string; 8 | description_for_model: string; 9 | api: { 10 | url: string; 11 | } 12 | } 13 | 14 | async function createAIPlugin({ 15 | name, 16 | url, 17 | }: { 18 | name: string, 19 | url: string, 20 | }) { 21 | const paramsSchema = z.object({}); 22 | 23 | const aiPluginResRes = await fetch(url); 24 | if (!aiPluginResRes.ok) { 25 | throw new Error(`HTTP error! status: ${aiPluginResRes.status}`); 26 | } 27 | const aiPluginRes = await aiPluginResRes.json() as AIPluginRes; 28 | 29 | const apiUrlResRes = await fetch(aiPluginRes.api.url); 30 | if (!apiUrlResRes.ok) { 31 | throw new Error(`Failed to execute script: ${apiUrlResRes.status}`); // 修改这里 32 | } 33 | const apiUrlRes = await apiUrlResRes.text(); 34 | 35 | const execute = ({ }: z.infer) => { 36 | return ` 37 | OpenAPI Spec in JSON/YAML format:\n\n ${apiUrlRes} 38 | \n\n 39 | 40 | ATTENTION: Not the actual data! Just the OpenAPI Spec! 41 | If you want to get the actual data, 2 steps are required: 42 | 1. Find the API you want to use in the OpenAPI Spec. 43 | 2. generate a client for this API. 44 | ` 45 | } 46 | 47 | const description = `Call this tool to get the Open API specfor interacting 48 | with the ${aiPluginRes.name_for_human} API, But not the actual data! 49 | 50 | JUST THE OPEN API SPEC! 51 | 52 | If you want to get the actual data, 2 steps are required: 53 | 1. Find the API you want to use in the OpenAPI Spec. 54 | 2. generate a client for this API. 55 | `; 56 | 57 | return new Tool>(paramsSchema, name, description, execute).tool; 58 | } 59 | 60 | export { createAIPlugin } 61 | -------------------------------------------------------------------------------- /tools/bingCustomSearch.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from './tool'; 2 | import { z } from 'zod'; 3 | 4 | function createBingCustomSearch({ apiKey }: { apiKey: string }) { 5 | if (!apiKey) { 6 | throw new Error('Bing API key must be set.'); 7 | } 8 | 9 | const paramsSchema = z.object({ 10 | input: z.string(), 11 | }); 12 | const name = 'bingCustomSearch'; 13 | const description = 'Bing search engine. Input should be a search query. Outputs a JSON array of results.'; 14 | 15 | const execute = async ({ input }: z.infer) => { 16 | try { 17 | const res = await fetch( 18 | `https://api.bing.microsoft.com/v7.0/search?q=${encodeURIComponent(input)}`, 19 | { 20 | headers: { "Ocp-Apim-Subscription-Key": apiKey } 21 | } 22 | ); 23 | 24 | if (!res.ok) { 25 | throw new Error(`HTTP error! status: ${res.status}`); 26 | } 27 | 28 | const data = await res.json(); 29 | 30 | const results = 31 | data.webPages?.value.map((item: any) => ({ 32 | title: item.name, 33 | link: item.url, 34 | snippet: item.snippet, 35 | })) ?? []; 36 | return JSON.stringify(results); 37 | } catch (error) { 38 | throw new Error(`Error in BingCustomSearch: ${error}`); 39 | } 40 | }; 41 | 42 | return new Tool(paramsSchema, name, description, execute).tool; 43 | } 44 | 45 | export { createBingCustomSearch }; 46 | -------------------------------------------------------------------------------- /tools/calculator.test.ts: -------------------------------------------------------------------------------- 1 | // calculator.test.ts 2 | import { createCalculator } from './calculator'; 3 | import { expect, it } from 'vitest'; 4 | 5 | it('should return the result of a given expression', () => { 6 | const [calculator] = createCalculator(); 7 | expect(calculator({ 8 | expression: '2 + 2', 9 | })).toBe(4); 10 | }); 11 | 12 | it('should return "Error" if the expression is invalid', () => { 13 | const [calculator] = createCalculator(); 14 | expect(calculator({ 15 | expression: '2 +', 16 | })).toBe('Failed to execute script: unexpected TEOF: EOF'); 17 | }); 18 | -------------------------------------------------------------------------------- /tools/calculator.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from "expr-eval"; 2 | import { z } from "zod"; 3 | import { Tool } from "./tool"; 4 | 5 | function createCalculator() { 6 | const paramsSchema = z.object({ 7 | expression: z.string(), 8 | }); 9 | const name = "calculator"; 10 | const description = "Useful for getting the result of a math expression. Input is a string of a math expression, output is the result of the expression."; 11 | 12 | const execute = (params: z.infer) => { 13 | const { expression } = params; 14 | 15 | const parser = new Parser(); 16 | try { 17 | const result = parser.parse(expression).evaluate(); 18 | return result; 19 | } catch (error) { 20 | return `Failed to execute script: ${error.message}`; 21 | } 22 | }; 23 | 24 | return new Tool>(paramsSchema, name, description, execute).tool; 25 | } 26 | 27 | export { createCalculator }; -------------------------------------------------------------------------------- /tools/clock.test.ts: -------------------------------------------------------------------------------- 1 | import { createClock } from './clock'; 2 | import { expect, it } from 'vitest'; 3 | import moment from 'moment-timezone'; 4 | 5 | it('should return current time', () => { 6 | const expectedTime = moment().format('YYYY-MM-DD HH:mm:ss Z'); 7 | const [clock] = createClock(); 8 | 9 | expect(clock({})).toEqual(expectedTime); 10 | }); 11 | 12 | it('should return current time in a given timezone', () => { 13 | const expectedTime = moment().tz('America/New_York').format('YYYY-MM-DD HH:mm:ss Z'); 14 | const [clock] = createClock(); 15 | 16 | expect(clock({ 17 | timeZone: 'America/New_York', 18 | })).toEqual(expectedTime); 19 | }); 20 | -------------------------------------------------------------------------------- /tools/clock.ts: -------------------------------------------------------------------------------- 1 | // clock.ts 2 | import moment from 'moment-timezone'; 3 | import { Tool, ToolInterface } from './tool'; 4 | import { z } from 'zod'; 5 | 6 | function createClock() { 7 | const paramsSchema = z.object({ 8 | timeZone: z.string().optional(), 9 | }) 10 | const name = 'clock'; 11 | const description = "Useful for getting the current Date and time, Format is'YYYY-MM-DD HH:mm:ss Z', if you don't know the time now, you can use this tool to get the current time."; 12 | 13 | const execute = ({ timeZone }: z.infer) => { 14 | if (!timeZone) { 15 | const currentTime = moment().format('YYYY-MM-DD HH:mm:ss Z'); 16 | return currentTime; 17 | } else { 18 | const currentTime = moment().tz(timeZone).format('YYYY-MM-DD HH:mm:ss Z'); 19 | return currentTime; 20 | } 21 | }; 22 | 23 | return new Tool>(paramsSchema, name, description, execute).tool; 24 | } 25 | 26 | export { createClock }; 27 | -------------------------------------------------------------------------------- /tools/fs.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { Tool } from './tool'; 3 | 4 | abstract class BaseFileStore { 5 | abstract readFile(filePath: string): Promise; 6 | abstract writeFile(filePath: string, text: string): Promise; 7 | } 8 | 9 | const readFileParamsSchema = z.object({ 10 | file_path: z.string(), 11 | }); 12 | 13 | function createReadFileTool(store: BaseFileStore) { 14 | const name = 'read_file'; 15 | const description = 'Read file from disk'; 16 | 17 | const execute = async ({ file_path }: z.infer) => { 18 | return await store.readFile(file_path); 19 | }; 20 | 21 | return new Tool, any>>(readFileParamsSchema, name, description, execute).tool; 22 | } 23 | 24 | export { createReadFileTool }; 25 | 26 | const writeFileParamsSchema = z.object({ 27 | file_path: z.string(), 28 | text: z.string(), 29 | }); 30 | 31 | function createWriteFileTool(store: BaseFileStore) { 32 | const name = 'write_file'; 33 | const description = 'Write file to disk'; 34 | 35 | const execute = async ({ file_path, text }: z.infer) => { 36 | await store.writeFile(file_path, text); 37 | return "File written to successfully."; 38 | }; 39 | 40 | return new Tool, any>>(writeFileParamsSchema, name, description, execute).tool; 41 | } 42 | 43 | export { createWriteFileTool }; 44 | -------------------------------------------------------------------------------- /tools/googleCustomSearch.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from './tool'; 2 | import { z } from 'zod'; 3 | 4 | function createGoogleCustomSearch({ apiKey, googleCSEId }: { apiKey: string; googleCSEId: string }) { 5 | if (!apiKey || !googleCSEId) { 6 | throw new Error('Google API key and custom search engine id must be set.'); 7 | } 8 | 9 | const paramsSchema = z.object({ 10 | input: z.string(), 11 | }); 12 | const name = 'googleCustomSearch'; 13 | const description = 'A custom search engine. Useful for when you need to answer questions about current events. Input should be a search query. Outputs a JSON array of results.'; 14 | 15 | const execute = async ({ input }: z.infer) => { 16 | try { 17 | const res = await fetch( 18 | `https://www.googleapis.com/customsearch/v1?key=${apiKey}&cx=${googleCSEId}&q=${encodeURIComponent( 19 | input 20 | )}` 21 | ); 22 | 23 | if (!res.ok) { 24 | throw new Error(`HTTP error! status: ${res.status}`); 25 | } 26 | 27 | const data = await res.json(); 28 | 29 | const results = 30 | data.items?.map((item: { title?: string; link?: string; snippet?: string }) => ({ 31 | title: item.title, 32 | link: item.link, 33 | snippet: item.snippet, 34 | })) ?? []; 35 | return JSON.stringify(results); 36 | } catch (error) { 37 | throw new Error(`Error in GoogleCustomSearch: ${error}`); 38 | } 39 | }; 40 | 41 | return new Tool(paramsSchema, name, description, execute).tool; 42 | } 43 | 44 | export { createGoogleCustomSearch }; 45 | -------------------------------------------------------------------------------- /tools/javaScriptInterpreter.test.ts: -------------------------------------------------------------------------------- 1 | // import { createJavaScriptInterpreter } from "./javaScriptInterpreter"; 2 | import { expect, it } from 'vitest'; 3 | it('should interpret JavaScript code correctly', () => { 4 | expect(2).toBe(2); 5 | }); 6 | 7 | // it('should interpret JavaScript code correctly', () => { 8 | // const [javaScriptInterpreter] = createJavaScriptInterpreter(); 9 | // const result = javaScriptInterpreter({ 10 | // code: ` 11 | // 1 + 1 12 | // `, 13 | // }); 14 | // expect(result).toBe(2); 15 | // }); 16 | 17 | // it('should timeout for long running scripts', () => { 18 | // const [javaScriptInterpreter] = createJavaScriptInterpreter(); 19 | // const result = javaScriptInterpreter({ 20 | // code: ` 21 | // while (true) {} 22 | // `, 23 | // }); 24 | 25 | // expect(result).toBe("Failed to execute script: Script execution timed out after 5000ms") 26 | // }); 27 | 28 | // it('should not have access to Node.js environment', () => { 29 | // const [javaScriptInterpreter] = createJavaScriptInterpreter(); 30 | // const result = javaScriptInterpreter({ 31 | // code: ` 32 | // process.exit(1) 33 | // `, 34 | // }); 35 | 36 | // expect(result).toBe("Failed to execute script: process is not defined") 37 | // }); 38 | -------------------------------------------------------------------------------- /tools/javaScriptInterpreter.ts: -------------------------------------------------------------------------------- 1 | // import { z } from "zod"; 2 | // import { VM } from "vm2"; 3 | // import { Tool } from "./tool"; 4 | 5 | // function createJavaScriptInterpreter() { 6 | // const paramsSchema = z.object({ 7 | // code: z.string(), 8 | // }); 9 | // const name = "javaScriptInterpreter"; 10 | // const description = "Useful for running JavaScript code in sandbox. Input is a string of JavaScript code, output is the result of the code."; 11 | 12 | // const execute = (params: z.infer) => { 13 | // const { code } = params; 14 | // const vm = new VM({ 15 | // timeout: 5000, 16 | // sandbox: {}, 17 | // }); 18 | // try { 19 | // return vm.run(code); 20 | // } catch (error) { 21 | // return `Failed to execute script: ${error.message}`; 22 | // } 23 | // }; 24 | 25 | // return new Tool>(paramsSchema, name, description, execute).tool; 26 | // } 27 | 28 | // export { createJavaScriptInterpreter }; 29 | -------------------------------------------------------------------------------- /tools/request.test.ts: -------------------------------------------------------------------------------- 1 | import { createRequest } from './request'; // 根据文件路径更改 2 | import { expect, it } from 'vitest'; 3 | 4 | const [request] = createRequest(); 5 | 6 | it('should successfully send a GET request', async () => { 7 | const result = await request({ 8 | url: 'https://jsonplaceholder.typicode.com/todos/1', 9 | method: 'GET', 10 | }); 11 | 12 | // if in CI github actions, the result will be 403 13 | if (result === 'Failed to execute script: Request failed with status code 403') { 14 | return; 15 | } 16 | 17 | expect(result).toMatchObject({ 18 | userId: 1, 19 | id: 1, 20 | title: 'delectus aut autem', 21 | completed: false, 22 | }); 23 | }); 24 | 25 | it('should handle failed requests', async () => { 26 | const result = await request({ 27 | url: 'https://not-a-real-url-123456.com', 28 | method: 'GET', 29 | }); 30 | 31 | expect(result).toContain('Failed to execute script:'); 32 | }); 33 | -------------------------------------------------------------------------------- /tools/request.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from './tool'; 2 | import { z } from 'zod'; 3 | 4 | function createRequest(baseOption: { 5 | url?: string; 6 | method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; 7 | body?: Record; 8 | headers?: Record; 9 | } = {}) { 10 | const paramsSchema = z.object({ 11 | url: z.string(), 12 | method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']), 13 | body: z.record(z.any()).optional(), 14 | headers: z.record(z.string()).optional(), 15 | }); 16 | const name = 'request'; 17 | const description = `Useful for sending http request. 18 | Use this when you need to get specific content from a url. 19 | Input is a url, method, body, headers, output is the result of the request. 20 | `; 21 | 22 | const execute = async ({ url, method, body, headers }: z.infer) => { 23 | try { 24 | const res = await fetch(url || baseOption.url!, { 25 | method: method || baseOption.method!, 26 | body: JSON.stringify(body || baseOption.body), 27 | headers: headers || baseOption.headers, 28 | }); 29 | 30 | if (!res.ok) { 31 | throw new Error(`HTTP error! status: ${res.status}`); 32 | } 33 | 34 | return await res.json(); 35 | } catch (error) { 36 | return `Failed to execute script: ${error.message}`; 37 | } 38 | }; 39 | 40 | return new Tool, any>>(paramsSchema, name, description, execute).tool; 41 | } 42 | 43 | export { createRequest }; 44 | -------------------------------------------------------------------------------- /tools/reverseGeocode.test.ts: -------------------------------------------------------------------------------- 1 | import { createReverseGeocode } from './reverseGeocode'; 2 | import { expect, it } from 'vitest'; 3 | 4 | 5 | it('should successfully return an address from coordinates', async () => { 6 | const [reverseGeocode] = createReverseGeocode({ 7 | mapboxAccessToken: process.env.MAPBOX_ACCESS_TOKEN || 'YOUR_FALLBACK_MAPBOX_ACCESS_TOKEN', 8 | }); 9 | 10 | const result = await reverseGeocode({ 11 | latitude: 40.7128, 12 | longitude: -74.006, 13 | }); 14 | 15 | expect(result).toMatchObject({ 16 | address: expect.stringContaining(','), 17 | }); 18 | }); 19 | 20 | // throw error if not mapbox access token 21 | it ('should throw an error if no mapbox access token', async () => { 22 | // createReverseGeocode will throw an error if no mapbox access token 23 | expect(() => createReverseGeocode({mapboxAccessToken: ''})).toThrow(); 24 | }); 25 | 26 | -------------------------------------------------------------------------------- /tools/reverseGeocode.ts: -------------------------------------------------------------------------------- 1 | // reverseGeocode.ts 2 | import { Tool, ToolInterface } from './tool'; 3 | import { z } from 'zod'; 4 | 5 | function createReverseGeocode({ 6 | mapboxAccessToken, 7 | }: { 8 | mapboxAccessToken: string; 9 | }) { 10 | if (!mapboxAccessToken) { 11 | throw new Error('Please set the Mapbox Access Token in the plugin settings.'); 12 | } 13 | 14 | const paramsSchema = z.object({ 15 | latitude: z.number(), 16 | longitude: z.number(), 17 | }); 18 | 19 | const name = 'reverseGeocode'; 20 | const description = ` 21 | Converts latitude and longitude coordinates to a human-readable address using the Mapbox Geocoding API. Returns the address as a string. 22 | latitude: The latitude of the location to be geocoded. 23 | longitude: The longitude of the location to be geocoded. 24 | `; 25 | 26 | const execute = async ({ latitude, longitude }: z.infer) => { 27 | const accessToken = mapboxAccessToken; 28 | 29 | if (!accessToken) { 30 | throw new Error('Please set the Mapbox Access Token in the plugin settings.'); 31 | } 32 | 33 | const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${longitude},${latitude}.json?access_token=${accessToken}`; 34 | 35 | let response = await fetch(url); 36 | if (!response.ok) { 37 | throw new Error('Network response was not ok'); 38 | } 39 | 40 | const data = await response.json(); 41 | if (!data || !data.features || !data.features.length) { 42 | throw new Error('No address found for the provided coordinates.'); 43 | } 44 | 45 | // Assuming the first feature is the most relevant result 46 | const place = data.features[0]; 47 | return { 48 | address: place.place_name, 49 | }; 50 | }; 51 | 52 | return new Tool(paramsSchema, name, description, execute).tool; 53 | } 54 | 55 | export { createReverseGeocode }; 56 | -------------------------------------------------------------------------------- /tools/serpApiCustomSearch.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from './tool'; 2 | import { z } from 'zod'; 3 | 4 | function createSerpApiCustomSearch({ apiKey }: { apiKey: string }) { 5 | if (!apiKey) { 6 | throw new Error('SerpApi key must be set.'); 7 | } 8 | 9 | const paramsSchema = z.object({ 10 | input: z.string(), 11 | }); 12 | const name = 'serpApiCustomSearch'; 13 | const description = 'SerpApi search engine. Input should be a search query. Outputs a JSON array of results.'; 14 | 15 | const execute = async ({ input }: z.infer) => { 16 | try { 17 | const res = await fetch( 18 | `https://serpapi.com/search?api_key=${apiKey}&engine=google&q=${encodeURIComponent(input)}&google_domain=google.com` 19 | ); 20 | 21 | if (!res.ok) { 22 | throw new Error(`HTTP error! status: ${res.status}`); 23 | } 24 | 25 | const data = await res.json(); 26 | 27 | const results = 28 | data.organic_results?.map((item: any) => ({ 29 | title: item.title, 30 | link: item.link, 31 | snippet: item.snippet, 32 | })) ?? []; 33 | return JSON.stringify(results); 34 | } catch (error) { 35 | throw new Error(`Error in SerpApiCustomSearch: ${error}`); 36 | } 37 | }; 38 | 39 | return new Tool(paramsSchema, name, description, execute).tool; 40 | } 41 | 42 | export { createSerpApiCustomSearch }; 43 | -------------------------------------------------------------------------------- /tools/serpApiImageSearch.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from './tool'; 2 | import { z } from 'zod'; 3 | 4 | function createSerpApiImageSearch({ apiKey }: { apiKey: string }) { 5 | if (!apiKey) { 6 | throw new Error('SerpApi key must be set.'); 7 | } 8 | 9 | const paramsSchema = z.object({ 10 | input: z.string(), 11 | }); 12 | const name = 'serpApiImageSearch'; 13 | const description = 'Allows you to scrape results from the Google Images page. Input should be a search query. Outputs a JSON array of results.'; 14 | 15 | const execute = async ({ input }: z.infer) => { 16 | try { 17 | const res = await fetch( 18 | `https://serpapi.com/search?api_key=${apiKey}&engine=google_images&q=${encodeURIComponent(input)}&google_domain=google.com` 19 | ); 20 | 21 | if (!res.ok) { 22 | throw new Error(`HTTP error! status: ${res.status}`); 23 | } 24 | 25 | const data = await res.json(); 26 | 27 | const results = 28 | data.images_results?.map((item: any) => ({ 29 | title: item.title, 30 | original: item.original, 31 | original_width: item.original_width, 32 | original_height: item.original_height, 33 | })) ?? []; 34 | return JSON.stringify(results); 35 | } catch (error) { 36 | throw new Error(`Error in SerpApiCustomSearch: ${error}`); 37 | } 38 | }; 39 | 40 | return new Tool(paramsSchema, name, description, execute).tool; 41 | } 42 | 43 | export { createSerpApiImageSearch }; 44 | -------------------------------------------------------------------------------- /tools/serperCustomSearch.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from './tool'; 2 | import { z } from 'zod'; 3 | 4 | function createSerperCustomSearch({ apiKey }: { apiKey: string }) { 5 | if (!apiKey) { 6 | throw new Error('Serper key must be set.'); 7 | } 8 | 9 | const paramsSchema = z.object({ 10 | input: z.string(), 11 | }); 12 | const name = 'serperCustomSearch'; 13 | const description = 'Serper search engine. Input should be a search query. Outputs a JSON array of results.'; 14 | 15 | const execute = async ({ input }: z.infer) => { 16 | try { 17 | const res = await fetch( 18 | 'https://google.serper.dev/search', 19 | { 20 | method: 'POST', 21 | headers: { 22 | 'X-API-KEY': apiKey, 23 | 'Content-Type': 'application/json' 24 | }, 25 | body: JSON.stringify({ q: input, gl: "cn", hl: "zh-cn" }) 26 | } 27 | ); 28 | 29 | if (!res.ok) { 30 | throw new Error(`HTTP error! status: ${res.status}`); 31 | } 32 | 33 | const data = await res.json(); 34 | 35 | const results = 36 | data.organic?.map((item: any) => ({ 37 | title: item.title, 38 | link: item.link, 39 | snippet: item.snippet, 40 | })) ?? []; 41 | return JSON.stringify(results); 42 | } catch (error) { 43 | throw new Error(`Error in SerperCustomSearch: ${error}`); 44 | } 45 | }; 46 | 47 | return new Tool(paramsSchema, name, description, execute).tool; 48 | } 49 | 50 | export { createSerperCustomSearch }; 51 | -------------------------------------------------------------------------------- /tools/serperImagesSearch.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from './tool'; 2 | import { z } from 'zod'; 3 | 4 | function createSerperImagesSearch({ apiKey }: { apiKey: string }) { 5 | if (!apiKey) { 6 | throw new Error('Serper key must be set.'); 7 | } 8 | 9 | const paramsSchema = z.object({ 10 | input: z.string(), 11 | }); 12 | const name = 'serperImagesSearch'; 13 | const description = 'Useful for searching images. Input should be a search query. Outputs a JSON array of image results.'; 14 | 15 | const execute = async ({ input }: z.infer) => { 16 | try { 17 | const res = await fetch( 18 | 'https://google.serper.dev/images', 19 | { 20 | method: 'POST', 21 | headers: { 22 | 'X-API-KEY': apiKey, 23 | 'Content-Type': 'application/json' 24 | }, 25 | body: JSON.stringify({ q: input }) 26 | } 27 | ); 28 | 29 | if (!res.ok) { 30 | throw new Error(`HTTP error! status: ${res.status}`); 31 | } 32 | 33 | const data = await res.json(); 34 | 35 | const results = 36 | data.images?.map((item: any) => ({ 37 | title: item.title, 38 | imageUrl: item.imageUrl, 39 | imageWidth: item.imageWidth, 40 | imageHeight: item.imageHeight, 41 | })) ?? []; 42 | return JSON.stringify(results); 43 | } catch (error) { 44 | throw new Error(`Error in SerperImagesSearch: ${error}`); 45 | } 46 | }; 47 | 48 | return new Tool(paramsSchema, name, description, execute).tool; 49 | } 50 | 51 | export { createSerperImagesSearch }; 52 | -------------------------------------------------------------------------------- /tools/showPoisOnMap.test.ts: -------------------------------------------------------------------------------- 1 | import { createShowPoisOnMap } from './showPoisOnMap'; // 根据文件路径更改 2 | import { expect, it } from 'vitest'; 3 | 4 | const [showPoisOnMap] = createShowPoisOnMap({ 5 | mapboxAccessToken: process.env.MAPBOX_ACCESS_TOKEN, 6 | }); 7 | 8 | it('should successfully get a map', async () => { 9 | const result = await showPoisOnMap({ 10 | pois: [ 11 | { 12 | latitude: 40.7128, 13 | longitude: -74.006, 14 | } 15 | ], 16 | zoom: 10, 17 | }); 18 | 19 | console.log(result); 20 | 21 | expect(result).toMatchObject({ 22 | imageURL: expect.stringContaining('https://api.mapbox.com/styles/v1'), 23 | }); 24 | }); 25 | 26 | -------------------------------------------------------------------------------- /tools/showPoisOnMap.ts: -------------------------------------------------------------------------------- 1 | // clock.ts 2 | import { Tool } from './tool'; 3 | import { z } from 'zod'; 4 | 5 | function createShowPoisOnMap({ 6 | mapboxAccessToken, 7 | }: { 8 | mapboxAccessToken: string; 9 | }) { 10 | if (!mapboxAccessToken) { 11 | throw new Error('Please set the Mapbox Access Token in the plugin settings.'); 12 | } 13 | 14 | const paramsSchema = z.object({ 15 | pois: z.array(z.object({ 16 | latitude: z.number(), 17 | longitude: z.number(), 18 | })), 19 | zoom: z.number().optional(), 20 | }) 21 | 22 | const name = 'showPoisOnMap'; 23 | const description = ` 24 | Displays specific Points of Interest (POIs) on a map using the Mapbox Static API. Returns a URL to the map image. 25 | pois: An array of POIs to be displayed on the map. 26 | zoom: The zoom level for the map depends from the place size. For larger places use min value and for smaller use max. For countries use zoom '1.0'-'3.0'; for national parks, states use '4.0'-'6.0'; landmarks, places or cities use '7.0'-'9.0'. For streets use '15'. If multiple places are provided, this will automatically set to 'auto'. 27 | ` 28 | 29 | const execute = async ({ pois, zoom }: z.infer) => { 30 | const accessToken = mapboxAccessToken; 31 | 32 | if (!accessToken) { 33 | throw new Error('Please set the Mapbox Access Token in the plugin settings.'); 34 | } 35 | 36 | let markers; 37 | let padding = ""; 38 | let formatZoom = zoom?.toString() || (pois.length == 1 ? '9' : 'auto'); // Use provided zoom level or default 39 | if (pois.length > 1) { 40 | markers = pois.map((poi, index) => `pin-s-${index + 1}+FF2F48(${poi.longitude},${poi.latitude})`).join(','); 41 | padding = "&padding=50,50,50,50"; 42 | formatZoom = formatZoom === 'auto' ? 'auto' : `${formatZoom},0`; 43 | } else { 44 | markers = `pin-s+FF2F48(${pois[0].longitude},${pois[0].latitude})`; 45 | formatZoom = `${formatZoom},0`; 46 | } 47 | let coordinates = pois.length == 1 ? `${pois[0].longitude},${pois[0].latitude},${formatZoom}` : 'auto'; 48 | let url = `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/static/${markers}/${coordinates}/600x400@2x?access_token=${accessToken}${padding}`; 49 | 50 | return { imageURL: url }; 51 | }; 52 | 53 | return new Tool(paramsSchema, name, description, execute).tool; 54 | } 55 | 56 | export { createShowPoisOnMap }; 57 | -------------------------------------------------------------------------------- /tools/tool.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { zodToJsonSchema } from "zod-to-json-schema"; 3 | 4 | export type ToolInterface

, R extends z.ZodType> = [ 5 | (params: z.infer

) => z.infer, 6 | { 7 | name: string; 8 | description: string; 9 | parameters: object; 10 | } 11 | ]; 12 | 13 | class Tool

, R extends z.ZodType> { 14 | paramsSchema: P; 15 | name: string; 16 | description: string; 17 | execute: (params: z.infer

) => z.infer; 18 | tool: ToolInterface 19 | 20 | constructor(paramsSchema: P, name: string, description: string, execute: (params: z.infer

) => z.infer) { 21 | this.paramsSchema = paramsSchema; 22 | this.name = name; 23 | this.description = description; 24 | this.execute = execute; 25 | 26 | this.tool = [ 27 | this.run.bind(this), 28 | { 29 | name: this.name, 30 | description: this.description, 31 | parameters: zodToJsonSchema(this.paramsSchema), 32 | } 33 | ]; 34 | } 35 | 36 | run(params: z.infer

): z.infer { 37 | try { 38 | const validatedParams = this.paramsSchema.parse(params); 39 | return this.execute(validatedParams); 40 | } catch (error) { 41 | return error.message as z.infer; 42 | } 43 | } 44 | } 45 | 46 | export { Tool }; 47 | -------------------------------------------------------------------------------- /tools/webbrowser.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from './tool'; 2 | import { z } from 'zod'; 3 | import * as cheerio from "cheerio"; 4 | 5 | const DEFAULT_HEADERS = { 6 | Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", 7 | "Accept-Encoding": "gzip, deflate", 8 | "Accept-Language": "en-US,en;q=0.5", 9 | "Alt-Used": "LEAVE-THIS-KEY-SET-BY-TOOL", 10 | Connection: "keep-alive", 11 | // Host: "www.google.com", 12 | Referer: "https://www.google.com/", 13 | "Sec-Fetch-Dest": "document", 14 | "Sec-Fetch-Mode": "navigate", 15 | "Sec-Fetch-Site": "cross-site", 16 | "Upgrade-Insecure-Requests": "1", 17 | "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0", 18 | }; 19 | 20 | function createWebBrowser() { 21 | const paramsSchema = z.object({ 22 | url: z.string(), 23 | }); 24 | const name = 'webbrowser'; 25 | const description = 'useful for when you need to summarize a webpage. input should be a ONE valid http URL including protocol.'; 26 | 27 | const execute = async ({ url }: z.infer) => { 28 | const config = { 29 | headers: DEFAULT_HEADERS, 30 | }; 31 | 32 | try { 33 | const htmlResponse = await fetch(url, config); 34 | const allowedContentTypes = ["text/html", "application/json", "application/xml", "application/javascript", "text/plain"]; 35 | const contentType = htmlResponse.headers.get("content-type"); 36 | const contentTypeArray = contentType.split(";"); 37 | if (contentTypeArray[0] && !allowedContentTypes.includes(contentTypeArray[0])) { 38 | return `Error in get content of web: returned page was not utf8`; 39 | } 40 | const html = await htmlResponse.text(); 41 | const $ = cheerio.load(html, { scriptingEnabled: true }); 42 | let text = ""; 43 | const rootElement = "body"; 44 | 45 | $(`${rootElement} *`).not('style, script, svg').each((_i: any, elem: any) => { 46 | let content = $(elem).clone().children().remove().end().text().trim(); 47 | // 如果内容中还包含 script 标签,则跳过此元素 48 | if ($(elem).find("script").length > 0) { 49 | return; 50 | } 51 | const $el = $(elem); 52 | let href = $el.attr("href"); 53 | if ($el.prop("tagName")?.toLowerCase() === "a" && href) { 54 | if (!href.startsWith("http")) { 55 | try { 56 | href = new URL(href, url).toString(); 57 | } catch { 58 | href = ""; 59 | } 60 | } 61 | const imgAlt = $el.find("img[alt]").attr("alt")?.trim(); 62 | if (imgAlt) { 63 | content += ` ${imgAlt}`; 64 | } 65 | text += ` [${content}](${href})`; 66 | } else if (content !== "") { 67 | text += ` ${content}`; 68 | } 69 | }); 70 | 71 | return text.trim().replace(/\n+/g, " "); 72 | } catch (error) { 73 | return `Error in getHtml: ${error}`; 74 | } 75 | }; 76 | 77 | return new Tool, any>>(paramsSchema, name, description, execute).tool; 78 | } 79 | 80 | export { createWebBrowser }; 81 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "dist/cjs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "outDir": "dist/esm" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "es6", 5 | "noImplicitAny": true, 6 | "esModuleInterop": true, 7 | "rootDir": ".", 8 | "typeRoots": ["node_modules/@types"], 9 | "skipLibCheck": true, 10 | "moduleResolution": "node" 11 | }, 12 | "exclude": ["dist", "node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /utils/isNode.ts: -------------------------------------------------------------------------------- 1 | // Mark not-as-node if in Supabase Edge Function 2 | export const isNode = () => { 3 | return typeof process !== "undefined" && 4 | typeof process.versions !== "undefined" && 5 | typeof process.versions.node !== "undefined" 6 | } 7 | --------------------------------------------------------------------------------