Minimum Viable ChatGPT Plugin

A purple droplet representing Elixir as a librarian and a robot representing ChatGPT as a patron.
Image by Annie Ruygt

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

Screenshot of OpenAI ChatGPT Plugin UI

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.

Screenshot of OpenAI ChatGPT Plugin UI with your new Plugin Listed

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!

Fly.io ❤️ Elixir

Fly.io is a great way to run your Phoenix Apps close to your users. It’s really easy to get started. You can be running in minutes.

Deploy a Phoenix app today!