Minimum Viable ChatGPT Plugin · The Phoenix Files
Creating a ChatGPT Plugin with Elixir Phoenix
This is a post about creating a ChatGPT Plugin with Elixir Phoenix. If you want to deploy your Phoenix App right now, then check out how to get started. You could be up and running in minutes.
Problem
You just got access to the ChatGPT Plugin beta, and you want to hook up your database of knowledge to the AI GPT4, so it can help you and customers navigate your complex world. You’ve never done that, and all of their docs are in Python…
Solution
A ChatGPT plugin is essentially a manifest file and an OpenAPI spec, to tell ChatGPT how to consume an API. Phoenix is perfect for building the kind of APIs that ChatGPT consumes, so let’s see how to create a minimum viable plugin using Phoenix and Elixir! The only prerequisite is a searchable database; this can be anything that accepts text queries and returns data you want, from SQLite full text search to a third party API like Algolia.
We can build more complex plugins for ChatGPT to consume with, but this guide walks through only the most basic example; to set a foundation for us to build on. Let’s get started with a fresh project.
Phoenix
mix phx.new --no-assets --no-html --no-live --no-mailer --no-ecto --no-dashboard chat_gpt_plugin
When building a ChatGPT Plugin, we’re actually building a standard JSON API with an OpenAPI spec configuration. So we’re generating a new Phoenix Application with basically just an Endpoint and Router. If you have an existing Phoenix Application, you should be able to copy the code directly into your code base.
Next up let’s generate a JSON API:
mix phx.gen.json Search Document documents title:string body:string --no-context
* creating lib/chat_gpt_plugin_web/controllers/document_controller.ex
* creating lib/chat_gpt_plugin_web/controllers/document_json.ex
* creating lib/chat_gpt_plugin_web/controllers/changeset_json.ex
* creating test/chat_gpt_plugin_web/controllers/document_controller_test.exs
* creating lib/chat_gpt_plugin_web/controllers/fallback_controller.ex
Add the resource to your :api scope in lib/chat_gpt_plugin_web/router.ex:
resources "/documents", DocumentController, except: [:new, :edit]
Telling the Phoenix generator to skip generating the context with the --no-context flag. Let’s first open up the controller and make some edits: removing everything except the index function, it should look like this:
defmodule ChatGptPluginWeb.DocumentController do
use ChatGptPluginWeb, :controller
alias ChatGptPlugin.Search
action_fallback ChatGptPluginWeb.FallbackController
def index(conn, %{"query" => query}) do
# Here is where YOU search
documents = Search.list_documents(query)
render(conn, :index, documents: documents)
end
end
Noting that I am purposely leaving out HOW you search for documents. When I was developing this example I used SQLite Full Text Search, but you can use Postgres or a fancy Vector Database, or Elasticsearch!
Next up, we will add a route to our Router, and it should look something like this:
defmodule ChatGptPluginWeb.Router do
use ChatGptPluginWeb, :router
pipeline :api do
plug :accepts, ["json"]
end
scope "/api", ChatGptPluginWeb do
pipe_through :api
get "/gpt-search", DocumentController, :index
end
end
We’ll also want to update our view code by editing document_json.ex:
defmodule ChatGptPluginWeb.DocumentJSON do
@doc """
Renders a list of documents.
"""
def index(%{documents: documents}) do
%{data: for(document <- documents, do: data(document))}
end
@doc """
Renders a single document.
"""
def show(%{document: document}) do
%{data: data(document)}
end
defp data(document) do
%{
title: document.title,
body: document.body
}
end
end
I simply removed the match on %Document{} as I don’t have that. Feel free to modify this to match your model! And in terms of Phoenix specific stuff, we are done here.
Chat GPT Specifics
ChatGPT has a couple specific needs for your local plugin to work:
- CORS Enabled
- It needs your server to serve the following files available:
/.well-known/ai-plugin.json/openapi.yaml
To add CORS we’ll need to add the cors_plug dependency to our mix.exs:
{:cors_plug, "~> 3.0"},
And then add a line to our endpoint.ex, cleaning up the file to look something like this:
defmodule ChatGptPluginWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :chat_gpt_plugin
# CORS Config for local development
plug CORSPlug,
origin: ["http://localhost:4000", "https://chat.openai.com"],
methods: ["GET", "POST"],
headers: ["*"]
plug Plug.Static,
at: "/",
from: :chat_gpt_plugin,
gzip: false,
only: ChatGptPluginWeb.static_paths()
if code_reloading? do
plug Phoenix.CodeReloader
end
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.MethodOverride
plug Plug.Head
plug ChatGptPluginWeb.Router
end
I removed the LiveView mount and Session related options as we won’t be needing them for our setup.
That’s it for CORS! Let’s update our ChatGptPluginWeb.static_paths() function in our chat_gpt_plugin_web.ex file:
def static_paths, do: ~w(.well-known openapi.yaml favicon.ico robots.txt)
Adding the .well-known folder and openapi.yaml file to known static paths, also removing the images and asset stuff we won’t be needing.
And finally, create an openapi.yaml file at priv/static/openapi.yaml:
openapi: 3.0.0
info:
title: Example Documents Search Plugin with Elixir, Phoenix, and Sqlite3 plugin.
description: Plugin for searching docs
version: 1.0.0
servers:
- url: http://localhost:4000/api/chatgpt
paths:
/gpt-search:
get:
operationId: searchDocuments
summary: Search for documents
description: This endpoint takes a query and searches for documentation
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- query
properties:
query:
type: string
description: The document description to search for.
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
results:
type: array
items:
type: object
properties:
title:
type: string
description: The document title.
contents:
type: string
description: The document contents.
This is pretty verbose as OpenAPI’s description docs tend to be, but if you read through line by line it’s fairly self-explanatory. It’s describing our API, the get /gpt-search route we made and the query parameter we expect, and finally explains the shape of the JSON we’re returning. OpenAI will use this to tell ChatGPT how to retrieve documents, if it needs them.
Finally, our priv/static/.well-known/ai-plugin.json:
{
"schema_version": "v1",
"name_for_human": "Documentation Search",
"name_for_model": "doc_search",
"description_for_human": "Example Documents Search Plugin with Elixir, Phoenix, and Sqlite3 plugin.",
"description_for_model": "You are an expert documentation researcher, when answering questions about my documents you reference the documentation often",
"auth": {
"type": "none"
},
"api": {
"type": "openapi",
"url": "http://localhost:4000/openapi.yaml",
"is_user_authenticated": false
},
"logo_url": "logo.png",
"contact_email": "jason@fly.io",
"legal_info_url": "http://localhost:4000/terms"
}
There is more detail in the docs about these various fields, but the most important one is description_for_model as this is your prompt when searching the API.
And that is it! If you mix phx.server the running application, open up https://chat.openai.com, click the Plugins Dropdown at the top then this little “Develop your own plugin” link
And then enter your URL http://localhost:4000/ it should find it and start working! If all goes well, you should have your logo and description showing up in the plugin list. When chatting with ChatGPT it will make a decision when to reference your API based on the prompt and the expected info.