El contenido de este blog se escribe en Markdown, pero hace tiempo que llevo dándole vueltas a la idea de cambiar a un formato igualmente ligero, pero algo más potente. Entre otras cosas, me interesa que se trate de un formato extensible. Mi amigo Alejandro Quirós me recomendó que el echase un vistazo a Asciidoc, por lo que he estado durante un tiempo viéndolo y probándolo localmente.
Asciidoc parece tener todo lo que necesito, es un formato de texto ligero que, por defecto, contiene todos los tipos de elemento de Markdown y algunos más, por ejemplo, los admonitions, que permiten crear cajas de advertencia o tip.
Echando un ojo atrás a mis entradas, me he dado cuenta que en algunas de ellas hago comentarios sobre diferentes tweets, por lo que he pensado que me vendría bien la capacidad de poder insertar un tweet dentro de una entrada. Asciidoc por defecto no permite insertar un tweet, pero al ser extensible, se puede crear un plugin para ello.
El software que transforma documentos Asciidoc en documentos HTML se llama Asciidoctor y, está escrito en Ruby. Las extensiones para Asciidoctor por lo tanto se escriben en Ruby.
ChatGPT
Salvo que hayas estado viviendo en una cueva los últimos días, sabrás que es ChatGPT, una inteligencia artificial conversational capaz de tener conversaciones a través de un chat.
ChatGPT es capaz de mantener todo tipo de conversaciones manteniendo el contexto con los mensajes anteriores y, si se le pide, es capaz de programar.
Dado que yo no tengo experiencia alguna con Ruby, he pensado en pedirle a ChatGPT que se encargue de programar dicha extensión por mí. Tras intercambiar una larga lista de mensajes pidiendole ciertos cambios, avisandole de errores que me ocurrían, y hacer algunos cambios a mano sobre el código generado, la IA ha conseguido escribir un plugin para Asciidoctor completamente funcional para insertar tweets en un documento.
La conversación con ChatGPT
Lo primero que he hecho es pedirle en líneas generales lo que necesito, y esto es lo que ha ocurrido:
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 endOnce 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.
Por lo que podemos ver, en este código ocurren tres cosas:
- Utilizando el API de Twitter para Ruby, obtiene el contenido del tweet.
- Le pasa el tweet a una función para que lo transforme en HTML.
- Crea un bloque de Asciidoc con el contenido HTML.
Analicemos el código un poco. Una de las primeras cosas que hace es pasarle el ID directamente al API de Twitter, pero no comprueba que sea válido (númerico), así que se lo hago saber a la IA.
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 theid
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 endThis 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.
Yo esperaba que validase que sea numérico pero, sorprendentemente, hace uso del API de Twitter para comprobar si existe. Aunque es una respuesta correcta, esto conlleva una llamada extra al API, que tiene un límite de peticiones. Lo ideal para mi es que la ejecución del script utilice únicamente una llamada y no varias. Se lo comento y le soy más específico con la validación que necesito.
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 theprocess
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 endThis code will check if the
tweet_id
variable contains a numeric value using theNumeric
class, and raise an error if it does not. This will ensure that the tweet ID is valid without making an extra API call.
Como no tengo experiencia progrmando con Ruby, desconozco que hace el método is_a?
, por lo que le pregunto directamente.
Ivan:
What is_a? method returns?
ChatGPT:
Theis_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" endThis will output
x is a string
to the console, because thex
variable is an instance of theString
class.On the other hand, if you have the following code:
x = 3 if x.is_a?(String) puts "x is a string" endThis will not output anything to the console, because the
x
variable is not an instance of theString
class (it is an instance of theFixnum
class, which is a descendant of theNumeric
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.
Tal como ha dicho, equivaldría al operador instanceof
de Java o is
de C#. Debemos asumir que Asciidoctor enviará el ID como un string (así me encargaré yo mismo cuando cree los documentos Asciidoc), por lo que le informo de ello.
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 thetweet_id
variable in theprocess
method is always a string, even if it contains a numeric value. To check if this string contains an integer value, you can use theString#to_i
method, which converts the string to an integer if it contains a valid integer value, or returns0
if it does not.Here is an example of how you might update the
process
method to use theString#to_i
method to check if thetweet_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 endIn this code, the
tweet_id.to_i != 0
expression will check if thetweet_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.
La IA ha entenido perfectamente lo que le he querido decir y corrige de nuevo el script, además me explica como funciona. ¡Perfecto!
Nótese que en cada cambio que está haciendo, además está actualizando los comentarios para que estos no queden desactualizados. ¡Un 10!
Una vez hecha la validación del tweet, vamos a continuar. Le voy a pedir que implemente el método generate_tweet_html
que me había dejado únicamente con un TODO.
Ivan:
Please, implement generate_tweet_html method.
ChatGPT:
To implement thegenerate_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 endIn this code, the
generate_tweet_html
method takes atweet
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 theprocess
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 theprocess
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 endThis 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.
Aquí ocurren varias cosas. La primera de ella, es que dentro de este método, está usando el API de Twitter para obtener de nuevo los datos que ya conocía previamente, pues son los que les viene por parámetro. Yo mismo me encargo de borrar la primera línea.
También se puede observar, la IA me ha devuelto de nuevo el contenido del método process
sin ningún cambio. Esto es una constante lo largo de toda la conversación, por lo que a partir de aquí, obmitiré esa parte de sus respuestas.
Mi siguiente problema es que para hacer uso del API de Twitter para Ruby, este require instalarlo a través de una gema. Cargar gemas desde Asciidoc, aunque es posible, es relativamente complicado, por lo que le pido a la IA que por favor lo reemplace haciendo directamente llamadas HTTP al API de Twitter.
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 theNet::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 endIn this code, the
fetch_tweet
method takes atweet_id
as an argument and uses theNet::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 endThis 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.
Como se puede ver, no sólo me ha implementado método fetch_tweet, si no que además me ha modificado los métodos generate_tweet_html y process para que hagan uso de estos.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
Aquí no sólo me ha implementado un método para obtener el contenido del tweet, si no que además ha modificado el método generate_tweet_html
para que lo llame, y ha eliminado esta llamada del método process
. No soy capaz de entender a qué se debe ese cambio en la organización del código, pero no me preocupa y sigo con lo mío.
Este método que ha implementado utiliza el API 1.1 de Twitter. Para acceder al API 1.1 necesitas hacer una petición formal a Twitter y pedir que te habiliten el acceso. Mi única opción es hacer uso del API 2.0 de Twitter, por lo que se lo comento. Dicho y hecho.
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 thefetch_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 endIn 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.
Como se puede observar, en el header de autorización me pone un string que yo debería de reemplazar por mi bearer token. Le pido que lo obtenga directamente desde una variable de entorno. Otra vez más, dicho y hecho.
Ivan:
Can you replace 'Bearer <YOUR_BEARER_TOKEN>' 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 thefetch_tweet
method. To do this, you can use theENV
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 endIn this code, the
fetch_tweet
method uses theENV
global variable to access the value of theTWITTER_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.
Ahora le pido a la IA que modifique el método fetch_tweet para que lance un error si no existe.
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 a404
status code and an error message in the body. You can check for this response in thefetch_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 endIn this code, the
fetch_tweet
method checks the status code of the response from the Twitter API. If the status code is404
, 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.
Esto está bien para cuando el tweet no existe, sin embargo podría haber otros motivos por los que la respuesta no sea correcta, como usuario no autenticado, o API caída. Le pido a la IA que lo compruebe.
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 not200
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 not200
: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
Ahora comprueba que el la respuesta sea sí o sí 200, y no sólo que no sea 404. Además, manteniendo el contexto anterior, ha mantenido el caso de que pueda ser 404. ¡Bravo!
Llegados a este punto, parece que hay una extensión de Asciidoctor que podría funcionar. Hay algunos detalles más que pulir, como por ejemplo que la plantilla que genera el HTML espera un objeto como el que proporciona el método TwitterClient.fetch_tweet
que hemos visto al principio, en vez de un Hash
que devuelve el método JSON.parse
. He intentado que lo arregle, pero cada cambio que añadía la rompía más, y eso me ha tocado arreglarlo a mí.
También le ha faltado añadir los imports necesarios para que pueda encontrar las clases JSON
, Net::HTTP
entre otras. La comprobación de que el ID del tweet sea un entero tampoco funcionaba. Tras arreglar todos estos detallitos, algunos por mí mismo, otros con ayuda de la IA, tengo una extensión para Asciidoctor totalmente funcional.
El código de la extensión está disponible en un repositorio que he creado en GitHub.
Conclusiones
Aunque he tenido que hacer algunos cambios a mano, la IA ha hecho la mayor parte del trabajo. Yo no sé nada de Ruby y hacer algo así me habría costado varias horas o incluso días, entre aprender la sintaxis de Ruby, buscar como se hacen llamadas HTTP, como se parsea un JSON, como se comprueba una expresión regular, etc.
En cambio, ordenando a la IA y, con algunas modificaciones menores propias, he conseguido mi objetivo en una hora.
El ahorro de tiempo y trabajo que esta tecnología supone hoy en día es algo que no tiene precedentes.
Hay 1 comentarios en esta entrada. Pulsa aquí para comentar desde GitHub.
Muy buen artículo, ¿Estamos a cada vez más cerca de la singularidad tecnológica?
Hola. Lo primero de todo, muchas gracias por comentar.
Es difícil concluir si estamos cerca de la singularidad tecnológica, pero desde luego, aún no hemos llegado.
Yo con ChatGPT he hecho cosas increíbles, cientos de experimentos que han arrojado resultados sorprendentes, y que tengo intención de recopilar en un vídeo o en un hilo de Twitter. Desde luego, me atrevo a afirmar que es uno de los mayores avances tecnológicos que la humanidad ha logrado jamás. Esta tecnología ya ha empezado a cambiar el mundo tal y como lo conocemos, y lo que documento que logra hacer en esta misma entrada de blog es una prueba de ello.
Sin embargo, hay veces que la IA falla al razonar. No es que pueda dar información falsa o inexacta, pero bien razonada. Estoy hablando de casos en los que directamente no razona correctamente. Vamos a ver algunos ejemplos.
El primer ejemplo, hay veces que parece confundir cosas básicas como el significado de las palabras "sí" y "no".
En el segundo ejemplo, una amiga le pidió que escribiese un correo electrónico para presentar su agencia de marketing digital a los restaurantes. Esta fue la conversación.
Aquí hay dos problemas que cualquier persona con un mínimo de capacidad intelectual vería rápido. El primero y más obvio es que a pesar de que le ha dicho en su mensaje que la agencia de llama (nombre censurado), en la firma del mensaje en la última palabra, la IA parecía no conocer le nombre de la agencia y ha escrito "[Nombre de la agencia]". Esto me sorprende porque cuando le pedí que escribiese una entrada para este blog, sí fue capaz de firmarlo correctamente.
El segundo problema quizás sea menos obvio, pero la IA no parece saber qué es un restaurante. El mensaje está redactado para ser dirigido a una empresa que vende productos, como puede ser una tienda. En vez de "aumentar tus ventas", debería de escribir "aumentar la afluencia personas". Es cierto que el restaurante vende, pero suena raro. Lo normal es decir que la gente va al restaurante.
En conclusión.
Aunque la IA es impresionante, e incluso puede razonar correctamente, hay veces que falla al hacer razonamientos básicos, lo que indica que, al menos hoy en día, no hemos llegado a la singularidad tecnológica.