用 Go 编写 ChatGPT 插件

简介: 用 Go 编写 ChatGPT 插件

ChatGPT 插件平台有望成为影响深远的"下一件大事",因此对于开发者来说,有必要对 ChatGPT 插件的开发有一定的了解。原文: Writing a ChatGPT Plugin With Go



我工作的附带福利之一是偶尔可以接触试用一些很酷的新技术,最近的一项技术是 OpenAI 的 ChatGPT 插件平台。


ChatGPT 是一款突破性的 AI 聊天机器人,无论好坏,它都席卷了世界。在 2023 年 3 月,ChatGPT 插件和插件平台的发布引发了持续关注(包括我),这可是件大事。


如果你把 ChatGPT 的发布等同于 2007 年发布 iPhone,那么插件的发布就像是在 2008 年发布的移动应用和 App Store。这不仅是当今最强大、最先进的 AI 模型开发服务平台,也是一个可以立即触达 1 亿用户的机会。


对于像我这样的程序员来说,就像猫薄荷之于猫一样,当我拿到访问权限时,立马跳了起来。


ChatGPT 插件非常简单,实际上只不过是 ChatGPT 调用并获取信息的 API,可以基于返回信息向用户提供答案。是的,你没看错,ChatGPT 插件只是标准 API。


那么,怎么实现 ChatGPT 插件呢?实现 ChatGPT 插件必须提供 2 个文件:


  1. 描述插件的 manifest 文件(本质上是 JSON 文件)
  2. 描述插件中 API 的OpenAPI规范(YAML 文件)


接下来看看我是如何创建新加坡 HDB 停车场插件的。


停车场插件


我决定从一个非常简单的插件开始。在新加坡,住房发展局(HDB)是负责公共住房的政府机构。公共住房在新加坡是件大事,约有 80%的人住在公共住房中,因此 HDB 停车场占新加坡住宅停车场的大部分。


我想创建一个插件,可以告诉我关于新加坡 HDB 停车场的使用情况。这些信息都可以在 data.gov.sg 上获得,这是一个免费数据门户网站,提供新加坡政府的公开数据集和 API。


简而言之,插件有两个功能:


  1. 提供 HDB 停车场的资料,可以指定地点或指定停车场
  2. 提供 HDB 停车场的可用情况,可以指定地点或指定停车场


data.gov.sg 门户网站有个针对这些目标很有用的 API——HDB停车场使用情况API,这是一个实时 API。至于 HDB 停车场信息,有一个定期更新的数据集,称为HDB停车场信息。幸运的是,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服务,该服务会立即创建相应的结构体,然后只需根据需要对其进行调整。

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)
}

复制代码


然后遍历CarParkAvailabilityCarParkRecords中的停车场数据,并匹配停车场编号以过滤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




你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。微信公众号:DeepNoMind

目录
相关文章
|
3月前
|
机器学习/深度学习 自然语言处理 数据可视化
如何在 ChatGPT 上使用 Wolfram 插件回答数学问题
如何在 ChatGPT 上使用 Wolfram 插件回答数学问题
31 0
|
4月前
|
网络架构
flutter推荐路由器插件:go_router
flutter推荐路由器插件:go_router
55 0
|
4月前
|
Go 开发者
Go语言插件开发:Pingo库实践
Go语言插件开发:Pingo库实践
29 0
|
12月前
|
人工智能 数据可视化 数据挖掘
重磅:ChatGPT提供第三方插件ChatGPT plugins,每个人、每个公司都可用上大模型
重磅:ChatGPT提供第三方插件ChatGPT plugins,每个人、每个公司都可用上大模型
1006 0
|
8月前
|
SQL 机器学习/深度学习 Java
8款最新宝藏IDEA插件「含ChatGPT」,不容错过,赶紧试试吧
工欲善其事必先利其器,Intellij IDEA是一款十分流行的编程工具,不仅提供了非常强大的基础功能,还构建了一套插件市场生态,让用户可以根据自己需要安装插件,提升开发效率,这里介绍几款好用的插件,感兴趣的小伙伴赶紧试试吧!
|
8月前
|
人工智能 自然语言处理 Java
当代码遇见AI:IDEA开启ChatGPT插件,分分钟成为高效程序猿!
当代码遇见AI:IntelliJ IDEA开启ChatGPT插件,分分钟成为高效程序猿!
1329 0
|
8月前
CHATGPT最强插件
CHATGPT最强插件
|
10月前
|
Go C++
VS Code安装go 插件报错处理
在命令行中设置如下go 变量,重启vscode 即可
84 0
|
10月前
|
Web App开发 数据采集 人工智能
一招解决ChatGPT对话经常中断问题:KeepChatGPT插件
一招解决ChatGPT对话经常中断问题:KeepChatGPT插件
167 0
一招解决ChatGPT对话经常中断问题:KeepChatGPT插件
|
11月前
|
人工智能 安全 机器人
ChatGPT Plus的插件是如何工作的?
ChatGPT Plus初体验,以及插件是如何工作的?
94 0
ChatGPT  Plus的插件是如何工作的?