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 dep 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. I am no expert here, but the docs lay out better examples when searching.
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! I found this step to be a little finicky, you can enable Plugin Developer Mode
in the settings of the bottom left-hand side of the page. You can also check the Browser Developer Tools for console errors, which often have better error messages than the UI. One other thing to try is to directly link to the plugin http://localhost:4000/.well-known/ai-plugin.json
that might have better luck.
If all goes well, you should have your logo and description showing up in the plugin list, here is one I was playing with internally here at Fly.io! When chatting with ChatGPT it will make a decision when to reference your API based on the prompt and the expected info. If your description has keywords like Fly.io it will know to query your API for more info.
Try it yourself!
I also put together a “Single File” example showing all the steps in one simple spot.
https://github.com/jeregrine/single_file_gpt_plugin
Further Considerations
You aren’t limited to text, and in fact you will likely have a better time with structured output as in this example:
https://mobile.twitter.com/sean_moriarity/status/1648085165011288064
ChatGPT is pretty good at building your specific API inputs if well-defined in your OpenAPI.yaml. On that note, I don’t recommend maintaining an openapi.yaml file by hand. There are tons of OpenAPI libraries out there such as open_api_spex that will give you a declarative API for building out your YAML without as much boilerplate.
Really the limit is your imagination on what you can build with Plugins and on top of OpenAI’s ChatGPT. If you found this helpful please reach out and let me know what you’ve built!