# 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](/content/docs/elixir/index.html). 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

```bash
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:

```bash
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:

```elixir
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](/content/phoenix-files/sqlite3-full-text-search-with-phoenix/index.html), 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:

```elixir
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`:

```elixir
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](https://github.com/mschae/cors_plug) dependency to our `mix.exs`:

```elixir
  {:cors_plug, "~> 3.0"},
```

And then add a line to our `endpoint.ex`, cleaning up the file to look something like this:

```elixir
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:

```elixir
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`:

```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`:

```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](https://platform.openai.com/docs/plugins/getting-started/writing-descriptions), 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](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.
