Учебник по разработке плагина ChatGPT
В этом уроке мы сделаем простой плагин ChatGPT, который генерирует QR-коды.
Прежде чем начать, вы должны иметь некоторое представление о Python, FastAPI и Poetry. Если вы не знакомы с ними, вы можете изучить основы здесь: Учебник по Python, Учебник по FastAPI и Учебник по Poetry. Также необходим доступ разработчика к ChatGPT. Если у вас еще нет доступа, вы можете присоединиться к списку ожидания. OpenAI пришлет вам электронное письмо, когда вы получите доступ. Вы можете подтвердить свой доступ, зайдя в магазин плагинов ChatGPT и проверив, есть ли у вас опция “Разработать свой собственный плагин”.
Вы можете найти окончательный исходный код этого плагина здесь.
Как работают плагины ChatGPT
Плагины OpenAI позволяют подключить ChatGPT к сторонним приложениям, взаимодействуя с API для выполнения широкого спектра действий.
Получив инструкции по использованию API, ChatGPT интеллектуально вызывает бэкэнд API для выполнения таких действий, как поиск товаров в каталогах, получение информации из базы знаний, получение информации в реальном времени и ее анализ – в общем, все, что вы определили в своем API, может быть интегрировано с ChatGPT.
Для создания плагина ChatGPT вам понадобятся три вещи:
- API Backend: В данном руководстве мы используем FastAPI.
- Manifest File: Содержит метаданные и инструкции для ChatGPT о том, как взаимодействовать с вашим плагином
- OpenAPI Schema: Определяет информацию обо всех конечных точках вашего API бэкенда, включая описания конечных точек и запросов на естественном языке, которые может понять ChatGPT.
Файл манифеста, названный ai-plugin.json, содержит важную информацию о плагине, такую как название, описание, URL бэкенда API, URL файла схемы OpenAPI, информация об аутентификации, контактная информация и т. д.
Мы будем использовать шаблон 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, а не в основной ветке
Создайте и активируйте виртуальную среду и установите зависимости:
poetry shell
poetry install
Запустите бэкэнд API локально:
poetry run start
Теперь бэкэнд API должен быть доступен по адресу http://localhost:8000. Вы можете просмотреть интерактивную документацию по адресу http://localhost:8000/docs (который сейчас пуст). Кроме того, файл схемы OpenAPI находится по адресу http://localhost:8000/openapi.json. Он понадобится нам позже.
В следующем разделе мы рассмотрим стартовый шаблон и изучим его содержимое.
Изучение стартового шаблона
В стартовом шаблоне вы увидите несколько файлов и папок. Давайте пройдемся по наиболее важным файлам по очереди.
ai_plugin.json
Это файл манифеста для нашего плагина. Он содержит метаданные и инструкции для ChatGPT о том, как взаимодействовать с нашим плагином.
{
"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. Мы динамически заменим $host на URL нашего API-бэкенда.
Остальные поля практически не требуют пояснений.
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",
)
Обратите внимание, что мы динамически заменяем строку $host в файле манифеста на URL нашего бэкенда API.
services/qr.py
В этом файле мы определим служебные функции для генерации QR-кодов с помощью библиотеки qrcode.
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() инициирует сервер и запускает API-бэкенд по адресу http://localhost:8000.
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)
Я определил сценарий Poetry в файле pyproject.toml, чтобы легко запустить сервер с помощью команды 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, что делает разработку плагина намного, намного проще.
логотип.png
Этот логотип будет показан пользователям, когда они откроют магазин плагинов.
Создание конечных точек API
Наш простой плагин делает только одну вещь: генерирует QR-коды из текста. На диаграмме ниже показана архитектура нашего плагина.
Когда пользователь просит ChatGPT сгенерировать QR-код, ChatGPT вызывает конечную точку /generate нашего API, передавая в качестве параметра строку, которую нужно закодировать. Конечная точка возвращает ссылку на сгенерированное изображение QR-кода, и ChatGPT отображает изображение QR-кода пользователю как картинку в формате markdown.
Все сгенерированные изображения хранятся в простом словаре 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 отображает изображение, он будет использовать эту конечную точку для получения изображения.
Тестирование конечных точек API с помощью SwaggerUI
Одной из наиболее полезных функций FastAPI является автоматическая генерация схемы OpenAPI с интерактивной документацией для ручного тестирования.
Помните, что интерактивную документацию можно посмотреть на сайте http://localhost:8000/docs.
Нажмите кнопку “Попробовать” и введите строку, которая будет закодирована в QR-коде. Затем нажмите кнопку “Выполнить”. Вы увидите ответ от конечной точки API.
Теперь вышеуказанный URL будет указывать на изображение, сгенерированное конечной точкой API.
Отлично! Наши конечные точки API работают так, как ожидалось, мы можем перейти к следующему шагу.
Запуск плагина локально в ChatGPT
Прежде чем мы перейдем в ChatGPT для установки плагина, нам нужно сделать еще одну вещь: сделать наш API-бэкенд доступным для ChatGPT. Мы можем легко сделать это с помощью промежуточного ПО CORSMiddleware от FastAPI. Добавьте следующие строки в server/main.py сразу после определения объекта app.
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. Нам нужно добавить пару описаний для маршрутов и запросов в спецификацию OpenAPI, чтобы указать ChatGPT, как взаимодействовать с бэкендом API. Также нам нужно скрыть конечную точку /image/{img_hash}.png из спецификации OpenAPI, поскольку она не будет использоваться непосредственно ChatGPT.
Добавление описаний на естественном языке к конечным точкам и запросам
Когда пользователь делает запрос, ChatGPT просматривает описания конечных точек в спецификации OpenAPI вместе с полем description_for_model в файле манифеста, чтобы понять, как взаимодействовать с бэкендом API.
Как мы обсуждали ранее, вся спецификация OpenAPI не передается в подсказку плагина из-за ограничений на размер контекста. Подсказка плагина – это TypeScript-подобная спецификация, созданная на основе информации из файла манифеста и спецификации OpenAPI.
Давайте рассмотрим, как просмотреть подсказку плагина, которая генерируется и передается в ChatGPT.
Использование плагина devtool
Вы можете использовать консоль разработчика для просмотра манифеста, спецификации OpenAPI и запроса плагина (спецификация TypeScript) при разработке на localhost. Консоль разработчика можно открыть, перейдя в “Настройки” и выбрав пункт “Открыть консоль разработчика”.
В настоящее время подсказка для нашего плагина выглядит следующим образом:
// 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, вам нужно будет обновить плагин из plugin devtools и начать новую сессию чата, чтобы изменения вступили в силу.
Добавление описаний к конечным точкам с помощью docstrings
Докстринги конечных точек автоматически добавляются в спецификацию OpenAPI в качестве поля описания. Давайте добавим описания к конечным точкам с помощью 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. Поэтому если вы перейдете на сайт 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.
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
Скрытие конечных точек из спецификации OpenAPI
ChatGPT не знает ничего о нашем API, кроме того, что определено в спецификации OpenAPI. При создании больших плагинов или интеграции с существующими API вы можете захотеть скрыть некоторые конечные точки от спецификации OpenAPI.
Помните, что ChatGPT ошибочно отправил запрос непосредственно к конечной точке /image/{img_hash}.png, которая должна использоваться только для отображения изображения. Поэтому давайте скроем ее от спецификации OpenAPI, добавив параметр include_in_schema=False в декоратор конечной точки.
@app.get("/image/{img_hash}.png", include_in_schema=False)
def get_image(img_hash: str):
# Endpoint code here
Теперь вы больше не увидите конечную точку в интерактивной документации, но сможете получить к ней доступ. Теперь давайте проверим, работает ли наш плагин так, как ожидалось.
Отлично! Наш плагин работает идеально! Обязательно протестируйте несколько раз с разными подсказками, чтобы убедиться, что все работает правильно.
Понимание механизма запросов ChatGPT
Когда я разрабатывал этот плагин, у меня возник один насущный вопрос: как ChatGPT отправляет запросы к моему API? Немного поэкспериментировав, я выяснил несколько интересных вещей.
Подсказка плагина является единственным источником информации, которую ChatGPT имеет о нашем API, и, если вы заметили, не содержит подробностей о конечных точках или о том, как передавать данные запроса. Когда ChatGPT хочет отправить запрос, он выводит специальный структурированный формат, который бэкенд-система ChatGPT интерпретирует как команду.
Например, если ChatGPT хочет отправить запрос на создание QR-кода для https://chat.openai.com/, он выдает структурированный результат, приведенный ниже. Этот вывод разбирается и обрабатывается внутренней системой ChatGPT, после чего запрос отправляется в наш API. Обратите внимание, что я опустил некоторые специальные маркеры, которые используются для различения ответа на разговор и структурированного ответа.
assistant to=qr_code.generate_qr_code_generate_post
{
"string": "https://chat.openai.com/"
}
Здесь у нас есть имядлямодели, которое мы определили в файле манифеста, и operationId конечных точек в спецификации OpenAPI. Вся коммуникация с бэкендом ChatGPT происходит через объект JSON, и ответ также представляет собой объект JSON.
Подведение итогов
Вот и все. Вы создали свой первый плагин ChatGPT для генерации QR-кодов.
Стартовый шаблон, который мы использовали в этом руководстве, отлично подходит для быстрого запуска, но в реальном приложении вам нужно будет следовать лучшим практикам, таким как модульное построение кодовой базы, обработка аутентификации, валидация запросов, обработка ошибок, написание тестов и многое другое.
Надеюсь, вам понравилась эта статья, и если у вас есть вопросы, не стесняйтесь оставить комментарий ниже.
Хорошего кодинга!