用 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

目录
相关文章
|
7月前
|
机器学习/深度学习 自然语言处理 数据可视化
如何在 ChatGPT 上使用 Wolfram 插件回答数学问题
如何在 ChatGPT 上使用 Wolfram 插件回答数学问题
118 0
|
存储 编译器 Go
结合ChatGPT辅助学习Go语言底层原理
在当今的信息时代,编程语言已经成为了科技领域的基础工具之一。其中,Go语言以其简洁、高效和并发性强等优点,在开发者中赢得了广泛的好评。然而,对于初学者或是想要深入理解Go语言底层原理的人来说,直接阅读和理解其源码可能会面临一定困难。我主要从以下几个方面分享一些使用ChatGPT辅助学习Go语言底层源码的小技巧。
257 0
结合ChatGPT辅助学习Go语言底层原理
|
4月前
|
Go API 微服务
当 go-zero 邂逅 chatgpt...
当 go-zero 邂逅 chatgpt...
|
20天前
|
开发框架 Go 计算机视觉
纯Go语言开发人脸检测、瞳孔/眼睛定位与面部特征检测插件-助力GoFly快速开发框架
开发纯go插件的原因是因为目前 Go 生态系统中几乎所有现有的人脸检测解决方案都是纯粹绑定到一些 C/C++ 库,如 OpenCV 或 dlib,但通过 cgo 调用 C 程序会引入巨大的延迟,并在性能方面产生显著的权衡。此外,在许多情况下,在各种平台上安装 OpenCV 是很麻烦的。使用纯Go开发的插件不仅在开发时方便,在项目部署和项目维护也能省很多时间精力。
|
3月前
|
人工智能 API Python
ChatGPT 插件开发
本教程旨在帮助您掌握ChatGPT API的基本使用方法,包括应用开发、代码分析、插件开发及专属领域模型应用等。通过学习,您将为未来的人工智能应用开发打下坚实基础。教程包含官方文档介绍、环境搭建步骤及Python示例代码,助您快速上手。请注意,API调用需收费,初始提供5美元免费额度。
|
4月前
|
人工智能 自然语言处理 程序员
使用 go-openai 轻松调用 chatGPT:释放无限创造力!
使用 go-openai 轻松调用 chatGPT:释放无限创造力!
|
4月前
|
Go
Go - 如何编写 ProtoBuf 插件 (一) ?
Go - 如何编写 ProtoBuf 插件 (一) ?
49 2
|
4月前
|
Go
Go - 如何编写 ProtoBuf 插件 (三) ?
Go - 如何编写 ProtoBuf 插件 (三) ?
25 1
|
5月前
|
运维 监控 测试技术
Golang质量生态建设问题之接入并使用Go单元测试插件的问题如何解决
Golang质量生态建设问题之接入并使用Go单元测试插件的问题如何解决
|
4月前
|
人工智能 自然语言处理 Linux
免费ChatGPT4o灵办AI可体验浏览器插件
灵办AI就是您所需的最佳助手!我们为您带来了一款多功能AI工具,ChatGPT4o不仅能为您提供精准翻译,还能满足您的对话需求、智能续写、AI搜索、文档阅读、代码生成与修正等多种需求。灵办 AI,真正让工作和学习变得轻松高效!一款多功能智能助手,旨在提升工作和学习效率。它提供实时翻译、对话问答、搜索、写作和网页阅读等服务,支持多种浏览器和操作系统,帮助用户随时获取信息,打破语言障碍,优化内容创作和信息处理。
133 0
下一篇
DataWorks