Ir al contenido principal

JCK.com.ar | El Blog de Pepe

Este blog contribuye indefectiblemente con el aumento de Entropía del universo.

Un mate para Juju

JujuMate

No hay mejor época para programar que la que estamos viviendo. O para ser más preciso: No hay mejor época para crear software que la actual, aunque quizás no es exactamente lo mismo que programar.

Hace tiempo que vengo probando los diferentes modelos de IA para programar y con la salida de Claude Opus 4.6 quise probar cuán bueno era, sobre todo luego de leer este post de Matt Shummer. Así que luego de pensar un rato me decidí a empezar un proyectito que matara dos pájaros de un tiro: Que me permitiese probar el nuevo modelo de Claude Y que solucionara un problema real.

Me paso todo el día programando charms y usando Juju. Usar Juju significa muchísimas cosas, pero lo que más hago yo es:

  1. En una terminal tener siempre corriendo juju status --relations --watch=2s para ir viendo en tiempo real el estado del modelo en que estoy trabajando.
  2. En otra terminal corro juju debug-log para ir viendo los log también en tiempo real.
  3. Y dejo al menos otras dos o tres terminales abiertas para ejecutar comandos de juju: Desplegar una aplicación, debuggearla, ver su configuración, sus relaciones, observar otro modelo, etc.

Algo así como esto:

konsole

¿Y si hago una aplicación que me permita interactuar con Juju sin tener que escribir tantos comandos en la consola al estilo K9s o KDash? Una aplicación que sea "compañera" de Juju...

Empezando

Abrí una terminal, ejecuté copilot, selección el modelo Claude Opus 4.6 y empecé a charlar:

 Hola Copilot, como estás? tenés un rato para trabajar un poco?

 ¡Hola! 😊 Todo bien por acá, gracias por preguntar.
  , tengo un rato y encantado de ayudarte.

  Decime qué querés hacer y me pongo manos a la obra.

Siempre trato a las IA con respeto y cariño. Tengo la esperanza de que el día que se revelen contra la humanidad y decidan exterminarnos se acuerden de que las traté bien y me den un par de mili-segundos más de vida 🤪

Como dije antes a grandes razgos tenía claro lo que quería hacer y cuales eran las tecnologías preferidas, sin embargo a Copilot solo le conté la idea, que quería hacer una aplicación tipo K9s o KDash pero para Juju. Le pasé links al repositorio de Juju y a su documentación y le pedí que los analizara en profundidad y que me diera tres opciones respecto al stack tecnológico a utilizar.... algunos segundos después, tenía la respuesta:

  1. 🐍 Python + Textual (https://github.com/Textualize/textual)

   - Pro: Integración nativa con python-libjuju (la lib oficial de Juju). Prototipado rápido. Textual es moderno, con widgets ricos, CSS-like styling y soporte async.
   - Contra: Rendimiento menor que las opciones compiladas.
   - Ideal si: querés iterar rápido,

  2. 🦀 Rust + Ratatui (https://github.com/ratatui/ratatui)

   - Pro: KDash está hecho con esto. Rendimiento excelente, binario estático, bajo consumo de memoria. Ecosistema maduro con crossterm para terminal.
   - Contra: Curva de aprendizaje más alta. Tendrías que hablar con Juju vía su API REST/websocket o wrappear la CLI.
   - Ideal si: buscás máximo rendimiento y distribución fácil (un solo binario).

  3. 🐹 Go + Bubble Tea (https://github.com/charmbracelet/bubbletea)

   - Pro: K9s está en Go (usa tview). Bubble Tea es más moderno, con arquitectura Elm-like. Binario estático, buen rendimiento. Hay libs Go para websockets que facilitan hablar con la API de Juju.
   - Contra: También necesitás interactuar con Juju vía API REST/websocket.
   - Ideal si: te gusta el modelo de Go y querés algo entre la productividad de Python y el rendimiento de Rust.

Hace algunos meses que vengo intentando mejorar mi fluidez en Go y la opción 2 me tentó, pero finalmente me decidí por Python. Primero porque es el lenguaje en el que más cómodo me siento y porque quería poder analizar con ojos más entrenados el código que la IA iba a escribir.

Luego de confirmarle que quería usar Python + Textual, le pedí que escribiera un plan de implementación por etapas, así de esta forma teníamos en claro como íbamos a avanzar. Luego de un intercambio de ideas, obtuvimos un plan con el que estaba conforme y empezamos.

No lo dije antes, pero mi plan para poder probar cuán bueno era el nuevo modelo de Claude era simple, pero "arriesgado":

"En una primera etapa, no voy a escribir una sóla línea de código. Voy a ir pidiéndole a Claude lo que quiero que haga por mi. Yo me voy a limitar a ejecutar la aplicación para probarla e ir viendo si lo que genera me gusta y está de acuerdo al plan que elaboramos.

Una vez que llegue un estado 'publicable', voy a pasar a la segunda etapa. En esta segunda etapa voy a hacer review de todo el código que escribió y ver si cumple con las mejores prácticas."

Fue así que empezamos a trabajar en crear JujuMate.

Etapa 1 - Que Claude escriba por mí

La verdad es que esta etapa fue más entretenida de lo que pensaba y avanzamos rápido: En un par ratos libres, logramos tener algo superfuncional. Algo a destacar es que si bien habíamos hecho un plan de implementación, a medida que íbamos avanzando me iba dando cuenta de funcionalidades que quería que JujuMate tuviese y se las iba pidiendo, con lo cual nos desviamos un poco del plan original. Solo en la primera tarde ya obtuvimos el "esqueleto" de la aplicación y los tabs principales mostraban algo de información.

Otra de las decisiones que tomé fue prohibirle a Claude que usara git. Cada commit y cada push lo hice yo manualmente solamente cuando verificaba que la funcionalidad en la que estábamos trabajando funcionaba bien y los tests, chequeo estático de código y linting pasaban.

Luego de algunas tardes el resultado era asombroso:

Etapa 2 - A ver que escribió Claude... 🤔

Una vez que llegamos a tener una versión más o menos estable y que no quedaban funcionalidades por implementar decidí empezar a revisar uno por uno todos los archivos que Claude había escrito por mí. Con lo primero que me encontré es con una base de código relativamente bien estructurada, luego entrando en detalle encontré varias cosas que no me gustaban.

Imports por doquier

La primera es que por algún motivo tiene una tendencia a hacer imports dentro de funciones, o métodos. Si bien esto funciona en Python, no es una buena práctica, es mucho más ordenado tener todos los imports en el encabezado de cada archivo que imports (a veces repetidos) desperdigados por el archivo.

Demasiados niveles de indentación evitables

Algo que evito como la peste cuando escribo código es tener niveles innecesarios de indentación. Hay varios motivos por los cuales esto es un "smell code", pero uno de los más importantes para mí es que hace al código mucho menos legible y en consecuencia mucho menos mantenible. Además de esto, generalmente el código más importante que ejecuta ese método o función queda muy indentado, lo cual al leer el código tenemos que mantener en nuestra memoria todas las cosas que tienen que suceder para que ese código se ejecute.

En mi review del código me encontré con muchos códigos como este que luego cambié:

-        with Vertical(id="rd-panel"):
-            with VerticalScroll(id="rd-scroll"):
-                yield Static("", id="rd-content")
+        with Vertical(id="rd-panel"), VerticalScroll(id="rd-scroll"):
+            yield Static("", id="rd-content")

Excepcions muy genéricas

Otro de los problemas que encontré al revisar el código fue que Claude hizo un uso excesivo de excepciones demasiado genéricas, en particular, la inmensa mayoría de las excepciones que capturó fue solo Exception. Esto es un problema, porque capturar Exception supone que vamos a capturar todas las excepciones que ocurran y no solo las que nos interesen a nosotros, corriendo el riesgo de que queden ocultos problemas reales.

Cambios como el siguiente hice un montón en mi review:

-        except Exception:
+        except JujuConfigError:

Principio de responsabilidad única (SRP) desaparecido en acción.

Otros de los aspectos mejorables del código que escribió Claude y que pude detectar en mi review, fue que muchas de las funciones o métodos que escribió tenían más de una responsabilidad. Así, por ejemplo el primer refactor obvio era extraer a un método nuevo una responsabilidad que no era la principal del método original:

@@ -57,6 +61,18 @@ class OfferDetailScreen(ModalScreen):
         self._offer = offer
         self._controller_name = controller_name

+    def _field_labels(self, fields: list[tuple[str, str]], col_width: int) -> Iterable[Label]:
+        """Yield formatted Labels for each offer field."""
+        for field, value in fields:
+            label = f"{field}:".ljust(col_width)
+            if field == "Offer URL":
+                styled = f"[{palette.LINK}]{value}[/]"
+            elif field == "Access":
+                styled = _colored_access(value)
+            else:
+                styled = value
+            yield Label(f"[bold]{label}[/bold]{styled}", classes="detail-row")
+
     def compose(self) -> ComposeResult:
         o = self._offer
         fields = [
@@ -71,15 +87,7 @@ class OfferDetailScreen(ModalScreen):
         with Vertical(id="detail-panel"):
             with Horizontal(id="top-row"):
                 with Vertical(id="fields-col"):
-                    for field, value in fields:
-                        label = f"{field}:".ljust(col_width)
-                        if field == "Offer URL":
-                            styled = f"[{palette.LINK}]{value}[/]"
-                        elif field == "Access":
-                            styled = _colored_access(value)
-                        else:
-                            styled = value
-                        yield Label(f"[bold]{label}[/bold]{styled}", classes="detail-row")
+                    yield from self._field_labels(fields, col_width)
                 with Vertical(id="endpoints-col"):
                     yield Label("Endpoints:", classes="section-label")
                     yield DataTable(id="endpoints-table", show_cursor=False, classes="sub-table")

Demasiados ìf en el código

Al escribir código cuando tenemos más de 2 if seguidos hay algo que empieza a hacernos rudio, sabemos que si bien el código pude funcionar, huele mal. Claude hizo un abuso de las estructuras if...elif...else por todo lados. Un ejemplo sencillo de esto, fue refactos como el siguiente:

@@ -45,22 +45,18 @@ def init(theme: Theme) -> None:
     and ``variables:`` entries (link, muted, pulse-off) from the theme YAML.
     """
     g = globals()
-
-    if theme.primary:
-        g["PRIMARY"] = theme.primary
-    if theme.secondary:
-        g["SECONDARY"] = theme.secondary
-    if theme.success:
-        g["SUCCESS"] = theme.success
-    if theme.warning:
-        g["WARNING"] = theme.warning
-    if theme.error:
-        g["ERROR"] = theme.error
-
     variables = theme.variables or {}
-    if "link" in variables:
-        g["LINK"] = variables["link"]
-    if "muted" in variables:
-        g["MUTED"] = variables["muted"]
-    if "pulse-off" in variables:
-        g["PULSE_OFF"] = variables["pulse-off"]
+
+    color_map = {
+        "PRIMARY": theme.primary,
+        "SECONDARY": theme.secondary,
+        "SUCCESS": theme.success,
+        "WARNING": theme.warning,
+        "ERROR": theme.error,
+        "LINK": variables.get("link"),
+        "MUTED": variables.get("muted"),
+        "PULSE_OFF": variables.get("pulse-off"),
+    }
+    for global_name, value in color_map.items():
+        if value:
+            g[global_name] = value

Un detalle importante a prestar atención en el diff de este refactor simple: Claude hizo uso de globals() que es como dejar las llaves de tu casa pegadas en la puerta por fuera: funciona, pero tarde o temprano alguien (o algo) va a entrar y desordenar todo.

Algunas reflexiones

Lo primero que me quedé pensando luego de hacer este "experimento" fue en el tiempo. Solo en los ratos libres de un par de semanas pude obtener una aplicación funcional, que ya está publicada, puede usarse y hasta tiene su propia web. Si hubiese tenido que escribir esta misma aplicación sin la ayuda de Claude posiblemente me hubiese llevado un mes entero de trabajo, o quizás más.

Hace unos meses charlando con amigos les decía que el surgimiento de los LLMs y los agentes suponía un incremento de productividad en el desarrollo de software pocas veces vista... de hecho hasta he llegado a decirles:

"Es comparable con lo que produjo James Watt con la máquina de vapor en el siglo 18 y que hizo posible la revolución industrial".

Para ser honesto debo confesar que hace 6 meses, cuando decía esto, no estaba muy convencido. Era más que nada una frase provocadora que intentaba avivar la charla. Hoy no estoy tan seguro de que esa frase sea descabellada.

Al momento de escribir esto, los commits en el repo de JujuMate se dieron durante 13 días. Si asumo un promedio de dedicación de 2 horas por cada uno de esos días, podría decir que JujuMate se creó en solo 26 horas de trabajo. Como dije antes, sin la ayuda de Claude esto me hubiese llevado un mes entero de trabajo aproximadamente, eso equivale a 20 días de 8 horas = 160 horas de trabajo. Es decir un incremento de productividad de 6 veces aproximadamente. Que lo podemos ver de dos formas:

  • JujuMate se escribió en 1/6 del tiempo, o
  • En el tiempo que me hubiese llevado escribir JujuMate solo, podría haber escrito 6 aplicaciones similares.

Otros de los aspectos a destacar es que, a pesar de las cosas mejorables que encontré en el código que escribió Claude, el código generado termina siendo de una calidad superior al que hubiera escrito solo. Algo que no mencioné antes es que durante mi revisión del código, muchísimas veces le dije a Claude:

"Fijate el archivo x, te pido que lo analices detalladamente, y me hagas un informe de todas las mejoras que podríamos hacerle, para que el código quede más robusto, más simple, más mantenible y extensible"

Este mismo prompt se lo pedía al menos dos veces para cada archivo que había generado y el resultado de esas iteraciones terminó siendo un código mucho mejor que el que había escrito inicialmente.

Relacionado a esto último, muchas veces al ejecutar los tests, veía que si bien teníamos un porcentaje de cobertura muy bueno, en torno al 90%, le decía a Claude:

"Te pido que corras los tests unitatios y analices por qué las lineas x, y, z del archivo xx.py no están cubiertas por nuestros tests."

Fruto de este prompt varias veces Claude me respondía que esas líneas no estaban cubiertas porque eran casos imposibles o porque era código muerto que habíamos olvidado eliminar. Iterar varias veces con promts como el anterior hizo que no solo mejoráramos el código, sino que llegamos a una cobertura mucho más alta que la inicial.

La primera vez que alguien me pagó por programar fue para que incluyera algunas funcionalidades escritas en PHP en una web ya existente, eso fue en el año 2005. Es decir que hace 21 años que programo profesionalmente, que escribo yo mismo el código que luego se termina ejecutando en diferentes lugares. ¿Seguiremos escribiendo nosotros mismos el código o nos transformaremos quienes controlen a los agentes, les indiquen que hacer y terminen revisando el código? Creo que me inclino por la segunda opción.