Building a ChatGPT Plugin for Medium

Technical exploration & capabilities of our proof of concept

Thomas Ricouard
Medium Engineering

--

OpenAI recently announced the support of plugins for ChatGPT. Plugins are a groundbreaking feature for ChatGPT as they solve one of the platform’s biggest problems, which is its lack of access to the internet and up to date content. ChatGPT was trained on a dataset from 2021 and does not have direct access to the internet. Plugins provide a solution to this limitation. Content platforms like Medium can expose their content to ChatGPT, based on the user prompt and installed plugins, ChatGPT can trigger the correct API of your plugins to retrieve a piece of content and do some manipulation on it.

Plugin support for Chat GPT is still in private alpha, we got early access at Medium, so we could explore the possibilities for our content to be retrievable by ChatGPT

I want to point out this is a technical exploration, as of now, we don’t yet have a plan about releasing this plugin.
This article oriented around the technical demonstration side of things, how we built it, how a ChatGPT plugin work and what our plugin can do.

🤖 Our setup

Building a ChatGPT plugin is fairly simple, you need to expose a .well-known/ai-plugin.json on your domain. It provides the basic information so that ChatGPT can understand that your domain support plugin, and can link the appropriate description, icons, link etc.. to the end user when installing the plugin.

{
"schema_version": "v1",
"name_for_human": "Medium plugin",
"name_for_model": "MediumGPTPlugin",
"description_for_human": "Plugin for accessing, browsing and extracting Medium content.",
"description_for_model": "Requests Medium posts, stories, to do manipulation and queries on the content.",
"auth": {
"type": "none"
},
"api": {
"type": "openapi",
"url": "https://medium.com/_/gpt/openapi.yaml",
"is_user_authenticated": false
},
"logo_url": "https://miro.medium.com/v2/1*m-R_BkNf1Qjr1YbyOIJY2w.png",
"contact_email": "support@medium.com",
"legal_info_url": "https://policy.medium.com/medium-terms-of-service-9db0094a1e0f"
}

The most important part is the OpenAPI YAML file you’ll expose, you can see ours in the url field in the code snippet above. This is a spec that will allow ChatGPT to understand your API. It use the OpenAPI specification, you can actually get started by writing your spec there and you can then export the spec into a server interface in various languages. There is also a linter over there so you can make sure your API spec is correct.

On my side, I was actually writing the MediumGPT service in Go (most of our microservices are in Go at Medium), what I did to actually write the OpenAPI specification? I pasted my Go code into ChatGPT and asked it to output the OpenAPI specification. With(out) surprises, it was perfect, a few changes and it was basically ready.

Why do we need a new service you might ask? We have an internal API in GraphQL, but ChatGPT can’t really talk to a GraphQL API (maybe in the future, a well documented schema sound very powerful), so I needed to make a middleware in Go that expose parts of it as a standard REST API.

The MediumGPT microservice is a simple middleware that convert hardcoded GraphQL queries and expose them in a REST API as JSON results. Straightforward, simple enough for a proof of concept. Also simple enough for me as it was the first time I was actually writing Go code for our backend 😎.

To give you an idea here is what one of our bridge function that serve the trending posts looks like:

type Post struct {
ID string `json:"id"`
Title string `json:"title"`
// ... More Post object fields
}

func (a *App) Trending(ctx context.Context) ([]*model.Post, error) {
req := graphql.NewRequest(`
query trending {
... GQL Query
}
`)
var respData struct {
Trending struct {
Posts []*model.Post `json:"posts"`
} `json:"trending"`
}
if err := a.client.Run(ctx, req, &respData); err != nil {
return nil, errors.Wrap(err, "fetching trending from rito")
}

return respData.Trending.Posts, nil
}

And what the OpenAPI spec for this endpoint look like:

openapi: 3.0.3
paths:
/v1/trending:
get:
summary: Get trending stories
description: Returns a list of stories that are currently trending on Medium
operationId: getTrendingStories
responses:
"200":
description: A list of trending stories
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Story"
components:
schemas:
Story:
type: object
properties:
id:
type: string
description: The unique ID of the story
title:
type: string
description: The title of the story

The most important part is indeed your OpenAPI documentation. This is what ChatGPT will parse to understand what your plugin can do, what API to trigger and when. Your summary and description need to be simple, the OpenAI documentation is very clear about it in the “Writing description” part of their documentation.

You might feel like you would need to provide complexe description of your API with all the trigger possible etc… but this is not how ChatGPT will understand it. Description of your schema objects is also important, be simple & clear about what each field is about, and ChatGPT will make sense of it altogether.

For the proof of concept we exposed 4 API to ChatGPT

  • Get a list of the trending posts. (Contain the post title, subtitle, author info, link, topics, etc… but not the actual content).
  • Query one specific post to get its full content (if not behind the paywall).
  • Query one topic to get trending, latest posts in this topics, alongside related topics.
  • Search posts using a specific query.

Once all that was built and deployed, this is when the fun truly began.

Installing the MediumGPT plugin on ChatGPT interface

Let’s go!

👾 Debugging the plugin

Now I hope you’re ready to get your 🧠 explode! Debugging the plugin was probably the best part for me. Why? Because I debugged the plugin directly on ChatGPT itself.

First, let’s ask ChatGPT if it understand our plugin API surface

The answer is definitely yes, this is the stage where I first read what it understood of some API and went back to the documentation to adjust them a bit. It’s important that you look at each API description and see if it’s fit what you had in mind.

One of the top question we had on our mind was “Does the user needs to specifically ask for Medium in their prompt in order for ChatGPT to trigger our plugin”

So let’s ask ChatGPT

The answer is Yes and also no. Right now we can run ChatGPT only with one plugin installed. So it’s quite biased toward using the Medium plugin anyway. We can imagine that if multiple platforms expose stories / posts API, then adding Medium to the prompt will be important to target our plugin specifically.

For example I can definitely trigger it by asking what are the 3 trending articles, ChatGPT will trigger the Medium plugin and use the trending API correctly. And then give me the 3 first articles of the response.

We even get nice little card at the end of the message

Let’s do one more test to see if it’s really understand our API and schema. In the post model, we have one attribute, isLocked, and it’s documented like so

isLocked:
type: boolean
description: Whether the story is locked behind the paywall

Let’s try some query to see if ChatGPT understand which posts are behind the paywall vs not behind the paywall.

And indeed it’s right! Those 3 stories are not behind the paywall.

Notice also how it doens’t trigger a call to our plugin because it already have the previous response in its context already.

Now, that we know that our plugin work correctly and that ChatGPT understand our API, we can start to make some more complexe (and useful) queries. I would also like to see if it can chain queries together to form a result.

And it works! It correctly first fetch our trending API and then fetch the content of the first story. And then give me a summary of the story.

Oh and you can actually read the full post here

Let’s get a little bit further and get some related posts because I want to dig further into this topic

🤯 It triggered our search API with a not too bad query, and we get some related results

Another example of chaining queries, I’ve asked to get the latest post in the topic of the story because I was interested in it. Sure enough it first fetched the topic and then fetched the content of the first post in there.

You can also be much more natural in your question of course. Below I ask to get general opinion and a summary of a heated tech debate: SwiftUI VS UIkit.

It successfully extracted my query to make a search query.

And then went to fetch the content of a relevant post to make me a summary of that post with a link to the full story at the bottom.

For a last example, I wanted to see if it could be smart about our API. You see we have one hole in the API. Our getTopic call require a topic ID to get the actual topics data. So I’ve asked ChatGPT how it could get topic info without its ID:

And it correctly understood that you would have first to use the search API to get story related to Artificial Intelligence, and from there you can grab the topic ID from the post topic object to then to a query on our Topic API. I was a bit mind blown that it could come up with this chain of thought.

For the practical use of this plugin, with some more API exposed to ChatGPT, the possibilities could be limitless. I see it as a natural query language for browsing and doing manipulation on any Medium content (once again, for content not behind the paywall). We’re not yet at a stage where we know where we’ll go with this plugin. But building and playing with it was a very fun experience.

I hope you enjoyed this article and feel free to reach out if you have any questions.

--

--

📱 🚀 🇫🇷 [Entrepreneur, iOS/Mac & Web dev] | Now @Medium, @Glose 📖| Past @google 🔍 | Co-founded few companies before, a movies 🎥 app and smart browser one.