How to write a plugin for ChatGPT-4?
(image by Midjourney 5.2)

How to write a plugin for ChatGPT-4?

Intro

Creating a plugin for OpenAI #openai ChatGPT-4 can be a rewarding experience, allowing you to extend the capabilities of the #ai model and tailor it to your specific needs. This concise #tutorial will guide you through creating a plugin, using the example of a simple summarisation plugin named "Sumly". It uses a wrapper to the relevant RapidAPI's API to serve the relevant endpoints (from a URL or a prompted text).

We run #ai #ml #development projects, incl. #chatgpt plugins, at artiqode .

No alt text provided for this image


All right, let's look at the step-by-step guide:

Step 1: Build an API

The first step in creating a plugin is to build an API. This API will interface your plugin and the ChatGPT-4 model. In our example, we have a simple #api that provides two endpoints: one for summarising text retrieved from a URL (`/url`), and another for summarising text directly (`/text`).

?The API is defined in #python using the Quart and Quart-CORS libraries. The API server is set to allow requests from `https://chat.openai.com`, which is necessary when running a localhost plugin. The server provides endpoints for getting the URL and text summaries and serves the plugin logo and manifest file.

Here is the Python code for the API:

import json
import requests
import urllib.parse

import quart
import quart_cors
from quart import request


# Note: Setting CORS to allow chat.openapi.com is only required when running a localhost plugin
app = quart_cors.cors(quart.Quart(__name__), allow_origin="https://chat.openai.com")
HOST_URL = "https://localhost:3000"
headers = {
? ? ? ? ? ? "content-type": "application/x-www-form-urlencoded",
? ? ? ? ? ? "X-RapidAPI-Key": ".........................", # put your RapidAPI-Key Here
? ? ? ? ? ? "X-RapidAPI-Host": "textanalysis-text-summarization.p.rapidapi.com"
? ? ? ? }


# Endpoint for summarizing text retrieved from a URL
@app.get("/url")
async def get_url():
? ? query = request.args.get("query")
? ? res = requests.get(
? ? ? ? f"{HOST_URL}/url?search={query}")
? ? body = res.json()
? ? return quart.Response(response=json.dumps(body), headers=headers, status=200)


# Endpoint for summarizing text directly
@app.get("/text")
async def get_text():
? ? query = request.args.get("query")
? ? res = requests.get(
? ? ? ? f"{HOST_URL}/text?search={query}")
? ? body = res.json()
? ? return quart.Response(response=json.dumps(body), headers=headers, status=200)


# Endpoint for serving the plugin logo
@app.get("/logo.png")
async def plugin_logo():
? ? filename = 'summary.png'
? ? return await quart.send_file(filename, mimetype='image/png')


# Endpoint for serving the plugin manifest file
@app.get("/.well-known/ai-plugin.json")
async def plugin_manifest():
? ? host = request.headers['Host']
? ? with open(".well-known/ai-plugin.json") as f:
? ? ? ? text = f.read()
? ? ? ? # This is a trick we do to populate the PLUGIN_HOSTNAME constant in the manifest
? ? ? ? text = text.replace("PLUGIN_HOSTNAME", f"https://{host}")
? ? ? ? return quart.Response(text, mimetype="text/json")


# Endpoint for serving the OpenAPI specification
@app.get("/openapi.yaml")
async def openapi_spec():
? ? host = request.headers['Host']
? ? with open("openapi.yaml") as f:
? ? ? ? text = f.read()
? ? ? ? # This is a trick we do to populate the PLUGIN_HOSTNAME constant in the OpenAPI spec
? ? ? ? text = text.replace("PLUGIN_HOSTNAME", f"https://{host}")
? ? ? ? return quart.Response(text, mimetype="text/yaml")


def main():
? ? app.run(debug=True, host="0.0.0.0", port=5001)




if __name__ == "__main__":
? ? main()        

In this code, we define several endpoints using the `@app.get` decorator provided by the Quart library. Each endpoint corresponds to a specific function in our API. For example, the `/url` endpoint summarises text retrieved from a URL, while the `/text` endpoint is used to summarise text directly.

Step 2: Document the API in the OpenAPI yaml or JSON format

The next step is to document your API using the OpenAPI specification. This specification provides a standard, language-agnostic interface to your API, which allows both humans and computers to understand the capabilities of your service without access to the source code, documentation, or network traffic inspection.

Our example defines the OpenAPI specification in a file named `openapi.yaml`. This file describes the structure of the API, including the available endpoints, the HTTP methods they support, the parameters they accept, and the responses they return.

Here is the OpenAPI specification for our API:

openapi: 3.0.1
info:
? ? title: Sumly
? ? description: Summarises content from a text or a URL.
? ? version: "v1"
servers:
? ? - url: https://localhost:3000
paths:
? ? /url:
? ? ? ? get:
? ? ? ? ? ? operationId: getUrl
? ? ? ? ? ? summary: Summarises text retrieved from the given URL.
? ? ? ? ? ? parameters:
? ? ? ? ? ? ? ? - in: query
? ? ? ? ? ? ? ? ? name: query
? ? ? ? ? ? ? ? ? schema:
? ? ? ? ? ? ? ? ? ? ? type: string
? ? ? ? ? ? ? ? ? description: Summarises text retrieved from the given URL.
? ? ? ? ? ? responses:
? ? ? ? ? ? ? ? "200":
? ? ? ? ? ? ? ? ? ? description: OK
? ? /text:
? ? ? ? get:
? ? ? ? ? ? operationId: getText
? ? ? ? ? ? summary: Summarises text from the content.
? ? ? ? ? ? parameters:
? ? ? ? ? ? ? - in: query
? ? ? ? ? ? ? ? name: search
? ? ? ? ? ? ? ? schema:
? ? ? ? ? ? ? ? ? type: string
? ? ? ? ? ? ? ? description: Summarises text from the content.
? ? ? ? ? ? responses:
? ? ? ? ? ? ? ? "200":
? ? ? ? ? ? ? ? ? ? description: OK        

This specification defines two paths (`/url` and `/text`), each supporting the `GET` HTTP method. Each path has an `operationId`, a `summary`, a list of `parameters`, and a list of `responses`. The `operationId` is a unique identifier for the operation, the `summary` provides a brief description of the operation, the `parameters` describe the input parameters for the operation, and the `responses` describe the possible responses from the operation.

Step 3: Create a JSON Manifest File

?The final step in creating a plugin is to create a JSON manifest file. This file provides metadata about your plugin, including its name, description, authentication type, API specification URL, logo URL, contact email, and legal information URL.

?In our example, the manifest file is named `ai-plugin.json` and is hosted on the API's domain. The manifest file is accessible via the `/.well-known/ai-plugin.json` path on the API's domain, which is required for ChatGPT-4 to connect with your plugin.

Here is the JSON manifest file for our plugin:

{
? ? "schema_version": "v1",
? ? "name_for_human": "Sumly",
? ? "name_for_model": "Sumly",
? ? "description_for_human": "Summarise a text or URL content swiftly.",
? ? "description_for_model": "Summarise a text or URL content swiftly.",
? ? "auth": {
? ? ? ? "type": "none"
? ? },
? ? "api": {
? ? ? ? "type": "openapi",
? ? ? ? "url": "https://localhost:3000/openapi.yaml",
? ? ? ? "is_user_authenticated": false
? ? },
? ? "logo_url": "PLUGIN_HOSTNAME/logo.png",
? ? "contact_email": "....................",
? ? "legal_info_url": "https://example.com/legal"
}        

In this file, we define several properties of our plugin, including its name (`name_for_human` and `name_for_model`), description (`description_for_human` and `description_for_model`), authentication type (`auth`), API specification URL (`api.url`), logo URL (`logo_url`), contact email (`contact_email`), and legal information URL (`legal_info_url`).

Step 4: Host the Plugin

The final step is to host the plugin on a server. In our example, we use a simple HTTP server written in Python. This server handles requests to the plugin's endpoints and forwards them to the appropriate API.

You need to make the plugin files accessible over HTTP when requesting a deployment on ChatGPT (even as a plugin developer version which is visible only to you):

No alt text provided for this image
OpenAI ChatGPT Plugin deployment

Anyways, here is the Python code for the server:

import http.server
import socketserver
import requests
import urllib.parse
from urllib.parse import parse_qs


PORT = 3000


class CORSRequestHandler(http.server.SimpleHTTPRequestHandler):
? ? def end_headers(self):
? ? ? ? self.send_header('Access-Control-Allow-Origin', '*')
? ? ? ? self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
? ? ? ? self.send_header('Access-Control-Allow-Headers', 'X-Requested-With')
? ? ? ? self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate')
? ? ? ? return super().end_headers()


? ? def do_OPTIONS(self):
? ? ? ? self.send_response(200, "ok")
? ? ? ? self.end_headers()


? ? def do_GET(self):
? ? ? ? # Parse request path
? ? ? ? parsed_path = urllib.parse.urlparse(self.path)
? ? ? ? path = parsed_path.path
? ? ? ? query = parse_qs(parsed_path.query)


? ? ? ? # Host URL and headers for external API
? ? ? ? HOST_URL = "https://textanalysis-text-summarization.p.rapidapi.com"


? ? ? ? payload = {}


? ? ? ? headers = {
? ? ? ? ? ? "content-type": "application/x-www-form-urlencoded",
? ? ? ? ? ? "X-RapidAPI-Key": ".........................", # put your RapidAPI-Key Here
? ? ? ? ? ? "X-RapidAPI-Host": "textanalysis-text-summarization.p.rapidapi.com"
? ? ? ? }

? ? ? ? # Forward request to external API if path starts with "/url" or "/text"
? ? ? ? if path.startswith('/url') or path.startswith('/text'):
? ? ? ? ? ? search = query.get("search", [""])[0]? # Get search term or default to empty string
? ? ? ? ? ? url = HOST_URL
? ? ? ? ? ? if path.startswith('/url'):
? ? ? ? ? ? ? ? ? ? url = url + '/text-summarizer-url'
? ? ? ? ? ? ? ? ? ? payload = {
? ? ? ? ? ? ? ? ? ? ? ? "url": search,
? ? ? ? ? ? ? ? ? ? ? ? "sentnum": "5"
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? elif path.startswith('/text'):
? ? ? ? ? ? ? ? ? ? url = url + '/text-summarizer-text'
? ? ? ? ? ? ? ? ? ? payload = {
? ? ? ? ? ? ? ? ? ? ? ? "text": search,
? ? ? ? ? ? ? ? ? ? ? ? "sentnum": "5"
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? response = requests.post(url, data=payload, headers=headers)
? ? ? ? ? ? self.send_response(response.status_code, url)
? ? ? ? ? ? self.send_header('Content-type', 'application/json')
? ? ? ? ? ? self.end_headers()
? ? ? ? ? ? self.wfile.write(response.content)
? ? ? ? else:? # Handle other requests normally
? ? ? ? ? ? super().do_GET()


with socketserver.TCPServer(("", PORT), CORSRequestHandler) as httpd:
? ? print("Serving at port", PORT)
? ? httpd.serve_forever()        

This code defines a custom request handler (`CORSRequestHandler`) that extends the `SimpleHTTPRequestHandler` provided by the `http.server` module. This custom handler adds CORS headers to the responses, allowing the plugin to be accessed from different origins. It also overrides the `do_GET` method to handle GET requests to the plugin's endpoints.


With these steps, you should now have a fully functional plugin for ChatGPT-4. Remember to test your plugin thoroughly to ensure it works as expected.

You can also get the source code from GitHub:

xartous/Sumly: Sumly plugin for ChatGPT (github.com)


Happy coding! ??

要查看或添加评论,请登录

社区洞察

其他会员也浏览了