乐趣区

关于人工智能:用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”,并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的反对和能源,激励我继续写下去,和大家独特成长提高!

本文由 mdnice 多平台公布

退出移动版