ChatGPT插件平台有望成为影响深远的"下一件大事",因此对于开发者来说,有必要对ChatGPT插件的开发有一定的了解。原文: Writing a ChatGPT Plugin With Go[1]
我工作的附带福利之一是偶尔可以接触试用一些很酷的新技术,最近的一项技术是OpenAI的ChatGPT插件平台。
ChatGPT是一款突破性的AI聊天机器人,无论好坏,它都席卷了世界。在2023年3月,ChatGPT插件和插件平台的发布引发了持续关注(包括我),这可是件大事。
如果你把ChatGPT的发布等同于2007年发布iPhone,那么插件的发布就像是在2008年发布的移动应用和App Store。这不仅是当今最强大、最先进的AI模型开发服务平台,也是一个可以立即触达1亿用户的机会。
对于像我这样的程序员来说,就像猫薄荷之于猫一样,当我拿到访问权限时,立马跳了起来。
ChatGPT插件非常简单,实际上只不过是ChatGPT调用并获取信息的API,可以基于返回信息向用户提供答案。是的,你没看错,ChatGPT插件只是标准API。
那么,怎么实现ChatGPT插件呢?实现ChatGPT插件必须提供2个文件:
-
描述插件的manifest文件(本质上是JSON文件) -
描述插件中API的 OpenAPI [2]规范(YAML文件)
接下来看看我是如何创建新加坡HDB停车场插件的。
停车场插件
我决定从一个非常简单的插件开始。在新加坡,住房发展局(HDB)[3]是负责公共住房的政府机构。公共住房在新加坡是件大事,约有80%的人住在公共住房中,因此HDB停车场占新加坡住宅停车场的大部分。
我想创建一个插件,可以告诉我关于新加坡HDB停车场的使用情况。这些信息都可以在data.gov.sg上获得,这是一个免费数据门户网站,提供新加坡政府的公开数据集和API。
简而言之,插件有两个功能:
-
提供HDB停车场的资料,可以指定地点或指定停车场 -
提供HDB停车场的可用情况,可以指定地点或指定停车场
data.gov.sg门户网站有个针对这些目标很有用的API——HDB停车场使用情况API[4],这是一个实时API。至于HDB停车场信息,有一个定期更新的数据集,称为HDB停车场信息[5]。幸运的是,HDB停车场信息数据集是CKAN action API的前端。
无论如何,现在这两种功能都有API可用。这就完事了,对吧?没那么快。CKAN action API根据日期参数在数据集中返回所有内容,并不完全有用。我不能把所有数据都转到ChatGPT去,数据集是324kb,超过了token限制。然而,解决这个问题相对简单,只需要过滤一下。
此外,我们不能直接公开API。记住,ChatGPT插件需要manifest文件和OpenAI规范。manifest文件还指向像logo和合法页面这样的东西,需要提供具有相同域名和插件URL的电子邮件。所有这一切都意味着我们真的需要开发一个单独的API程序,在将data.gov.sg API发布到ChatGPT之前用来进行处理。
API程序
我用Go语言开发了API程序,这是很自然的选择,Go非常适合API。由于很多AI的东西都用Python,我最近一直在使用Python,所以也比较渴望回到Go。
JSON数据结构
我们从JSON数据结构定义开始。
package main
import "time"
// for car park information
type CarParkData struct {
CarParkInfo []struct {
TotalLots string `json:"total_lots"`
LotType string `json:"lot_type"`
LotsAvailable string `json:"lots_available"`
} `json:"carpark_info"`
CarParkNumber string `json:"carpark_number"`
UpdateDatetime string `json:"update_datetime"`
}
type CarParkAvailability struct {
Items []struct {
Timestamp time.Time `json:"timestamp"`
Data []CarParkData `json:"carpark_data"`
} `json:"items"`
}
// for car park availability
type CarParkRecord struct {
FullCount string `json:"_full_count"`
ShortTermParking string `json:"short_term_parking"`
CarParkType string `json:"car_park_type"`
YCoord string `json:"y_coord"`
XCoord string `json:"x_coord"`
Rank float64 `json:"rank"`
FreeParking string `json:"free_parking"`
GantryHeight string `json:"gantry_height"`
CarParkBasement string `json:"car_park_basement"`
NightParking string `json:"night_parking"`
Address string `json:"address"`
CarParkDecks string `json:"car_park_decks"`
ID int `json:"_id"`
CarParkNo string `json:"car_park_no"`
TypeOfParkingSystem string `json:"type_of_parking_system"`
}
type CarParks struct {
Help string `json:"help"`
Success bool `json:"success"`
Result struct {
ResourceID string `json:"resource_id"`
Fields []struct {
Type string `json:"type"`
ID string `json:"id"`
} `json:"fields"`
Q string `json:"q"`
Records []CarParkRecord `json:"records"`
Links struct {
Start string `json:"start"`
Next string `json:"next"`
} `json:"_links"`
Total int `json:"total"`
} `json:"result"`
}
看起来很复杂,但其实我作弊了,我只是从data.gov.sg API中获取JSON样本和输出,并使用Matt Holt提供的JSON-to-Struct[6]服务,该服务会立即创建相应的结构体,然后只需根据需要对其进行调整。
API
接下来是API程序本身。
package main
import (
"encoding/json"
"io"
"log"
"net/http"
"net/url"
"os"
"time"
)
const infoUrl = "https://data.gov.sg/api/action/datastore_search?\
resource_id=139a3035-e624-4f56-b63f-89ae28d4ae4c&q="
const availUrl = "https://api.data.gov.sg/v1/transport/\
carpark-availability?date_time="
func main() {
port := os.Getenv("PORT")
mux := http.NewServeMux()
mux.Handle("/static/", http.StripPrefix("/static",
http.FileServer(http.Dir("./static"))))
mux.Handle("/.well-known/ai-plugin.json", cors(manifest))
mux.Handle("/openapi.yaml", cors(specs))
mux.Handle("/info", cors(info))
mux.Handle("/availability", cors(availability))
server := &http.Server{
Addr: ":" + port,
Handler: mux,
}
server.ListenAndServe()
}
// middleware to set CORS for handlers
func cors(h http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")
w.Header().Set("Access-Control-Allow-Headers",
"Origin,Content-Type,OpenAI-Conversation-ID,OpenAI-Ephemeral-User-ID")
h.ServeHTTP(w, r)
})
}
// returns openai.yaml file
func specs(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "openapi.yaml")
}
// returns .well-known/ai-plugin.json file
func manifest(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, ".well-known/ai-plugin.json")
}
// handler function
// get information on car park
func info(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
records := getCarParkRecords(query)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(records)
}
// handler function
// get availability on car park
func availability(w http.ResponseWriter, r *http.Request) {
// get the car parks requested
query := r.URL.Query().Get("q")
records := getCarParkRecords(query)
// get the availability of car parks in Singapore now
now := time.Now()
dt := now.Format(time.RFC3339)
availability := new(CarParkAvailability)
carparkdata := []CarParkData{}
resp, err := http.Get(availUrl + url.QueryEscape(dt))
if err != nil {
log.Println("Cannot call url", availUrl+dt, err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
log.Println("Cannot read JSON file", err)
}
err = json.Unmarshal(data, availability)
if err != nil {
log.Println("Error unmarshalling json data:", err)
}
// filter off those that we want to look for
for _, item := range availability.Items {
for _, data := range item.Data {
for _, record := range records {
if data.CarParkNumber == record.CarParkNo {
carparkdata = append(carparkdata, data)
}
}
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(carparkdata)
}
// get card park records based on a query
func getCarParkRecords(query string) []CarParkRecord {
carparks := new(CarParks)
resp, err := http.Get(infoUrl + query)
if err != nil {
log.Println("Cannot call url", infoUrl+query, err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
log.Println("Cannot read JSON file", err)
}
err = json.Unmarshal(data, carparks)
if err != nil {
log.Println("Error unmarshalling json data:", err)
}
return carparks.Result.Records
}
这就是整个API程序,大约120行左右代码。我们来分析一下。
Manifest文件
之前提到有两个文件定义ChatGPT插件,一个是manifest文件,需要放在运行API程序的服务器上的.well-known/ai-plugin.json
路径下。
为此,我提供了一个JSON文件。如果不需要身份验证,则格式相对简单。
{
"schema_version": "v1",
"name_for_human": "HDB Car Park",
"name_for_model": "hdbcarpark",
"description_for_human": "For checking availability of car park \
lots at various HDB car parks around Singapore.",
"description_for_model": "For checking availability of car park \
lots at various HDB car parks around Singapore.",
"auth": {
"type": "none"
},
"api": {
"type": "openapi",
"url": "https://carpark.sausheong.com/openapi.yaml",
"is_user_authenticated": false
},
"logo_url": "https://carpark.sausheong.com/static/logo.png",
"contact_email": "sausheong@sausheong.com",
"legal_info_url": "https://carpark.sausheong.com/static/legal.html"
}
OpenAPI规范
第二个是API的OpenAPI规范。和前面一样,我提供了openapi.yaml
文件,文件位置在上面的manifest文件中指定。
openapi: 3.0.1
info:
title: HDB Car Park
description: For checking avaiability of car park lots at various HDB \
car parks around Singapore
version: 'v1'
servers:
- url: https://carpark.sausheong.com
paths:
/info?q={location}:
get:
operationId: getCarParkInfo
summary: Get information about HDB car parks at a location
parameters:
- in: path
name: location
schema:
type: string
required: true
description: location to search for HDB car parks
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/getCarParkInfo'
/availability?q={location}:
get:
operationId: getCarParkAvailability
summary: Get availability of HDB car parks in Singapore at this moment
parameters:
- in: path
name: location
schema:
type: string
required: true
description: location to search for HDB car parks
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/getCarParkAvailability'
components:
schemas:
getCarParkInfo:
type: array
items:
type: object
properties:
_full_count:
type: string
short_term_parking:
type: string
car_park_type:
type: string
y_coord:
type: string
x_coord:
type: string
rank:
type: number
free_parking:
type: string
gantry_height:
type: string
car_park_basement:
type: string
night_parking:
type: string
address:
type: string
car_park_decks:
type: string
_id:
type: integer
format: int
car_park_no:
type: string
type_of_parking_system:
type: string
getCarParkAvailability:
type: object
properties:
timestamp:
type: string
format: date-time
carpark_data:
type: array
items:
type: object
properties:
carpark_info:
type: array
items:
type: object
properties:
total_lots:
type: string
lot_type:
type: string
lots_available:
type: string
carpark_number:
type: string
update_datetime:
type: string
该文件定义了两个路径,/info
和/availability
,用于提供对应的两个功能。这两个路径还可以接受查询参数location
,用于过滤位置。
接下来看看具体功能。
停车场信息
getCarParkRecords
函数直接向data.gov.sg API发送GET请求,并返回CarParkRecord
结构的切片。
// get card park records based on a query
func getCarParkRecords(query string) []CarParkRecord {
carparks := new(CarParks)
resp, err := http.Get(infoUrl + query)
if err != nil {
log.Println("Cannot call url", infoUrl+query, err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
log.Println("Cannot read JSON file", err)
}
err = json.Unmarshal(data, carparks)
if err != nil {
log.Println("Error unmarshalling json data:", err)
}
return carparks.Result.Records
}
info
处理程序使用getCarParkRecords
来获取记录,并作为JSON响应返回。
// get information on car park
func info(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
records := getCarParkRecords(query)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(records)
}
停车场使用情况
检查停车场的使用情况稍微有点棘手。availability
处理程序首先调用getCarParkRecords
来获取感兴趣的停车场。
然后调用data.gov.sg CKAN API来获取新加坡所有HDB停车场的当前使用情况,并放入CarParkAvailability
结构体中。
// get availability on car park
func availability(w http.ResponseWriter, r *http.Request) {
// get the car parks requested
query := r.URL.Query().Get("q")
records := getCarParkRecords(query[:len(query)-1])
// get the availability of car parks in Singapore now
now := time.Now()
dt := now.Format(time.RFC3339)
availability := new(CarParkAvailability)
carparkdata := []CarParkData{}
resp, err := http.Get(availUrl + url.QueryEscape(dt))
if err != nil {
log.Println("Cannot call url", availUrl+dt, err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
log.Println("Cannot read JSON file", err)
}
err = json.Unmarshal(data, availability)
if err != nil {
log.Println("Error unmarshalling json data:", err)
}
// filter off those that we want to look for
for _, item := range availability.Items {
for _, data := range item.Data {
for _, record := range records {
if data.CarParkNumber == record.CarParkNo {
carparkdata = append(carparkdata, data)
}
}
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(carparkdata)
}
然后遍历CarParkAvailability
和CarParkRecords
中的停车场数据,并匹配停车场编号以过滤CarParkAvailability
。最后,以JSON格式返回停车场使用数据。
启用CORS
需要注意,要为API程序启用CORS(跨域资源共享,Cross-Origin Resource Sharing),从而允许ChatGPT调用API。我将其实现为每个处理程序的HTTP中间件。
// middleware to set CORS for handlers
func cors(h http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "https://chat.openai.com")
w.Header().Set("Access-Control-Allow-Methods", "GET")
w.Header().Set("Access-Control-Allow-Headers",
"Origin,Content-Type,OpenAI-Conversation-ID,OpenAI-Ephemeral-User-ID")
h.ServeHTTP(w, r)
})
}
就这么多!
部署插件
ChatGPT允许我们安装运行在计算机上的插件,也就是说,可以在本地主机上运行,这对于测试和调试非常有用。同时也意味着需要将manifest文件和OpenAPI规范指向localhost。
一旦对测试结果感到满意,就可以将其部署到互联网上,让最多15个用户进行测试。部署插件类似于部署任何API,在此不再详述,除了需要为服务提供适当的域名,并且API域名需要与manifest文件中的联系电子邮件地址相匹配,还需要通过HTTPS运行。
一旦部署了API,就可以将其作为插件安装在ChatGPT上。
为什么这么繁琐?
如果你认为对于API来说太过于繁琐,只要看看当我在ChatGPT上问更复杂的问题时,这个插件是如何回应的。请记住,插件不只是向用户提供数据,也为当今最先进的AI模型之一提供数据,以用于回答问题。
我试着比较多层停车场和地面停车场,这很有效,感觉我是在问一个拥有所有数据并能够为我分析数据的人。
问: 在淡滨尼,多层停车场是否比地面停车场有更多可用车位?
ChatGPT: 根据数据,这里是淡滨尼多层停车场和地面停车场的比较:
多层停车场:
总停车位: 26,486
空停车位: 14,070
空停车位占比: 53.1%
地面停车场:
总停车位: 26,486
空停车位: 198
空停车位占比: 18.6%
因此,与地面停车场相比,淡滨尼的多层停车场的可用车位比例更高。
接下来,我试着问哪个停车场车位最多,离这里有多远。
*问: 淡滨尼哪个停车场的车位最多,地址是什么? 离淡滨尼购物中心有多远?
ChatGPT: 淡滨尼可用车位最多的停车场位于NT5,共有1489个车位,其中有812个空车位。
此停车场地址为"淡滨尼中环7号510C座/淡滨尼广场"。
至于到淡滨尼购物中心的距离,我需要计算一下,请稍等。
"NT5"停车场位于"淡滨尼中环7号510C座/淡滨尼广场",距离淡滨尼购物中心约500米,步行大约6分钟。请注意,实际时间可能会根据路线和步行速度而有所不同。
这就很有意思了,当ChatGPT没有数据时,就会胡编乱造。它不知道停车场离购物中心有多远,所以就编了一个数字,猜测人们从停车场走到购物中心需要多长时间。实际距离为1公里,步行时间约为12分钟(谷歌地图)。
我还试着让它比较新加坡两个不同地区的停车场使用情况,也很有效。
问: 淡滨尼的空停车位与宏茂桥相比,占停车场容量的百分比如何?
ChatGPT: 以下是淡滨尼和宏茂桥的停车场可用情况:
淡滨尼:
总停车位: 13,739
空停车位: 4,751
空停车位占比: 34.6%
宏茂桥:
总停车位: 30,544
空停车位: 9,380
空停车位占比: 30.7%
请注意,这些数字是实时更新的,随时可能会发生变化。
总结
编写ChatGPT插件非常简单,并且是一个强大的工具。编写插件的技能与目前技能没什么不同,不像刚开始开发移动应用时需要更多专业技能。很容易看出,当它正式发布时,很可能会有一个插件热潮。更令人兴奋的是,插件可以相互协作以提供协同功能。
ChatGPT仍然存在问题,当数据不可用时,会产生幻觉现象。然而,从长远来看,这是可以解决的。我认为一个潜力巨大的AI市场正在开始。
代码
可以从下面Git仓库获得所有代码。
ChatGPT plugin for Singapore HDB car park availability[7]
你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!
参考资料
Writing a ChatGPT Plugin With Go: https://betterprogramming.pub/writing-a-chatgpt-plugin-with-go-c5a70764915d
[2]OpenAPI: https://www.openapis.org
[3]住房发展局(HDB): https://www.hdb.gov.sg/cs/infoweb/homepage
[4]HDB停车场使用情况API: https://data.gov.sg/dataset/carpark-availability
[5]HDB停车场信息: https://data.gov.sg/dataset/hdb-carpark-information
[6]JSON-to-Struct: https://mholt.github.io/json-to-go
[7]ChatGPT plugin for Singapore HDB car park availability: https://github.com/sausheong/carpark-cgpt/tree/main
本文由 mdnice 多平台发布