在本教程中,我们将制作一个简单的chatgpt插件来生成QR码。
开始之前,您应该熟悉Python和Fastapi。如果您不熟悉它们,则可以在此处学习基础知识:Python Tutorial和FastAPI tutorial。此外,需要开发人员访问CHATGPT。如果您还没有访问权限,则可以加入waitlist。
您可以找到此插件here的最终源代码。
chatgpt插件的工作方式
OpenAI插件可让您通过与开发人员定义的API进行互动以执行各种操作,将Chatgpt连接到第三方应用程序。
给出了有关如何使用API的说明,Chatgpt智能地调用API后端来执行诸如在搜索目录中查找产品,从知识库中检索信息,获取实时信息并基本上分析信息等动作。您在API中定义的任何内容都可以与Chatgpt集成。
要制作一个chatgpt插件,您需要三件事:
- API后端:我们正在使用FastApi进行本教程
- 清单文件:包含有关如何与插件互动的getgpt的元数据和说明
- OpenAPI架构:指定API后端所有终点的信息,包括对ChatGpt可以理解的终点和请求的自然语言描述
名为ai-plugin.json
的清单文件包含有关插件的基本信息,例如名称,描述,API后端的URL,OpenAPI架构文件的URL,身份验证信息,联系信息,联系信息等。
我们将使用ChatGPT QR Code Plugin Starter Template将其用于插件,因此我们可以主要专注于插件开发而不是后端开发。因此,让我们深入研究!
建立开发环境
让我们快速建立我们的开发环境以启动和运行。
我们将使用Poetry管理依赖关系,因此,如果您还没有,请安装它:
pip install poetry
克隆启动器模板并导航到项目目录:
git clone -b starter-template --single-branch https://github.com/mmz-001/chatgpt-qr-code-plugin
cd chatgpt-qr-code-plugin
注意:启动模板在
starter-template
分支中,而不是main
分支
创建和激活虚拟环境并安装依赖项:
poetry shell
poetry install
本地运行API后端:
poetry run start
现在,应在http://localhost:8000
上访问API后端。您可以在http://localhost:8000/docs
上查看交互式文档(现在为空)。另外,OpenAPI模式文件位于http://localhost:8000/openapi.json
。我们稍后需要。
在下一部分中,让我们通过启动器模板并探索其内容。
探索入门模板
您将在“入门模板”中看到几个文件和文件夹。让我们逐一浏览最重要的文件。
ai_plugin.json
这是我们插件的清单文件。它包含有关如何与我们的插件交互的getgpt的元数据和说明。
{
"schema_version": "v1",
"name_for_human": "QR Code",
"name_for_model": "qr_code",
"description_for_human": "Generate QR codes.",
"description_for_model": "Generate QR codes. The generated QR code link should be displayed as a markdown image.",
"auth": {
"type": "none"
},
"api": {
"type": "openapi",
"url": "$host/openapi.json",
"is_user_authenticated": false
},
"logo_url": "$host/.well-known/logo.png",
"contact_email": "support@example.com",
"legal_info_url": "http://www.example.com/legal"
}
让我们浏览最重要的领域。
-
schema_version
:清单模式的版本。目前,仅支持v1
。 -
name_for_human
:向用户显示的插件的名称。 -
name_for_model
:模型用来参考插件的名称。不允许空间,只有字母,数字和下划线 -
description_for_human
:向用户显示的插件的描述。 -
description_for_model
:显示在模型中显示的插件的描述,以提供有关如何以及何时使用插件的说明。 -
auth
:插件的身份验证信息。 Auth类型设置为none
,因为我们不需要此简单插件的身份验证。我将在以后的教程中谈论身份验证。 -
api
:有关API后端的信息。我们将用API后端的URL动态替换$host
。
其他领域几乎是自我解释的。您可以找到有关清单文件here的更多信息。
routers/well-known.py
我们已经在此文件中定义了一个路由器,该路由器将请求处理到/.well-known
端点,以服务我们的徽标和ai-plugin.json
清单文件。 Chatgpt在/.well-known/ai-plugin.json
上查找清单文件。因此,如果您的插件托管在http://example.com
上,Chatgpt在http://example.com/.well-known/ai-plugin.json
上查找清单文件。由于我们在本地运行插件,因此在http://localhost:8000/.well-known/ai-plugin.json
上提供了清单文件。
import json
from string import Template
from fastapi import APIRouter, Request
from fastapi.responses import FileResponse, Response
well_known = APIRouter(prefix="/.well-known", tags=["well-known"])
def get_host(request: Request):
host_header = request.headers.get("X-Forwarded-Host") or request.headers.get("Host")
protocol = request.headers.get("X-Forwarded-Proto") or request.url.scheme
return f"{protocol}://{host_header}"
def get_ai_plugin():
with open("ai-plugin.json", encoding="utf-8") as file:
return json.loads(file.read())
@well_known.get("/logo.png", include_in_schema=False)
async def logo():
return FileResponse("logo.png", media_type="image/png")
@well_known.get("/ai-plugin.json", include_in_schema=False)
async def manifest(request: Request):
ai_plugin = get_ai_plugin()
return Response(
content=Template(json.dumps(ai_plugin)).substitute(host=get_host(request)),
media_type="application/json",
)
请注意,我们正在用API后端的URL动态替换清单文件中的$host
字符串。
services/qr.py
在此文件中,我们将使用qrcode库为QR代码生成定义实用程序功能。
import qrcode
from io import BytesIO
def generate_qr_code_from_string(string: str) -> BytesIO:
"""Generate a QR code from a string and return a BytesIO object"""
img = qrcode.make(string)
img_buffer = BytesIO()
img.save(img_buffer, format="PNG")
return img_buffer
server/main.py
我们API后端最重要的部分在这里。 start()
函数启动服务器并在http://localhost:8000
上启动API后端。
from fastapi import FastAPI, Request, Query
from pydantic import BaseModel, Field
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from routers.well_known import well_known, get_ai_plugin, get_host
from hashlib import md5
from io import BytesIO
from services.qr import generate_qr_code_from_string
ai_plugin = get_ai_plugin()
app = FastAPI(
title=ai_plugin["name_for_human"],
description=ai_plugin["description_for_human"],
version="0.1.0",
)
app.include_router(well_known)
# We'll add our API endpoints here
def start():
import uvicorn
uvicorn.run("server.main:app", host="localhost", port=8000, reload=True)
我已经在pyproject.toml
中定义了一个诗歌脚本,可以轻松使用shell命令poetry run start
运行服务器。
现在,启动服务器,然后转到http://localhost:8000/docs
查看交互式文档。您会看到尚未定义端点,但是当我们从ai-plugin.json
读取它们时,显示了插件的名称和描述,并将其传递给FastApi构造函数,该构建器生成了OpenAPI架构文件。
要查看OpenAPI架构文件,请转到http://localhost:8000/openapi.json
。您会看到以下JSON文件:
{
"openapi": "3.0.2",
"info": {
"title": "QR Code",
"description": "Generate QR codes.",
"version": "0.1.0"
},
"paths": {}
}
chatgpt使用OpenAPI架构文件检索有关可用API端点的信息。此信息指导了Chatgpt如何与我们的后端互动。 由于OpenAPI架构文件的潜在大小,仅将有关端点的基本信息发送到插件提示。我们将准确地查看插件提示如何稍后生成。
FastApi的惊人之处在于它会自动生成OpenAPI架构文件,您可以使用交互式文档测试API端点(如我们稍后所见)。此外,交互式文档比OpenAPI架构文件更可读,使插件开发变得更加容易。
logo.png
此徽标将在用户打开插件商店时向用户显示。
创建API端点
我们的简单插件只能做一件事:从文本生成QR码。下图显示了我们插件的体系结构。
当用户要求chatgpt生成QR码时,Chatgpt调用API的/generate
端点传递的字符串将要编码为参数。端点返回到生成的QR代码映像的链接,ChatGpt将QR代码图像显示为Markdown Image。
所有生成的图像都存储在简单的Python词典中。字典键是图像的哈希,值是图像数据本身,以png格式表示为字节。我要保持简单,并将图像存储在内存中,因为这只是一个介绍性教程。在现实世界中,您需要将图像存储在合适的数据库中。
现在,让我们将API端点添加到server/main.py
。
_IMAGE_CACHE: dict[str, BytesIO] = dict()
class GenerateQRCodeRequest(BaseModel):
string: str
@app.get("/image/{img_hash}.png")
def get_image(img_hash: str):
img_bytes = _IMAGE_CACHE[img_hash]
img_bytes.seek(0) # Reset the buffer to the beginning
return StreamingResponse(img_bytes, media_type="image/png")
@app.post("/generate")
def generate_qr_code(data: GenerateQRCodeRequest, request: Request):
img_bytes = generate_qr_code_from_string(data.string)
img_hash = md5(img_bytes.getvalue()).hexdigest()
_IMAGE_CACHE[img_hash] = img_bytes
return {"link": f"{get_host(request)}/image/{img_hash}.png"}
请注意,/image/{img_hash}.png
端点从缓存中获取图像并将其返回为PNG图像。当Chatgpt显示图像时,它将使用此端点获取图像。
用Swaggerui测试API端点
Fastapi的最有用的功能之一是自动openapi架构生成带有用于手动测试的交互式文档。
记住,您可以在http://localhost:8000/docs
上查看交互式文档。
单击“尝试输出”按钮,然后输入在QR码中编码的字符串。然后单击“执行”按钮。您会看到API端点的响应。
现在,上面的URL将指向API端点生成的图像。
好!我们的API端点正在按预期工作,我们可以继续下一步。
在chatgpt中本地运行插件
在我们跳入chatgpt安装插件之前,我们只需要再做一件事:使我们的API后端可访问ChatGpt。我们可以使用Fastapi的CORSMiddleware
中间件轻松地做到这一点。在定义app
对象之后,将以下行添加到server/main.py
。
app.add_middleware(
CORSMiddleware,
allow_origins=["https://chat.openai.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
现在,要安装插件,请导航到插件存储,然后选择“开发自己的插件”。然后输入localhost:8000
作为域并继续。
注意:确保后端在继续之前运行。
验证了清单文件和OpenAPI规格后,安装插件。
开始一个新的聊天会话,并启用QR码插件。现在,让我们通过生成QR码来查看我们的插件。
您可以看到,插件从我们输入的字符串中生成了QR码。您可以通过单击插件名称旁边的向下箭头查看请求和响应。
有趣的是,Chatgpt使用了/image/{img_hash}.png
端点,该端点将图像本身返回为PNG图像。但是Chatgpt不应该将请求直接发送到此端点。它应该使用第一个请求提供的链接将图像显示为Markdown映像。
问题在于,OpenAPI规格中的信息并未完全描述Chatgpt应如何与API后端进行交互。我们需要为路由和请求添加几个描述,以告诉chatgpt如何与API后端进行交互。另外,我们需要从OpenAPI规格隐藏/image/{img_hash}.png
端点,因为这不会由Chatgpt直接使用。
在终点和请求中添加自然语言描述
当用户进行查询时,chatgpt查看OpenAPI规范中端点的描述以及清单文件中的description_for_model
字段,以找出如何与API后端进行交互。
正如我们前面讨论的那样,由于上下文大小的限制,整个OpenAPI规格未传递给插件提示符。 插件提示是从清单文件和OpenAPI规范中的信息生成的类似字样的规范。
让我们探索如何查看生成并传递给chatgpt的插件提示。
使用插件DevTool
在Localhost上开发时,您可以使用开发人员控制台查看清单,OpenAPI Spec和插件提示符(Typecript Spec)。可以通过转到“设置”并切换“打开插件DevTool”来打开插件DevTool。
当前,我们的插件的提示如下:
// Generate QR codes. The generated QR code link should be displayed as a markdown image.
namespace qr_code {
// Get Image
type get_image_image__img_hash__png_get = (_: {
img_hash: string,
}) => any;
// Generate Qr Code
type generate_qr_code_generate_post = (_: {
string: string,
}) => any;
} // namespace qr_code
您可以看到,生成的模式没有提供有关端点的太多信息。架构顶部的第一个注释是从清单文件中的description_for_model
字段中获取的,端点的描述是我们给我们端点的函数名称。由于这是一个简单的API,因此Chatgpt可以弄清楚如何使用这些端点,但是对于更复杂的API,我们需要为端点和请求类型提供详细的描述和有意义的名称。
FastApi的一个非常有用的功能是,它提供了各种方式将描述添加到OpenAPI规范中,因此您无需手动添加它们。
让我们看看如何在下一部分中使用FastAPI添加这些描述。
注意:当您更改OpenAPI架构文件或
ai-plugin.json
清单文件时,您需要从插件DevTools刷新插件,然后开始一个新的聊天会话以进行更改以生效。
使用DOCSTRINGS将描述添加到端点
作为描述字段,将自动添加到OpenAPI规格中的端点的Docstrings。让我们用Docstrings添加描述。
@app.get("/image/{img_hash}.png")
def get_image(img_hash: str):
"""Get an image from the cache."""
img_bytes = _IMAGE_CACHE[img_hash]
img_bytes.seek(0) # Reset the buffer to the beginning
return StreamingResponse(img_bytes, media_type="image/png")
@app.post("/generate")
def generate_qr_code(data: GenerateQRCodeRequest, request: Request):
"""Generate a QR code from a string and return a
link to the image."""
img_bytes = generate_qr_code_from_string(data.string)
img_hash = md5(img_bytes.getvalue()).hexdigest()
_IMAGE_CACHE[img_hash] = img_bytes
return {"link": f"{get_host(request)}/image/{img_hash}.png"}
现在,如果您查看交互式文档,您会看到描述已添加到端点。
请注意,交互式文档是由OpenAPI Spec生成的。因此,如果您转到http://localhost:8000/openapi.json
,您会在端点的描述下看到同样的东西。
我们将使用交互式文档在OpenAPI规范中查看信息,因为它更可读。
刷新插件后,插件提示将使用新的描述更新。
// Generate QR codes. The generated QR code link should be displayed as a markdown image.
namespace qr_code {
// Get an image from the cache.
type get_image_image__img_hash__png_get = (_: {
img_hash: string,
}) => any;
// Generate a QR code from a string and return a
// link to the image.
type generate_qr_code_generate_post = (_: {
string: string,
}) => any;
} // namespace qr_code
使用Pydantic模型向请求添加描述
由于这是一个简单的QR代码应用程序,因此我们的大多数请求类型都是自称的。但是在现实世界中,您要在请求中添加描述,以便chatgpt可以理解它们。
让我们在GenerateQRCodeRequest
模型的string
字段中添加描述。
class GenerateQRCodeRequest(BaseModel):
string: str = Field(
...,
description="The string to encode in the QR code",
)
我们可以在交互式文档的“模式”部分中查看添加的描述。
现在,插件提示将显示添加的描述。
// Generate QR codes. The generated QR code link should be displayed as a markdown image.
namespace qr_code {
// Get an image from the cache.
type get_image_image__img_hash__png_get = (_: {
img_hash: string,
}) => any;
// Generate a QR code from a string and return a
// link to the image.
type generate_qr_code_generate_post = (_: {
// The string to encode in the QR code
string: string,
}) => any;
} // namespace qr_code
隐藏端点的端点
chatgpt除了OpenAPI规范中的定义外,对我们的API一无所知。当构建较大的插件或与现有API集成时,您可能需要从OpenAPI规格中隐藏某些端点。
请记住,ChatGpt错误地将请求直接发送到/image/{img_hash}.png
端点,该端点只能用于显示图像。因此,让我们通过将include_in_schema=False
参数添加到端点的装饰器中,将其隐藏在OpenAPI规范中。
@app.get("/image/{img_hash}.png", include_in_schema=False)
def get_image(img_hash: str):
# Endpoint code here
现在,您将不再看到交互式文档中的端点,但是您仍然可以访问它。现在,让我们看看我们的插件是否按预期工作。
太好了!我们的插件非常完美!确保用不同的提示进行几次测试,以确保一切正常工作。
了解Chatgpt的请求机制
当我开发此插件时,我遇到的一个燃烧的问题是Chatgpt是如何向我的API发送请求的?因此,经过一点点实验,我发现了一些有趣的东西。
插件提示是唯一有关我们API的信息来源,如果您注意到,则不包含有关端点或如何传输请求数据的详细信息。当Chatgpt想要发送请求时,它会输出特殊的结构化格式,Chatgpt后端系统将其解释为命令。
例如,如果Chatgpt想要发送请求以创建https://chat.openai.com/
的QR码,则会产生以下给出的结构化输出。该输出由CHATGPT后端系统解析和处理,并将请求发送到我们的API。请注意,我省略了一些用于区分对话响应和结构化响应的特殊令牌。
assistant to=qr_code.generate_qr_code_generate_post
{
"string": "https://chat.openai.com/"
}
在这里,我们拥有name_for_model
,我们在OpenAPI规范中的清单文件和端点的operationId
中定义了name_for_model
。与ChatGpt后端的所有通信通过JSON对象发生,并且响应也是JSON对象。
包起来
就是这样。您已经创建了第一个用于生成QR码的ChatGpt插件。
我们在本教程中使用的启动模板非常适合快速启动和运行,但是在现实世界中,您需要遵循最佳实践,例如对代码库进行模块化,处理身份验证,验证请求,验证请求,处理错误,写作测试等等。
我希望您喜欢这篇文章,如果您有任何疑问,请随时在下面发表评论。
快乐编码!
ð嘿!如果您喜欢我的内容并想表现出一些爱,请随时使用buy me a coffee。每个杯子都可以帮助我为像您这样的令人难以置信的开发人员创建更多有用的内容!