Artificial intelligence in programming

GitHub avatar of Ivan Montilla
July 6, 2026
Post licensed under CC BY 4.0
Tags:
⚠ Info

This post is a translation of my own post La inteligencia artificial en el ámbito de la programación, written in 2022, a few weeks after ChatGPT was launched.

Keep the context of that time in mind: modern harnesses and agents did not exist when I wrote that post.

The content of this blog is written in Markdown, but for some time now I have been thinking about switching to a format that is just as lightweight, but a bit more powerful*. Among other things, I am interested in it being an extensible format. My colleague Alejandro Quirós recommended that I take a look at Asciidoc, so I have spent some time looking into it and testing it locally.

Asciidoc seems to have everything I need. It is a lightweight text format that, by default, contains all the element types from Markdown and a few more, for example admonitions, which allow you to create warning or tip boxes.

Looking back at my posts, I have noticed that in some of them I comment on different tweets, so I thought it would be useful to be able to embed a tweet inside a post. Asciidoc does not allow you to embed a tweet by default, but because it is extensible, you can create a plugin for it.

The software that transforms Asciidoc documents into HTML documents is called Asciidoctor and it is written in Ruby. Asciidoctor extensions are therefore written in Ruby.

⚠ Info

(*)Finally, in 2025, I decided to use MDX (Markdown with React Components) on I create some components to extends Markdown language. Asciidoc were discarded.

ChatGPT

Unless you have been living in a cave for the last few days, you will know what ChatGPT is: a conversational artificial intelligence capable of holding conversations through a chat.

ChatGPT is capable of holding all kinds of conversations while keeping the context from previous messages and, if asked, it is capable of programming.

Since I have no experience at all with Ruby, I thought about asking ChatGPT to program that extension for me. After exchanging a long list of messages asking for certain changes, warning it about errors I was getting, and making some manual changes to the generated code, the AI managed to write a fully functional Asciidoctor plugin to embed tweets in a document.

The conversation with ChatGPT

The first thing I did was ask it, in general terms, for what I needed, and this is what happened:

Ivan: Can you create an Asciidoctor extension in Ruby to embed a tweet inside an Ascidoc document?

ChatGPT: Yes, it is possible to create an Ascidoctor extension in Ruby to embed a tweet inside an Asciidoc document. To do this, you would first need to create a Ruby class that extends the Asciidoctor::Extensions::BlockProcessor class. This class would define a method for processing the tweet block in your Asciidoc document.

To embed the tweet, you would need to use the Twitter API to fetch the tweet by its ID and then generate the necessary HTML code to display the tweet on the page. You can then use the Asciidoctor::Document#create_block method to create a new block containing the HTML code for the tweet and add it to the document.

Here is an example of how this might look:

class TweetBlock < Asciidoctor::Extensions::BlockProcessor
  use_dsl
  named :tweet
  on_context :open

  def process parent, target, attrs
    tweet_id = target
    
    # Fetch the tweet using the Twitter API
    tweet = TwitterClient.fetch_tweet(tweet_id)

    # Generate the HTML code for the tweet
    html = generate_tweet_html(tweet)

     # Create a new block containing the tweet HTML and add it to the document
    create_block parent, :pass, html, attrs
  end

  def generate_tweet_html tweet
    # todo
  end
end

Once you have created this class, you can register it with Asciidoctor by adding it to the list of extensions in your Asciidoctor configuration. This will allow you to use the tweet block in your Asciidoc document to embed tweets.

Here is an example of how you might use this extension in your Asciidoc document:

tweet::1234567890[]

This would create a block containing the tweet with the ID 1234567890, as fetched from the Twitter API.

As we can see, three things happen in this code:

  1. Using the Twitter API for Ruby, it gets the content of the tweet.
  2. It passes the tweet to a function so it can transform it into HTML.
  3. It creates an Asciidoc block with the HTML content.

Let us analyze the code a bit. One of the first things it does is pass the ID directly to the Twitter API, but it does not check whether it is valid (numeric), so I let the AI know.

Ivan: tweet_id should contains the ID of the tweet, but this script doesn’t validate that is a valid tweet ID. Can you update it to add this validation?

ChatGTP: Yes, you are correct that the current example code does not validate that the id attribute contains a valid tweet ID. This could lead to errors if an invalid ID is provided.

To add this validation, you can use the Twitter API to check if a tweet with the specified ID exists. If the tweet does not exist, you can raise an error to let the user know that the ID is invalid.

Here is an example of how you might update the process method to add this validation:

def process parent, target, attrs
  tweet_id = target

  # Validate that the ID is a valid tweet ID
  unless TwitterClient.tweet_exists?(tweet_id)
    raise  "Invalid tweet ID: #{tweet_id}"
  end

  # Fetch the tweet using the Twitter API
  tweet = TwitterClient.fetch_tweet(tweet_id)
  
  # Generate the HTML code for the tweet
  html = generate_tweet_html(tweet)
  
  # Create a new block containing the tweet HTML and add it to the document
  create_block  parent, :pass, html, attrs
end

This code will check if a tweet with the specified ID exists using the TwitterClient.tweet_exists? method. If the tweet does not exist, it will raise an error with the invalid tweet ID. Otherwise, it will proceed to fetch the tweet and generate the HTML code to embed it in the document.

I expected it to validate that it was numeric, but surprisingly, it uses the Twitter API to check whether it exists. Although this is a correct answer, it requires an extra API call, and the API has a request limit. Ideally, for me, the script execution should use only one call, not several. I mention this to it and I am more specific about the validation I need.

Ivan: That’s a correct validation, but it requires to make an extra call to Twitter API, that is rate limited. A tweet ID is ever numeric. Can you update the script to validate that the tweet ID is numeric instead of checking if the tweet exists?

ChatGPT: Yes, you are correct that making an extra call to the Twitter API to validate the tweet ID can be wasteful if the ID is not valid, as it counts against your API rate limit. Additionally, tweet IDs are always numeric, so you can use this information to validate the ID without making an extra API call.

To do this, you can simply check if the tweet_id variable contains a numeric value, and raise an error if it does not. Here is an example of how you might update the process method to do this:

def process parent, target, attrs
  tweet_id = target

  # Validate that the ID is numeric
  unless tweet_id.is_a?(Numeric)
    raise  "Invalid tweet ID: #{tweet_id}"
  end

  # Fetch the tweet using the Twitter API
  tweet = TwitterClient.fetch_tweet(tweet_id)
 
  # Generate the HTML code for the tweet
  html = generate_tweet_html(tweet)
 
  # Create a new block containing the tweet HTML and add it to the document
  create_block  parent, :pass, html, attrs
end

This code will check if the tweet_id variable contains a numeric value using the Numeric class, and raise an error if it does not. This will ensure that the tweet ID is valid without making an extra API call.

Since I have no experience programming with Ruby, I do not know what the is_a? method does, so I ask it directly.

Ivan: What is_a? method returns?

ChatGPT: The is_a? method in Ruby returns a boolean value indicating whether the object on which it is called is an instance of the specified class or one of its descendants.

For example, if you have the following code:

x = "hello"

if x.is_a?(String)
  puts "x is a string"
end

This will output x is a string to the console, because the x variable is an instance of the String class.

On the other hand, if you have the following code:

x = 3

if x.is_a?(String)
  puts "x is a string"
end

This will not output anything to the console, because the x variable is not an instance of the String class (it is an instance of the Fixnum class, which is a descendant of the Numeric class).

In general, the is_a? method is useful for checking the type of an object at runtime and taking different actions based on that type.

As it said, it would be equivalent to Java’s instanceof operator or C#’s is operator. We should assume that Asciidoctor will send the ID as a string (I will make sure of that myself when I create the Asciidoc documents), so I let it know.

Ivan: Ok. We should asume that tweet_id is ever an string. Can you check if the string is an integer value?

ChatGPT: Yes, you are correct that the tweet_id variable in the process method is always a string, even if it contains a numeric value. To check if this string contains an integer value, you can use the String#to_i method, which converts the string to an integer if it contains a valid integer value, or returns 0 if it does not.

Here is an example of how you might update the process method to use the String#to_i method to check if the tweet_id variable contains a valid integer value:

def process parent, target, attrs
  tweet_id = target

  # Validate that the ID is an integer value
  unless tweet_id.to_i != 0
    raise  "Invalid tweet ID: #{tweet_id}" 
  end

  # Fetch the tweet using the Twitter API
  tweet = TwitterClient.fetch_tweet(tweet_id)
 
  # Generate the HTML code for the tweet
  html = generate_tweet_html(tweet)
 
  # Create a new block containing the tweet HTML and add it to the document
  create_block parent, :pass, html, attrs
end

In this code, the tweet_id.to_i != 0 expression will check if the tweet_id string contains a valid integer value. If it does not, it will raise an error with the invalid tweet ID. Otherwise, it will proceed to fetch the tweet and generate the HTML code to embed it in the document.

The AI understood perfectly what I meant and corrected the script again. It also explains how it works. Perfect!

Notice that with every change it makes, it also updates the comments so they do not become outdated. 10 out of 10!

Once the tweet validation is done, let us continue. I am going to ask it to implement the generate_tweet_html method, which it had left only with a TODO.

Ivan: Please, implement generate_tweet_html method.

ChatGPT: To implement the generate_tweet_html method, you will need to use the Twitter API to fetch the tweet by its ID and then generate the necessary HTML code to display the tweet on the page.

Here is an example of how you might implement the generate_tweet_html method:

def generate_tweet_html(tweet)
  # Use the Twitter API to fetch the tweet by its ID
  tweet = TwitterClient.fetch_tweet(tweet_id)

  # Generate the HTML code for the tweet
  html = <<~HTML
    <blockquote class="twitter-tweet">
      <p>#{tweet.text}</p>
      <a href="https://twitter.com/#{tweet.user.screen_name}/status/#{tweet.id}">
        #{tweet.created_at.strftime("%b %d %Y")}
      </a>
    </blockquote>
  HTML
end

In this code, the generate_tweet_html method takes a tweet object as an argument. It then uses the Twitter API to fetch the tweet by its ID and generates the necessary HTML code to display the tweet on the page. The generated HTML uses the <blockquote> element to display the tweet text and the <a> element to link to the tweet on Twitter.

Once you have implemented the generate_tweet_html method, you can use it in the process method to generate the HTML code for the tweet and add it to the document.

Here is an example of how you might use the generate_tweet_html method in the process method:

def process parent, target, attrs
  tweet_id = target

  # Validate that the ID is an integer value
  unless tweet_id.to_i != 0
    raise  "Invalid tweet ID: #{tweet_id}" 
  end

  # Fetch the tweet using the Twitter API
  tweet = TwitterClient.fetch_tweet(tweet_id)
 
  # Generate the HTML code for the tweet
  html = generate_tweet_html(tweet)
 
  # Create a new block containing the tweet HTML and add it to the document
  create_block  parent, :pass, html, attrs
end

This code will use the generate_tweet_html method to generate the HTML code for the tweet, and then create a new block containing the HTML and add it to the document. This will allow the tweet to be displayed on the page when the Asciidoc document is rendered.

Several things happen here. The first is that inside this method, it is using the Twitter API to get the data again, even though it already knew that data because it receives it as a parameter. I take care of deleting the first line myself.

You can also see that the AI returned the content of the process method again without any changes. This is a constant throughout the whole conversation, so from this point on, I will omit that part of its responses.

My next problem is that, in order to use the Twitter API for Ruby, it requires installing it through a gem. Loading gems from Asciidoc, while possible, is relatively complicated, so I ask the AI to please replace it by making HTTP calls directly to the Twitter API.

Ivan: I cannot load any gem, so I need to use only libraries available in Ruby core. Can you replace TwitterClient by a method that make an HTTP request to the Twitter API?

ChatGTP: Yes, it is possible to make an HTTP request to the Twitter API using only the libraries available in the Ruby core. To do this, you can use the Net::HTTP class to make an HTTP request to the Twitter API and parse the response.

Here is an example of how you might use the Net::HTTP class to fetch a tweet by its ID:

def fetch_tweet tweet_id
  # Build the URI for the Twitter API endpoint
  uri = URI("https://api.twitter.com/1.1/statuses/show.json?id=#{tweet_id}")

  # Set the authorization header for the request
  headers = {
    'Authorization' => 'Bearer <YOUR_BEARER_TOKEN>'
  }

  # Make the request and parse the JSON response
  response = Net::HTTP.get(uri, headers)
  tweet = JSON.parse(response)

  # Return the tweet object
  tweet
end

In this code, the fetch_tweet method takes a tweet_id as an argument and uses the Net::HTTP class to make an HTTP GET request to the Twitter API endpoint for fetching a tweet by its ID. It sets the appropriate authorization header for the request and parses the JSON response into a Ruby object. It then returns the tweet object, which can be used to generate the HTML code for the tweet.

Once you have implemented this method, you can use it in the generate_tweet_html method to fetch the tweet and generate the HTML code for it. Here is an example of how you might do this:

def generate_tweet_html(tweet_id)
  # Use the Twitter API to fetch the tweet by its ID
  tweet = fetch_tweet(tweet_id)

  # Generate the HTML code for the tweet
  html = <<~HTML
    <blockquote class="twitter-tweet">
      <p>#{tweet.text}</p>
      <a href="https://twitter.com/#{tweet.user.screen_name}/status/#{tweet.id}">
        #{tweet.created_at.strftime("%b %d %Y")}
      </a>
    </blockquote>
  HTML
end

This code will use the fetch_tweet method to fetch the tweet by its ID and generate the HTML code for it using the <blockquote> and <a> elements, as in the previous example. “`

As you can see, it has not only implemented the fetch_tweet method for me, but it has also modified the generate_tweet_html and process methods so they use it.

Once you have implemented these methods, you can use them in the process method to fetch the tweet and generate the HTML code for it, and add it to the document. Here is an example of how you might do this:

def process parent, target, attrs
  tweet_id = target

  # Generate the HTML code for the tweet
  html = generate_tweet_html(tweet)

  # Create a new block containing the tweet HTML and add it to the document
  create_block parent, :pass, html, attrs
end

Here it has not only implemented a method to get the content of the tweet, but it has also modified the generate_tweet_html method so it calls it, and it has removed that call from the process method. I am not able to understand the reason for this change in the organization of the code, but I am not worried and I continue with my thing.

The method it has implemented uses the Twitter API 1.1. To access API 1.1, you need to make a formal request to Twitter and ask them to enable access for you. My only option is to use the Twitter API 2.0, so I mention it. No sooner said than done.

Ivan: Can you use Twitter API 2.0 in fetch_tweet method?

ChatGPT: Yes, it is possible to use the Twitter API 2.0 to fetch a tweet by its ID in the fetch_tweet method. To do this, you will need to update the URI and the authorization header for the request to use the new API endpoint and authentication scheme.

Here is an example of how you might update the fetch_tweet method to use the Twitter API 2.0:

def fetch_tweet tweet_id
  # Construct the URL for the Twitter API request
  tweet_url = "https://api.twitter.com/2/tweets/#{tweet_id}"
  uri = URI.parse(tweet_url)

  # Create the HTTP request and set the necessary headers
  request = Net::HTTP::Get.new(uri)
  request['Authorization'] = 'Bearer <YOUR_BEARER_TOKEN>'
  request['Accept'] = 'application/json'

  # Make the HTTP request and retrieve the response
  response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    http.request(request)
  end

  # Parse the JSON response
  tweet = JSON.parse(response.body)

  # Return the tweet object
  tweet
end

In this code, the fetch_tweet method uses the new Twitter API 2.0 endpoint for fetching a tweet by its ID, and sets the appropriate authorization header for the request. It then makes the request and parses the JSON response into a Ruby object, just like in the previous example.

As you can see, in the authorization header it puts a string that I should replace with my bearer token. I ask it to get it directly from an environment variable. Once again, no sooner said than done.

Ivan: Can you replace ‘Bearer ’ string by an environment variable?

ChatGPT: Yes, it is possible to use an environment variable to store the bearer token for the Twitter API instead of hardcoding it in the fetch_tweet method. To do this, you can use the ENV global variable in Ruby to access the value of the environment variable.

Here is an example of how you might update the fetch_tweet method to use an environment variable for the bearer token:

def fetch_tweet tweet_id
  # Construct the URL for the Twitter API request
  tweet_url = "https://api.twitter.com/2/tweets/#{tweet_id}"
  uri = URI.parse(tweet_url)

  # Create the HTTP request and set the necessary headers
  request = Net::HTTP::Get.new(uri)
  request['Authorization'] = "Bearer #{ENV['TWITTER_BEARER_TOKEN']}"
  request['Accept'] = 'application/json'

  # Make the HTTP request and retrieve the response
  response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    http.request(request)
  end

  # Parse the JSON response
  tweet = JSON.parse(response.body)

  # Return the tweet object
  tweet
end

In this code, the fetch_tweet method uses the ENV global variable to access the value of the TWITTER_BEARER_TOKEN environment variable and use it as the bearer token for the request. This allows you to store the bearer token in an environment variable and use it in the code without hardcoding it.

Now I ask the AI to modify the fetch_tweet method so it raises an error if the tweet does not exist.

Ivan: Can you raise an error if tweet id doesn’t exists?

ChatGPT: Yes, it is possible to raise an error if the tweet ID does not exist by checking the response from the Twitter API. If the tweet does not exist, the API will return a response with a 404 status code and an error message in the body. You can check for this response in the fetch_tweet method and raise an error if it is received.

Here is an example of how you might update the fetch_tweet method to raise an error if the tweet ID does not exist:

def fetch_tweet tweet_id
  # Construct the URL for the Twitter API request
  tweet_url = "https://api.twitter.com/2/tweets/#{tweet_id}"
  uri = URI.parse(tweet_url)

  # Create the HTTP request and set the necessary headers
  request = Net::HTTP::Get.new(uri)
  request['Authorization'] = "Bearer #{ENV['TWITTER_BEARER_TOKEN']}"
  request['Accept'] = 'application/json'

  # Make the HTTP request and retrieve the response
  response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    http.request(request)
  end
  
  # Check the status code of the response 
  if response.code == '404'
    raise  "Tweet not found: #{tweet_id}"
  end

  # Parse the JSON response
  tweet = JSON.parse(response.body)

  # Return the tweet object
  tweet
end

In this code, the fetch_tweet method checks the status code of the response from the Twitter API. If the status code is 404, it means that the tweet does not exist, and the method raises an error with the message “Tweet not found: #{tweet_id}”. If the tweet exists, the method parses the JSON response and returns the tweet object as before.

This is fine for when the tweet does not exist. However, there could be other reasons why the response is not correct, such as an unauthenticated user, or the API being down. I ask the AI to check that.

Ivan: That’s is correct, but it only checks that the tweet doesn’t exists. There are any other reasons because the API can returns a response that is not 200, like unauthorised. Can you raise an error if response is not OK?

ChatGPT: Yes, it is possible to raise an error if the response from the Twitter API is not 200 by checking the status code of the response. If the response has a different status code, it means that there was an error and you should raise an error with the appropriate message.

Here is an example of how you might update the fetch_tweet method to raise an error if the response is not 200:

def fetch_tweet tweet_id
  # Construct the URL for the Twitter API request
  tweet_url = "https://api.twitter.com/2/tweets/#{tweet_id}"
  uri = URI.parse(tweet_url)

  # Create the HTTP request and set the necessary headers
  request = Net::HTTP::Get.new(uri)
  request['Authorization'] = "Bearer #{ENV['TWITTER_BEARER_TOKEN']}"
  request['Accept'] = 'application/json'

  # Make the HTTP request and retrieve the response
  response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    http.request(request)
  end
  
  # Check the status code of the response 
  if response.code != '200'
    if response.code == '404'
      raise "Error: Tweet not found with ID #{tweet_id}"
    else
      raise "Error: #{response.code} #{response.message}"
    end
  end

  # Parse the JSON response
  tweet = JSON.parse(response.body)

  # Return the tweet object
  tweet
end

Now it checks that the response is definitely 200, and not just that it is not 404. Also, keeping the previous context, it has maintained the case where it could be 404. Bravo!

At this point, it seems there is an Asciidoctor extension that could work. There are a few more details to polish, such as the fact that the template generating the HTML expects an object like the one provided by the TwitterClient.fetch_tweet method we saw at the beginning, instead of a Hash returned by JSON.parse. I tried to get it to fix this, but every change it added broke it even more, and I had to fix that myself.

It also failed to add the necessary imports so it could find the JSON, Net::HTTP, and other classes. The check that the tweet ID was an integer did not work either. After fixing all these little details, some by myself, others with the help of the AI, I now have a fully functional Asciidoctor extension.

The extension code is available in a repository I created on GitHub.

Conclusions

Although I had to make some manual changes, the AI did most of the work. I know nothing about Ruby, and doing something like this would have taken me several hours or even days, between learning Ruby syntax, looking up how to make HTTP calls, how to parse JSON, how to check a regular expression, etc.

Instead, by instructing the AI and making a few minor changes myself, I achieved my goal in one hour.

The amount of time and work this technology saves today is unprecedented.

Comments

Loading comments...

Write a comment on GitHub!