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:

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.