├── .gitignore ├── src ├── .well-known │ ├── logo.png │ ├── ai-plugin.json │ └── openapi.yaml ├── env.example ├── search-result.ts ├── index.ts └── utils.ts ├── tsconfig.json ├── LICENSE ├── package.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | */.env 4 | .env -------------------------------------------------------------------------------- /src/.well-known/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinamiri/chatgpt-plugin-googlesearch/HEAD/src/.well-known/logo.png -------------------------------------------------------------------------------- /src/env.example: -------------------------------------------------------------------------------- 1 | # Google Custom Search API for ChatGPT 2 | GOOGLE_API_KEY=your-google-api-key 3 | CX=your-google-cx 4 | ROOT_DOMAIN=your-root-domain 5 | CONTACT_EMAIL=Your Email 6 | PLUGIN_NAME=Your Plugin Name -------------------------------------------------------------------------------- /src/search-result.ts: -------------------------------------------------------------------------------- 1 | // src/search-result.ts 2 | 3 | class SearchResult { 4 | constructor(public title: string, public link: string, public content_chunk: string) { } 5 | 6 | toDict() { 7 | return { 8 | title: this.title, 9 | link: this.link, 10 | content_chunk: this.content_chunk 11 | }; 12 | } 13 | } 14 | 15 | export default SearchResult; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": false, 7 | "allowSyntheticDefaultImports": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "sourceMap": true, 13 | "outDir": "dist", 14 | "lib": [ 15 | "esnext", 16 | "dom" 17 | ], 18 | "esModuleInterop": true, 19 | }, 20 | "include": [ 21 | "**/*.ts" 22 | ], 23 | "exclude": [ 24 | "node_modules" 25 | ] 26 | } -------------------------------------------------------------------------------- /src/.well-known/ai-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "v1", 3 | "name_for_model": "googleSearch", 4 | "name_for_human": "Google Search Plugin", 5 | "description_for_model": "Plugin for searching using Google Custom Search Engine and fetching the inner text of the first pages in the search results.", 6 | "description_for_human": "Search using Google Custom Search Engine.", 7 | "auth": { 8 | "type": "none" 9 | }, 10 | "api": { 11 | "type": "openapi", 12 | "url": "https://your_subdomain.maila.ai/.well-known/openai.yaml", 13 | "has_user_authentication": false 14 | }, 15 | "logo_url": "https://your_subdomain.maila.ai/.well-known/logo.png", 16 | "contact_email": "kevin@maila.ai", 17 | "legal_info_url": "https://your_subdomain.maila.ai/legal" 18 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kevin Amiri 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-plugin-googlesearch", 3 | "version": "1.0.0", 4 | "description": "ChatGPT with Google Search Integration is a powerful plugin that enhances your conversational AI experience by providing real-time search capabilities directly within the chat interface. This integration enables ChatGPT to access and utilize Google's vast repository of data, making your conversations more informative, accurate, and engaging.", 5 | "main": "index.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "chatgpt-plugin", 11 | "chatgpt-plugin-google-search", 12 | "chatgpt-plugin-google", 13 | "chatgpt-plugin-search", 14 | "chatgpt-plugin-google-search-integration", 15 | "Express.js chatGPT plugin" 16 | ], 17 | "author": "Kevin Amiri", 18 | "license": "MIT", 19 | "dependencies": { 20 | "axios": "^1.3.6", 21 | "cheerio": "^1.0.0-rc.12", 22 | "dotenv": "^16.0.3", 23 | "express": "^4.18.2" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.21.4", 27 | "@babel/preset-env": "^7.21.4", 28 | "@babel/preset-typescript": "^7.21.4", 29 | "@types/body-parser": "^1.19.2", 30 | "@types/cheerio": "^0.22.31", 31 | "@types/express": "^4.17.17", 32 | "@types/node": "^18.11.18", 33 | "ts-node": "^10.9.1", 34 | "typescript": "^4.9.4" 35 | } 36 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Google Search Integration for ChatGPT (Plugin) 2 | 3 | A Typescript Express.js App 4 | 5 | ## Overview 6 | 7 | Google Search Integration for ChatGPT is a plugin that allows you to seamlessly perform Google searches and retrieve information from search results directly within the ChatGPT environment. This enhances your ChatGPT experience by providing relevant information at your fingertips. 8 | 9 | [A comprehensive guide to creating chatGPT plugin using Nodejs](https://cookbook.maila.ai/en/guides/creating-a-chatgpt-plugin) 10 | 11 | **Requirements:** 12 | 13 | - Access to ChatGPT plugins 14 | - A ChatGPT Plus account 15 | - Google search API key 16 | 17 | **Dependencies:** 18 | 19 | ```json 20 | { 21 | "axios": "^1.3.6", 22 | "cheerio": "^1.0.0-rc.12", 23 | "dotenv": "^16.0.3", 24 | "express": "^4.18.2" 25 | } 26 | ``` 27 | 28 | ## Features 29 | 30 | - Perform Google searches right from ChatGPT 31 | - Retrieve top search results 32 | 33 | ## Installation 34 | 35 | 1. Clone this repository: 36 | 37 | ```bash 38 | git clone https://github.com/kevinamiri/chatgpt-plugin-googlesearch.git 39 | ``` 40 | 41 | 2. Navigate to the project directory: 42 | 43 | ```bash 44 | cd chatgpt-plugin-googlesearch 45 | ``` 46 | 47 | 3. Install dependencies: 48 | 49 | ```bash 50 | npm install 51 | ``` 52 | 53 | 4. Start the Express server: 54 | 55 | ```bash 56 | cd src 57 | ``` 58 | 59 | ```bash 60 | npx ts-node index.ts 61 | ``` 62 | 63 | ## Usage and reference 64 | 65 | This project depends on **OpenAI plugins** and **Google Search API**. To set up your plugin correctly, please follow these steps: 66 | 67 | 1. Read the documentation for both dependencies: 68 | - [OpenAI Documentation](https://platform.openai.com/docs/plugins/introduction) 69 | - [Google Search API Documentation](https://developers.google.com/custom-search/v1/introduction) 70 | - [A comprehensive guide to creating chatGPT plugin using Nodejs](https://cookbook.maila.ai/en/guides/creating-a-chatgpt-plugin) 71 | 72 | 2. Ensure you have proper authentication in place, as this example does not provide it. 73 | 74 | 3. Verify that your plugin meets all the requirements specified in the OpenAI and Google Search API documentation. 75 | 76 | 77 | 78 | ## Contributing 79 | 80 | 1. Fork the repository on GitHub 81 | 2. Create a new branch with a descriptive name, e.g., `git checkout -b my-new-feature` 82 | 3. Make changes to the code, ensuring that it follows the existing style and conventions 83 | 4. Commit your changes, using clear and informative commit messages 84 | 5. Push your changes to your forked repository 85 | 6. Open a pull request against the main repository, describing your changes and how they improve the project 86 | 7. Wait for a review and respond to any feedback or requested changes 87 | 88 | ## License 89 | 90 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 91 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // path: src/index.ts 2 | 3 | import express from "express"; 4 | import * as http from "http"; 5 | import * as bodyParser from "body-parser"; 6 | import * as dotenv from 'dotenv'; 7 | import axios from 'axios'; 8 | import path from 'path'; 9 | import { processResults } from './utils'; 10 | 11 | 12 | const app = express(); 13 | 14 | 15 | // // Certificate (You do not need this part, It's better to use NGINX Reverse or similar 16 | // const certificate = fs.readFileSync('/etc/letsencrypt/live/maila.ai/fullchain.pem', 'utf8'); 17 | // const privateKey = fs.readFileSync('/etc/letsencrypt/live/maila.ai/privkey.pem', 'utf8'); 18 | // const ca = fs.readFileSync('/etc/letsencrypt/live/maila.ai/chain.pem', 'utf8'); 19 | 20 | // const credentials = { 21 | // key: privateKey, 22 | // cert: certificate, 23 | // ca: ca 24 | // }; 25 | 26 | 27 | 28 | const httpServer = http.createServer(app); 29 | // const httpsServer = https.createServer(credentials, app); 30 | 31 | const httpPort = 3000; 32 | // const httpsPort = 3001; 33 | 34 | app.use(function (req, res, next) { 35 | res.setHeader('Access-Control-Allow-Origin', '*'); 36 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); 37 | res.setHeader('Access-Control-Allow-Headers', 'Authorization, X-API-KEY, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Allow-Request-Method'); 38 | 39 | next(); 40 | }); 41 | // parse application/x-www-form-urlencoded 42 | app.use(bodyParser.urlencoded({ extended: false })) 43 | 44 | // parse application/json 45 | app.use(bodyParser.json()) 46 | 47 | app.use(express.urlencoded({ 48 | extended: true 49 | })) 50 | 51 | 52 | dotenv.config(); 53 | 54 | const apiKey = process.env.GOOGLE_API_KEY; 55 | const cx = process.env.CX; 56 | const rootDomain = process.env.ROOT_DOMAIN; 57 | 58 | console.log(apiKey, cx, rootDomain); 59 | 60 | app.get('/search', async (req, res) => { 61 | const query = req.query.q || ''; 62 | 63 | // Encode query string to be used in URL 64 | const queryToStr = encodeURIComponent(query.toString()); 65 | 66 | if (!query) { 67 | res.status(400).json({ error: 'No query provided' }); 68 | return; 69 | } 70 | 71 | const url = `https://www.googleapis.com/customsearch/v1?key=${apiKey}&cx=${cx}&q=${queryToStr}`; 72 | 73 | try { 74 | const response = await axios.get(url); 75 | console.log(response.data) 76 | const data = response.data; 77 | const results = data.items || []; 78 | const formattedResults = await processResults(results); 79 | res.json({ results: formattedResults }); 80 | } catch (error) { 81 | console.log(error) 82 | res.status(error?.response?.status).json({ error: 'Error fetching search results' }); 83 | } 84 | }); 85 | 86 | 87 | app.get('/.well-known/:filename', (req, res) => { 88 | const filename = req.params.filename; 89 | res.sendFile(path.join(__dirname, '.well-known', filename)); 90 | }); 91 | 92 | 93 | httpServer.listen(httpPort, () => { 94 | console.log(`HTTP Server running on port ${httpPort}`); 95 | }); 96 | -------------------------------------------------------------------------------- /src/.well-known/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Search and Fetch content 4 | description: Searches Google with the given query, returning search results. 5 | 6 | servers: 7 | - url: https://subdomain.maila.ai 8 | paths: 9 | /search: 10 | get: 11 | operationId: searchGet 12 | summary: Search Google and fetch HTML content 13 | description: Searches Google with the given query, returning search results and inner text from specific chunks array elements, initial 5, 11th-15th, and 21st-25th positions. 14 | parameters: 15 | - name: q 16 | in: query 17 | description: Search query 18 | required: true 19 | schema: 20 | type: string 21 | example: chatgpt 22 | responses: 23 | "200": 24 | description: Successful operation 25 | content: 26 | application/json: 27 | schema: 28 | type: object 29 | properties: 30 | results: 31 | type: array 32 | items: 33 | type: object 34 | properties: 35 | title: 36 | type: string 37 | description: The title of the search result 38 | link: 39 | type: string 40 | format: uri 41 | description: The URL of the search result 42 | chunk_content: 43 | type: string 44 | description: A summary of the HTML content of the search result (available for the first five results) 45 | example: 46 | results: 47 | - title: introducing ChatGPT 48 | link: https://openai.com/blog/chatgpt 49 | chunk_content: ChatGPT and GPT-3.5 were trained on an Azure AI supercomputing infrastructure. ChatGPT sometimes writes plausible-sounding but incorrect or nonsensical answers. 50 | - title: OpenAI 51 | link: https://openai.com 52 | chunk_content: Developing safe and beneficial AI requires people from a wide range of disciplines and backgrounds ... 53 | 54 | "400": 55 | description: Bad request 56 | content: 57 | application/json: 58 | schema: 59 | type: object 60 | properties: 61 | error: 62 | type: string 63 | description: Error message 64 | example: 65 | error: No query provided 66 | "default": 67 | description: Error fetching search results 68 | content: 69 | application/json: 70 | schema: 71 | type: object 72 | properties: 73 | error: 74 | type: string 75 | description: Error message 76 | example: 77 | error: Error fetching search results 78 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | // src/utils.ts 2 | 3 | import axios from 'axios'; 4 | import cheerio from 'cheerio'; 5 | import SearchResult from './search-result'; 6 | 7 | // This function splits the content into chunks of words 8 | function splitContentByWords( 9 | content: string, 10 | chunkSize: number = 2000 11 | ): string[] { 12 | const words = content ? content?.split(/\s+/) : []; 13 | const chunks: string[] = []; 14 | for (let i = 0; i < words.length; i += chunkSize) { 15 | const chunk = words.slice(i, i + chunkSize).join(' '); 16 | chunks.push(chunk); 17 | } 18 | 19 | return chunks; 20 | } 21 | 22 | // This function fetches the content from a given URL 23 | async function fetchContent(url: string): Promise { 24 | try { 25 | const response = await axios.get(url); 26 | if (response.status !== 200) { 27 | return null; 28 | } 29 | 30 | const $ = cheerio.load(response.data); 31 | 32 | let plainTextSummary = ''; 33 | 34 | // Due to limited context window, I selected the most important part of the document. 35 | $('h1').each((_, elem) => { 36 | const text = $(elem).text().trim(); 37 | plainTextSummary += `${text}:\n`; 38 | }); 39 | 40 | $('h2').each((_, elem) => { 41 | const text = $(elem).text().trim(); 42 | plainTextSummary += ` ${text}: `; 43 | }); 44 | 45 | $('p, ul').each((_, elem) => { 46 | const text = $(elem).text().trim() 47 | .replace(/\r?\n|\r/g, ' ') 48 | .replace(/\t+/g, ' ') 49 | .replace(/ {2,}/g, ' '); 50 | plainTextSummary += `${text}\n`; 51 | }); 52 | 53 | // Remove trailing comma and add a newline 54 | plainTextSummary = plainTextSummary.replace(/, $/, '\n'); 55 | 56 | return plainTextSummary || null; 57 | } catch (error) { 58 | console.error(`Error fetching content: ${error}`); 59 | return null; 60 | } 61 | } 62 | 63 | // This function processes the results by fetching the content of each link, splitting it into chunks, and creating a new SearchResult for each chunk 64 | export async function processResults(links: any[]): Promise { 65 | const chunks_: SearchResult[] = []; 66 | 67 | // Use Promise.all to fetch all contents concurrently 68 | const fetchPromises = links.map(async (link_: any) => { 69 | const { title, link } = link_; 70 | const content = await fetchContent(link); 71 | 72 | // If content is null, skip this link 73 | if (!content) { 74 | return; 75 | } 76 | 77 | // Split the content into chunks 78 | const chunks = splitContentByWords(content, 200); 79 | 80 | // For each chunk, create a new SearchResult and add it to the array 81 | chunks.forEach(chunk => { 82 | const searchResult = new SearchResult(title, link, chunk); 83 | console.log(searchResult); 84 | chunks_.push(searchResult); 85 | }); 86 | }); 87 | 88 | await Promise.all(fetchPromises); 89 | 90 | // Return the first 30 results 91 | return chunks_.slice(0, 30); 92 | } 93 | --------------------------------------------------------------------------------