Implementing AI Copywriting with ChatGPT in Elixir
Intoduction
Building on our previous exploration of creating a crypto news website using AI-generated content, we are now turning our focus towards the application of that parsed data. The goal of this next phase is to leverage the capabilities of OpenAI’s language model, ChatGPT, to copywrite the parsed information.
Previous article:
An Experiment in Creating a Crypto News Website
In this article, we will delve into the technicalities of how we can make this happen, using the versatile Elixir library, Tesla, to create a client module for making HTTP requests. Our client module will consist of two main methods. The first method aims to rewrite paragraph text with a specific prompt, effectively transforming the original information into an AI-generated version. The second method is designed to generate a copywritten description from the first paragraph of each parsed post.
CopywriteAPI module
The beauty of working with the Elixir Tesla library is that it simplifies the process of creating HTTP requests, making the implementation of our AI copywriting module far from complex. With a solid understanding of Elixir and the Tesla library, you’ll find that building the copywriter api module is a relatively straightforward process. So, without further ado, let’s dive into the code!
defmodule Pandora.CopywriterAPI do
use Tesla
require Logger
@api_token Application.compile_env!(:pandora, [Pandora.CopywriterAPI, :api_key])
plug Tesla.Middleware.BaseUrl, "https://api.openai.com"
plug Tesla.Middleware.Headers, [{"authorization", "Bearer #{@api_token}"}]
plug Tesla.Middleware.JSON
@rewrite_text_task "REDACTED"
@doc """
Rewrites text with given prompt.
https://platform.openai.com/docs/api-reference/completions
"""
def rewrite(text), do: do_request(text, @rewrite_text_task)
@description_task "REDACTED"
def create_description({:p, paragraph}), do: do_request(paragraph, @description_task)
defp do_request(text, prompt) do
tokens = ((String.length(text) / 4) |> ceil()) + 100
payload = %{model: "text-davinci-003", prompt: [prompt, text], max_tokens: tokens, temperature: 0.7}
with {:ok, %{body: %{"id" => _} = body}} <- post("/v1/completions", payload) do
body["choices"]
|> Enum.find(fn choice -> choice["index"] == 1 end)
|> Map.get("text")
|> String.trim()
else
error ->
Logger.error("Can't create description. Error: #{inspect(error)}")
error
end
end
end
Copywriter module
In this next phase of our journey, we delve into the implementation of the Copywriter GenServer process. This is where the magic really starts to happen.
Upon initialization, this process fetches a batch of parsed posts from the database, setting a limit to control the workload. It then meticulously processes these posts one by one, ensuring careful and accurate copywriting. After every post is processed, the GenServer sets a timer for a specified interval before proceeding to the next chunk, thereby controlling the pace of the operation.
The parsed posts are stored in the database, and their content is represented as a list of tuples, each containing an HTML tag and its corresponding binary content. Copywriter takes each post and reduces the content, copywriting each paragraph by calling upon our previously discussed CopywriterAPI.
But it doesn’t stop there. It also uses the CopywriterAPI to generate a captivating description from the first paragraph of the post. This description plays a pivotal role in capturing the attention of potential readers on the website.
Once the copywriting is done, the GenServer creates a new post in the database, ready to be displayed on the website. To increase the reach, it also publishes a message with this post in a Telegram channel, ensuring that our content reaches as many interested readers as possible. Lastly, it refreshes the sitemap.xml file, keeping our website up-to-date for web crawlers and consequently, improving our SEO.
Through the combination of Elixir, the Tesla library, and the Copywriter GenServer process, we have a well-oiled machine that transforms parsed data into SEO-friendly, AI-generated content. The possibilities are, indeed, exciting!
defmodule Pandora.Copywriter do
use GenServer
require Logger
alias Pandora.Post
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(opts) do
run_interval = Keyword.fetch!(opts, :run_interval)
limit = Keyword.fetch!(opts, :limit)
{:ok, %{limit: limit, run_interval: run_interval}, {:continue, :first_run}}
end
@impl true
def handle_continue(:first_run, %{limit: limit} = state) do
Logger.info("Starting copywriter and executing first run")
process_parsed_posts(limit)
{:noreply, state, {:continue, :schedule_next_run}}
end
def handle_continue(:schedule_next_run, %{run_interval: run_interval} = state) do
Process.send_after(self(), :process_parsed_posts, run_interval)
{:noreply, state}
end
@impl true
def handle_info(:process_parsed_posts, %{limit: limit} = state) do
process_parsed_posts(limit)
{:noreply, state, {:continue, :schedule_next_run}}
end
defp process_parsed_posts(limit) do
limit
|> Pandora.ParsedPost.load_unprocessed_posts()
|> Enum.each(fn parsed_post ->
content =
Enum.reduce(parsed_post.content, [], fn
{:p, chunk}, acc ->
case Pandora.CopywriterAPI.rewrite(chunk) do
text when is_binary(text) -> [{:p, text} | acc]
_ -> [{:p, chunk} | acc]
end
el, acc ->
[el | acc]
end)
|> Enum.reverse()
first_paragraph = Enum.find(content, fn {:p, _} -> true end)
description =
if first_paragraph do
Pandora.CopywriterAPI.create_description(first_paragraph)
end
attrs = %{
title: parsed_post.title,
content: content,
description: description,
datetime: parsed_post.datetime
}
case Post.insert(attrs) do
{:ok, post} ->
send_to_telegram(post)
Pandora.Sitemaps.generate()
Logger.info("Inserted new post with ID #{post.id}")
{:error, changeset} ->
Logger.error("Post insertion failed. Errors: #{inspect(changeset.errors)}")
end
Pandora.ParsedPost.mark_processed(parsed_post)
end)
end
@bot_token "REDACTED"
@chat_id "REDACTED"
def send_to_telegram(post) do
{_, first_paragraph} = Enum.find(post.content, fn {el, _content} -> el == :p end)
formatted_title = format(post.title)
url = PandoraWeb.Router.Helpers.post_url(PandoraWeb.Endpoint, :show, post.slug)
result =
Telegram.Api.request(@bot_token, "sendMessage", %{
chat_id: @chat_id,
text: "\*#{formatted_title}\*
#{format(first_paragraph)}
\[Read more\]\(#{format(url)}/\)",
# \[Read more\]\(#{url}\)",
parse_mode: "MarkdownV2"
})
case result do
{:ok, _} -> :ok
{:error, error} -> Logger.error("Telegram API error: #{inspect(error)}")
end
end
defp format(string) do
String.replace(
string,
["_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"],
fn symb -> "\\" <> symb end
)
end
end
Some numbers
We’ve successfully published over 500 posts, all curated and copywritten by our AI. Maintaining affordability with copywriting costs around $60. In terms of search engine visibility: 77 pages indexed by Google and 66 by Bing.
In conclusion, leveraging the power of Elixir, the Tesla library, and ChatGPT, we’ve implemented modules for transforming parsed data into AI-generated content for our crypto news website. Implementing the CopywriterAPI and the Copywriter process has demonstrated how AI copywriting can be a streamlined, efficient process with immense potential.
Stay tuned.