<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Christian Gonzales Komiya</title>
    <description>The latest articles on DEV Community by Christian Gonzales Komiya (@ckomiya).</description>
    <link>https://dev.to/ckomiya</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3976243%2F7ae6966c-9d81-4d32-ad97-3215224dc563.png</url>
      <title>DEV Community: Christian Gonzales Komiya</title>
      <link>https://dev.to/ckomiya</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ckomiya"/>
    <language>en</language>
    <item>
      <title>(Spanish) Las 8 expectativas de un Arquitecto de Software</title>
      <dc:creator>Christian Gonzales Komiya</dc:creator>
      <pubDate>Tue, 14 Jul 2026 22:32:20 +0000</pubDate>
      <link>https://dev.to/ckomiya/spanish-las-8-expectativas-de-un-arquitecto-de-software-1529</link>
      <guid>https://dev.to/ckomiya/spanish-las-8-expectativas-de-un-arquitecto-de-software-1529</guid>
      <description>&lt;p&gt;La figura del &lt;strong&gt;Arquitecto de Software&lt;/strong&gt; suele asociarse con alguien que toma decisiones técnicas importantes o diseña grandes sistemas. Sin embargo, el rol va mucho más allá de elegir tecnologías o dibujar diagramas.&lt;/p&gt;

&lt;p&gt;Más que preguntarnos &lt;strong&gt;qué es un arquitecto de software&lt;/strong&gt;, una mejor pregunta sería:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;¿Qué se espera realmente de un arquitecto de software?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A continuación comparto ocho expectativas fundamentales que todo arquitecto debería cumplir. Estas ideas están inspiradas en &lt;em&gt;Fundamentals of Software Architecture (2nd Edition)&lt;/em&gt; de Mark Richards y Neal Ford, y representan una excelente guía para quienes desean crecer profesionalmente hacia este rol.&lt;/p&gt;




&lt;h1&gt;
  
  
  1. 🏗️ Tomar decisiones arquitectónicas
&lt;/h1&gt;

&lt;p&gt;Un arquitecto debe definir las &lt;strong&gt;decisiones arquitectónicas&lt;/strong&gt; y los &lt;strong&gt;principios de diseño&lt;/strong&gt; que orientarán las decisiones tecnológicas del equipo, del departamento o incluso de toda la organización.&lt;/p&gt;

&lt;p&gt;La palabra clave aquí es &lt;strong&gt;orientar&lt;/strong&gt;, no &lt;strong&gt;imponer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Por ejemplo, decidir utilizar &lt;strong&gt;React&lt;/strong&gt; en un proyecto es una decisión técnica. En cambio, una decisión arquitectónica consiste en establecer que el frontend debe desarrollarse utilizando un &lt;strong&gt;framework reactivo&lt;/strong&gt;, permitiendo que el equipo elija entre Angular, React, Vue u otra alternativa adecuada.&lt;/p&gt;

&lt;p&gt;El trabajo del arquitecto consiste en proporcionar el marco de decisión para que los equipos puedan tomar buenas decisiones técnicas.&lt;/p&gt;

&lt;p&gt;Naturalmente, existen excepciones. Cuando una característica arquitectónica como la &lt;strong&gt;escalabilidad&lt;/strong&gt;, el &lt;strong&gt;rendimiento&lt;/strong&gt; o la &lt;strong&gt;disponibilidad&lt;/strong&gt; está en riesgo, el arquitecto puede necesitar imponer una tecnología o una restricción específica.&lt;/p&gt;




&lt;h1&gt;
  
  
  2. 🔍 Analizar continuamente la arquitectura
&lt;/h1&gt;

&lt;p&gt;Una arquitectura nunca está terminada.&lt;/p&gt;

&lt;p&gt;Con el tiempo cambian las necesidades del negocio, aparecen nuevas tecnologías y el software evoluciona. Una buena arquitectura debe evolucionar junto con él.&lt;/p&gt;

&lt;p&gt;También existe un fenómeno conocido como &lt;strong&gt;deterioro estructural&lt;/strong&gt; (&lt;em&gt;structural decay&lt;/em&gt;): pequeñas modificaciones realizadas por distintos desarrolladores terminan afectando características importantes como el rendimiento, la disponibilidad o la escalabilidad.&lt;/p&gt;

&lt;p&gt;El arquitecto debe evaluar continuamente si la arquitectura sigue siendo saludable y detectar oportunidades de mejora antes de que los problemas se vuelvan críticos.&lt;/p&gt;




&lt;h1&gt;
  
  
  3. 🚀 Mantenerse actualizado con las últimas tendencias
&lt;/h1&gt;

&lt;p&gt;Las decisiones arquitectónicas suelen tener impacto durante muchos años.&lt;/p&gt;

&lt;p&gt;Por ello, un arquitecto necesita mantenerse actualizado sobre nuevas tecnologías, herramientas y tendencias de la industria.&lt;/p&gt;

&lt;p&gt;Esto no significa perseguir todas las modas tecnológicas.&lt;/p&gt;

&lt;p&gt;Significa comprender cuáles realmente aportan valor y cuáles simplemente representan una tendencia pasajera.&lt;/p&gt;




&lt;h1&gt;
  
  
  4. ✅ Garantizar el cumplimiento de las decisiones arquitectónicas
&lt;/h1&gt;

&lt;p&gt;Diseñar una buena arquitectura no basta.&lt;/p&gt;

&lt;p&gt;También es necesario asegurarse de que los equipos realmente la implementen.&lt;/p&gt;

&lt;p&gt;Si cada desarrollador comienza a ignorar las reglas arquitectónicas, la arquitectura pierde coherencia y deja de ofrecer las características para las que fue diseñada.&lt;/p&gt;

&lt;p&gt;La arquitectura requiere &lt;strong&gt;gobernanza&lt;/strong&gt;, además de diseño.&lt;/p&gt;




&lt;h1&gt;
  
  
  5. 🌐 Comprender tecnologías diversas
&lt;/h1&gt;

&lt;p&gt;No se espera que un arquitecto sea experto en todas las tecnologías.&lt;/p&gt;

&lt;p&gt;Lo que sí se espera es que posea una visión amplia del ecosistema tecnológico.&lt;/p&gt;

&lt;p&gt;Debe conocer distintos lenguajes de programación, frameworks, plataformas, bases de datos, herramientas de integración y soluciones en la nube para poder seleccionar la alternativa más adecuada para cada contexto.&lt;/p&gt;

&lt;p&gt;En arquitectura suele ser más importante la &lt;strong&gt;amplitud del conocimiento&lt;/strong&gt; que la especialización extrema en una única tecnología.&lt;/p&gt;




&lt;h1&gt;
  
  
  6. 💼 Conocer el dominio del negocio
&lt;/h1&gt;

&lt;p&gt;La mejor solución técnica puede fracasar si no resuelve un problema de negocio.&lt;/p&gt;

&lt;p&gt;Un arquitecto necesita comprender:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;los objetivos del negocio;&lt;/li&gt;
&lt;li&gt;los procesos que soporta el sistema;&lt;/li&gt;
&lt;li&gt;las necesidades de los usuarios;&lt;/li&gt;
&lt;li&gt;las restricciones de la organización.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Su credibilidad no depende únicamente de su conocimiento técnico, sino también de su capacidad para hablar el lenguaje del negocio.&lt;/p&gt;




&lt;h1&gt;
  
  
  7. 🤝 Poseer habilidades interpersonales
&lt;/h1&gt;

&lt;p&gt;Una gran parte del trabajo de un arquitecto consiste en trabajar con personas.&lt;/p&gt;

&lt;p&gt;Debe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;liderar equipos;&lt;/li&gt;
&lt;li&gt;facilitar acuerdos;&lt;/li&gt;
&lt;li&gt;comunicar ideas complejas;&lt;/li&gt;
&lt;li&gt;resolver conflictos;&lt;/li&gt;
&lt;li&gt;formar y mentorizar desarrolladores.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Como decía Gerald Weinberg:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"No matter what they tell you, it's always a people problem."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;("No importa lo que te digan, al final siempre es un problema de personas").&lt;/p&gt;

&lt;p&gt;Las habilidades técnicas son indispensables, pero el liderazgo y la comunicación son igual de importantes.&lt;/p&gt;




&lt;h1&gt;
  
  
  8. 🧭 Comprender y desenvolverse en la política organizacional
&lt;/h1&gt;

&lt;p&gt;Las decisiones arquitectónicas casi siempre generan resistencia.&lt;/p&gt;

&lt;p&gt;Cada equipo tiene objetivos distintos.&lt;/p&gt;

&lt;p&gt;Cada área posee prioridades diferentes.&lt;/p&gt;

&lt;p&gt;Y cada decisión importante afecta a varias personas.&lt;/p&gt;

&lt;p&gt;Por ello, el arquitecto necesita desarrollar habilidades de:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;negociación;&lt;/li&gt;
&lt;li&gt;influencia;&lt;/li&gt;
&lt;li&gt;comunicación;&lt;/li&gt;
&lt;li&gt;construcción de consensos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Programar implica tomar decisiones sobre el código.&lt;/p&gt;

&lt;p&gt;Ser arquitecto implica tomar decisiones que afectan a toda una organización.&lt;/p&gt;

&lt;p&gt;Y eso requiere mucho más que conocimientos técnicos.&lt;/p&gt;




&lt;h1&gt;
  
  
  🎯 Reflexión final
&lt;/h1&gt;

&lt;p&gt;Muchas personas creen que convertirse en arquitecto significa dejar de programar.&lt;/p&gt;

&lt;p&gt;En realidad, significa ampliar el alcance de las decisiones que uno toma.&lt;/p&gt;

&lt;p&gt;Un arquitecto exitoso combina:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;conocimiento técnico;&lt;/li&gt;
&lt;li&gt;visión de negocio;&lt;/li&gt;
&lt;li&gt;liderazgo;&lt;/li&gt;
&lt;li&gt;comunicación;&lt;/li&gt;
&lt;li&gt;capacidad de negociación;&lt;/li&gt;
&lt;li&gt;aprendizaje continuo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La arquitectura de software no consiste únicamente en diseñar sistemas.&lt;/p&gt;

&lt;p&gt;Consiste en ayudar a que personas, equipos y tecnología trabajen juntos para construir soluciones que sigan siendo valiosas con el paso del tiempo.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 Referencia
&lt;/h2&gt;

&lt;p&gt;Este artículo está inspirado en los conceptos presentados en la siguiente obra:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Richards, M., &amp;amp; Ford, N. (2025). &lt;em&gt;Fundamentals of Software Architecture: A Modern Engineering Approach&lt;/em&gt; (2nd ed.). O'Reilly Media. ISBN: &lt;strong&gt;978-1-098-17551-1&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;El contenido de este artículo corresponde a una síntesis, interpretación y adaptación realizada por el autor con fines de aprendizaje y divulgación técnica. Para una explicación completa y en profundidad, se recomienda la lectura del libro.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✍️ Sobre este artículo
&lt;/h2&gt;

&lt;p&gt;Este artículo forma parte de mis notas de estudio sobre &lt;strong&gt;Arquitectura de Software&lt;/strong&gt;, donde comparto conceptos, aprendizajes y reflexiones obtenidas durante la lectura de libros especializados y mi experiencia profesional.&lt;/p&gt;

&lt;p&gt;Mi objetivo es convertir estas notas en contenido práctico que pueda servir tanto a otros ingenieros de software como a mí mismo como material de consulta en el futuro.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>career</category>
      <category>softwareengineering</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>(Spanish) Cómo piensa un agente ReAct: por qué el LLM no resuelve todo en una sola llamada</title>
      <dc:creator>Christian Gonzales Komiya</dc:creator>
      <pubDate>Mon, 13 Jul 2026 13:32:35 +0000</pubDate>
      <link>https://dev.to/ckomiya/spanish-como-piensa-un-agente-react-por-que-el-llm-no-resuelve-todo-en-una-sola-llamada-14fm</link>
      <guid>https://dev.to/ckomiya/spanish-como-piensa-un-agente-react-por-que-el-llm-no-resuelve-todo-en-una-sola-llamada-14fm</guid>
      <description>&lt;p&gt;Cuando empecé a jugar con LangGraph y un agente con tools (&lt;code&gt;add&lt;/code&gt;, &lt;code&gt;subtract&lt;/code&gt;, &lt;code&gt;multiply&lt;/code&gt;), me chocó algo que parecía contraintuitivo: si le pido al LLM &lt;code&gt;5 + 8/12&lt;/code&gt;, el modelo &lt;em&gt;no&lt;/em&gt; responde el resultado final de una. Hace una llamada, pide una tool, espera, hace otra llamada, pide otra tool, espera, y recién en una tercera llamada da la respuesta.&lt;/p&gt;

&lt;p&gt;Mi primera reacción fue pensar que el modelo era "limitado" o que solo podía resolver la parte para la que tenía una tool a la mano. La explicación real es más simple y más interesante: &lt;strong&gt;el modelo no puede escribir una tool call cuyo argumento sea un resultado que todavía no existe.&lt;/strong&gt; Este post explica esa idea con el ejemplo concreto que usé para entenderla.&lt;/p&gt;

&lt;h2&gt;
  
  
  El código base
&lt;/h2&gt;

&lt;p&gt;Un agente ReAct minimalista en LangGraph, con tres tools aritméticas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Herramienta de suma que agrega dos números.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;multiply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Función de multiplicación&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;

&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiply&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatDeepSeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deepseek-chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;model_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;system_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SystemMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Eres mi asistente de IA...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_continue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;last_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;last_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;continue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El grafo conecta &lt;code&gt;our_agent&lt;/code&gt; → &lt;code&gt;tools&lt;/code&gt; → &lt;code&gt;our_agent&lt;/code&gt; en bucle, hasta que &lt;code&gt;should_continue&lt;/code&gt; ya no encuentra &lt;code&gt;tool_calls&lt;/code&gt; en la última respuesta.&lt;/p&gt;

&lt;h2&gt;
  
  
  La confusión inicial: "resuelve solo una parte porque solo tiene esa tool"
&lt;/h2&gt;

&lt;p&gt;Con el input &lt;code&gt;5 + 8/12&lt;/code&gt;, uno esperaría que el modelo "vea" la expresión completa y la resuelva de un tiro. En cambio, pasa esto:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Llamada 1 → pide &lt;code&gt;divide(8, 12)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Llamada 2 → pide &lt;code&gt;add(5, 0.667)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Llamada 3 → responde en texto&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Da la impresión de que el modelo "hizo lo que pudo con la tool que tenía a mano, y no siguió". Pero esa lectura es engañosa. El límite no es la disponibilidad de tools — es la &lt;strong&gt;dependencia de datos&lt;/strong&gt; entre los dos pasos.&lt;/p&gt;

&lt;h2&gt;
  
  
  La razón real: no se puede prometer un resultado futuro
&lt;/h2&gt;

&lt;p&gt;Una tool call necesita valores concretos, no referencias simbólicas. El modelo no puede generar algo como:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"b"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"resultado_de_divide"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eso no es un valor válido en el esquema de tool calling. Solo puede escribir &lt;code&gt;add(5, 0.667)&lt;/code&gt; si &lt;code&gt;0.667&lt;/code&gt; ya existe como número real en el historial. Y ese número solo existe &lt;strong&gt;después&lt;/strong&gt; de que la tool &lt;code&gt;divide&lt;/code&gt; se ejecutó y su resultado volvió como respuesta.&lt;/p&gt;

&lt;p&gt;Por eso, en la primera llamada, lo único que el modelo puede pedir es la operación que &lt;strong&gt;ya tiene todos sus datos disponibles&lt;/strong&gt;: la división. No es que "decida" resolver solo esa parte — es la única parte que puede ejecutar con la información que tiene en ese instante.&lt;/p&gt;

&lt;h3&gt;
  
  
  Analogía: mandarle mensajes a un amigo con calculadora
&lt;/h3&gt;

&lt;p&gt;Imagina que sabes exactamente qué cuenta quieres hacer (5 + 8/12), pero no tienes calculadora — solo puedes escribirle a un amigo que sí tiene una, y esperar su respuesta antes de seguir.&lt;/p&gt;

&lt;p&gt;Le escribes: &lt;em&gt;"divide 8 entre 12"&lt;/em&gt;. Ya sabes que después vas a sumar 5 al resultado — pero no puedes escribir eso en el mismo mensaje, porque tu amigo no interpreta instrucciones encadenadas en lenguaje natural: solo ejecuta funciones con números concretos. Así que lo único que puedes mandar es la operación cuyos números ya tienes: la división. La suma queda para el siguiente mensaje, una vez que tengas la respuesta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prueba de que no es "una tool por llamada"
&lt;/h2&gt;

&lt;p&gt;Si el límite fuera "el modelo solo puede usar una tool por llamada", entonces con un input como:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Suma 4+2 y suma 7+8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Debería necesitar dos llamadas. Pero no es así — como ninguna de las dos sumas depende de la otra, el modelo pide &lt;strong&gt;ambas tool calls en la misma respuesta&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"b"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"b"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;ToolNode&lt;/code&gt; las ejecuta ambas sin volver a llamar al LLM entre medio. Total: &lt;strong&gt;2 llamadas al LLM&lt;/strong&gt;, no 3. Esto confirma que la regla real es &lt;em&gt;"no puedo pedir algo cuyo argumento depende de un resultado que aún no existe"&lt;/em&gt; — no una limitación arbitraria de cantidad de tools por turno.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué pasa cuando falta una tool
&lt;/h2&gt;

&lt;p&gt;Con &lt;code&gt;5 + 8/12&lt;/code&gt;, si el modelo tiene tanto &lt;code&gt;add&lt;/code&gt; como &lt;code&gt;divide&lt;/code&gt;, ambas operaciones las hace código Python con precisión garantizada, en 3 llamadas. Pero:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Si solo existe la tool &lt;code&gt;add&lt;/code&gt;&lt;/strong&gt; (no hay &lt;code&gt;divide&lt;/code&gt;): el modelo calcula la división mentalmente (como cualquier LLM sin tools, con su propio "cálculo interno"), y usa la tool solo para la suma. Total: 2 llamadas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Si solo existe la tool &lt;code&gt;divide&lt;/code&gt;&lt;/strong&gt; (no hay &lt;code&gt;add&lt;/code&gt;): el modelo pide la división como tool call, y luego calcula la suma mentalmente con el resultado. Total: 2 llamadas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El patrón se mantiene: el número de llamadas depende de cuántos pasos secuenciales &lt;em&gt;con dependencia de datos&lt;/em&gt; hay — no de cuántas tools existan en total. Cuando falta una tool para un paso, el modelo simplemente resuelve ese paso con su propio razonamiento en lugar de con código, y sigue adelante.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que el LLM sabe y lo que no sabe desde la primera llamada
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sí sabe, desde el inicio:&lt;/strong&gt; qué tools tiene disponibles y sus esquemas (nombre, descripción, parámetros). Eso se manda en &lt;strong&gt;cada&lt;/strong&gt; llamada — es literalmente lo que hace &lt;code&gt;bind_tools(tools)&lt;/code&gt;: adjuntar esa lista de forma fija a cada invocación.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No sabe, ni falta que sepa:&lt;/strong&gt; cuántas llamadas totales se van a necesitar. El modelo no planea "esto me va a tomar 3 vueltas" — es reactivo, no planificador de antemano. En cada llamada individual mira el historial actual y decide un solo paso siguiente: pedir tool(s) o responder en texto. Quien determina que &lt;em&gt;habrá&lt;/em&gt; otra llamada es LangGraph (&lt;code&gt;should_continue&lt;/code&gt; + el edge &lt;code&gt;tools → our_agent&lt;/code&gt;), no el modelo.&lt;/p&gt;

&lt;h2&gt;
  
  
  El input y output real de cada llamada
&lt;/h2&gt;

&lt;p&gt;Un punto clave: &lt;strong&gt;el LLM nunca ve clases de Python&lt;/strong&gt; como &lt;code&gt;AIMessage&lt;/code&gt; o &lt;code&gt;ToolMessage&lt;/code&gt;. Esas son abstracciones de LangChain. Lo que realmente viaja a la API es JSON con un campo &lt;code&gt;role&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Clase de LangChain&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;role&lt;/code&gt; real&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SystemMessage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;system&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HumanMessage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;user&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AIMessage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;assistant&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ToolMessage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tool&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Así se ve la secuencia completa para &lt;code&gt;5 + 8/12&lt;/code&gt; (con tools &lt;code&gt;add&lt;/code&gt; y &lt;code&gt;divide&lt;/code&gt;):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Llamada 1 — input:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Eres mi asistente de IA..."&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5 + 8/12"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Llamada 1 — output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"divide"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 8, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;b&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 12}"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Llamada 2 — input&lt;/strong&gt; (todo lo anterior + el resultado de la tool):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Eres mi asistente de IA..."&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5 + 8/12"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"divide"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 8, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;b&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 12}"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_call_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.6666666666666666"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Llamada 2 — output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 5, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;b&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 0.6666666666666666}"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Llamada 3 — input&lt;/strong&gt; (historial completo hasta ahora):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Eres mi asistente de IA..."&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5 + 8/12"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"divide"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 8, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;b&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 12}"&lt;/span&gt;&lt;span class="p"&gt;}}]},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_call_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.6666666666666666"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 5, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;b&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 0.6666666666666666}"&lt;/span&gt;&lt;span class="p"&gt;}}]},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_call_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5.666666666666667"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Llamada 3 — output&lt;/strong&gt; (ya sin &lt;code&gt;tool_calls&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5 + 8/12 = 5.666666666666667 (aproximadamente 5.67)."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nota clave: el &lt;code&gt;tool_call_id&lt;/code&gt; (&lt;code&gt;call_001&lt;/code&gt;, &lt;code&gt;call_002&lt;/code&gt;) es el hilo que conecta cada tool call con su respuesta correspondiente — así el modelo sabe cuál &lt;code&gt;ToolMessage&lt;/code&gt; responde a cuál petición suya.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output del modelo vs. input acumulado: quién arma el historial
&lt;/h2&gt;

&lt;p&gt;Algo que vale la pena separar bien: &lt;strong&gt;el output del LLM es solo el mensaje nuevo&lt;/strong&gt;, no el historial repetido. &lt;code&gt;model.invoke(...)&lt;/code&gt; te devuelve únicamente el &lt;code&gt;assistant&lt;/code&gt; que acaba de generar.&lt;/p&gt;

&lt;p&gt;Quien acumula todo es LangGraph, gracias al reducer definido en el estado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BaseMessage&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;add_messages&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cuando el nodo hace &lt;code&gt;return {"messages": [response]}&lt;/code&gt;, el reducer &lt;code&gt;add_messages&lt;/code&gt; no sobreescribe &lt;code&gt;state["messages"]&lt;/code&gt; — hace un &lt;strong&gt;append&lt;/strong&gt;. Así, para la siguiente llamada, &lt;code&gt;state["messages"]&lt;/code&gt; ya contiene todo lo acumulado hasta ese momento, y &lt;code&gt;model_call&lt;/code&gt; vuelve a mandar la lista completa (&lt;code&gt;[system_prompt] + state["messages"]&lt;/code&gt;) en cada invocación.&lt;/p&gt;

&lt;p&gt;Es la diferencia entre "el modelo te contesta una carta" y "tú guardas copia de cada carta en una carpeta, y le mandas la carpeta completa junto con la carta nueva la próxima vez".&lt;/p&gt;

&lt;h2&gt;
  
  
  Resumen: el patrón completo
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;El modelo recibe todo el historial acumulado hasta ese punto (nunca recuerda nada por sí mismo — no tiene memoria entre llamadas).&lt;/li&gt;
&lt;li&gt;Con esa información, decide &lt;strong&gt;un solo paso&lt;/strong&gt;: pedir tool(s) para las que ya tiene todos los datos concretos, o responder en texto si no falta nada.&lt;/li&gt;
&lt;li&gt;Cada tool call se ejecuta como código Python normal (&lt;code&gt;ToolNode&lt;/code&gt;), sin costo de LLM.&lt;/li&gt;
&lt;li&gt;LangGraph vuelve a invocar al modelo con el historial actualizado — y el ciclo se repite hasta que la respuesta ya no trae &lt;code&gt;tool_calls&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;El número de llamadas al LLM depende de cuántos pasos secuenciales con &lt;strong&gt;dependencia de datos&lt;/strong&gt; tiene la tarea — no de cuántas tools existan en total. Tools independientes se piden en paralelo, en una sola llamada.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cada llamada al LLM es una petición HTTP real, facturable en tokens, y el costo de entrada crece en cada vuelta porque se reenvía todo el historial acumulado — algo a tener en cuenta al diseñar agentes con muchos pasos encadenados.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>(Spanish) Dash: Monitoreo del Fenómeno El Niño</title>
      <dc:creator>Christian Gonzales Komiya</dc:creator>
      <pubDate>Thu, 09 Jul 2026 21:50:19 +0000</pubDate>
      <link>https://dev.to/ckomiya/dash-monitoreo-del-fenomeno-el-nino-2fd6</link>
      <guid>https://dev.to/ckomiya/dash-monitoreo-del-fenomeno-el-nino-2fd6</guid>
      <description>&lt;h1&gt;
  
  
  Dash: Monitoreo del Fenómeno El Niño
&lt;/h1&gt;

&lt;p&gt;Hace unas semanas quería construir un dashboard rápido para visualizar datos, y en vez de ir directo a mis herramientas de siempre (Streamlit, o armar un frontend con React), decidí probar &lt;strong&gt;Dash&lt;/strong&gt;. El resultado terminó siendo un proyecto que combina dos cosas que me interesan bastante: la simplicidad de construir dashboards en Python, y un tema que como peruano me toca de cerca — el Fenómeno El Niño.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué es Dash?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dash.plotly.com/" rel="noopener noreferrer"&gt;Dash&lt;/a&gt; es un framework de Python (de la gente de Plotly) para construir aplicaciones web de datos sin escribir una sola línea de JavaScript. Bajo el capó corre sobre Flask, React y Plotly.js, pero todo eso queda oculto: tú escribes Python, defines un &lt;code&gt;layout&lt;/code&gt; con componentes HTML y gráficos, y conectas la interactividad con &lt;strong&gt;callbacks&lt;/strong&gt; (funciones normales de Python decoradas con &lt;code&gt;@app.callback&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Lo que más me sorprendió:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Es sorprendentemente sencillo de aplicar.&lt;/strong&gt; Si ya sabes armar un gráfico con Plotly, ya sabes el 70% de Dash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Se ve increíble desde el día uno.&lt;/strong&gt; Los gráficos de Plotly ya traen zoom, pan, range sliders y tooltips interactivos gratis — sin escribir CSS ni JS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Todo vive en un solo lenguaje.&lt;/strong&gt; No hay que saltar entre Python para el backend y JavaScript para la interactividad del frontend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Con esa primera impresión, busqué un dataset con el que valiera la pena practicar. Y ahí es donde entra el segundo interés.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué El Niño
&lt;/h2&gt;

&lt;p&gt;Como peruano, El Fenómeno El Niño no es un tema abstracto de oceanografía: es lluvias e inundaciones en el norte del país, sequías en otras zonas, impacto directo en la pesca (nuestra anchoveta se mueve o desaparece), y en los casos más fuertes, desastres naturales que golpean carreteras, cultivos y ciudades enteras. Cada cierto tiempo el país entero está pendiente de si "viene El Niño" o no, y de qué tan fuerte va a ser.&lt;/p&gt;

&lt;p&gt;Quería entender mejor cómo se mide esto oficialmente, y construir algo que me permitiera ver la evolución del fenómeno en el tiempo, no solo el titular de la noticia del día.&lt;/p&gt;

&lt;h2&gt;
  
  
  ICEN y RONI: dos formas de medir el mismo fenómeno
&lt;/h2&gt;

&lt;p&gt;Investigando, encontré que no existe un solo índice para "El Niño" — hay al menos dos que se complementan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ICEN (Índice Costero El Niño):&lt;/strong&gt; mide la anomalía de temperatura superficial del mar &lt;strong&gt;frente a la costa peruana&lt;/strong&gt; (región Niño 1+2). Lo calcula el &lt;strong&gt;IGP&lt;/strong&gt; (Instituto Geofísico del Perú), y refleja las condiciones oceánicas locales que afectan directamente el clima y la pesca en el país.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RONI (Relative Oceanic Niño Index):&lt;/strong&gt; mide la anomalía en el &lt;strong&gt;Pacífico central-oriental&lt;/strong&gt; (región Niño 3.4). Lo calcula &lt;strong&gt;NOAA CPC&lt;/strong&gt; (el organismo climático de EE.UU.) y es el estándar internacional para monitorear el ENOS (El Niño-Oscilación del Sur) a escala global.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;¿Por qué existen ambos si miden "lo mismo"? Porque el fenómeno puede manifestarse distinto según la zona: un evento puede verse fuerte en RONI (a nivel global) pero débil en ICEN (en la costa peruana), o viceversa. Perú necesita su propio índice porque las condiciones costeras locales no siempre siguen el patrón del Pacífico central al pie de la letra.&lt;/p&gt;

&lt;p&gt;Con ambos índices en un mismo gráfico, se puede comparar la "foto global" del ENOS con la "foto local" que de verdad le importa al país.&lt;/p&gt;

&lt;h2&gt;
  
  
  El proyecto: Vigía ENOS
&lt;/h2&gt;

&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/ckomiya/vigia-enos" rel="noopener noreferrer"&gt;github.com/ckomiya/vigia-enos&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Demo: &lt;strong&gt;&lt;a href="https://vigia-enos.onrender.com/" rel="noopener noreferrer"&gt;vigia-enos.onrender.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;La estructura del proyecto es deliberadamente simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;etl/
  fetch_icen.py     # descarga y parsea ICEN (IGP)
  fetch_roni.py     # descarga y parsea RONI (NOAA CPC)
  transform.py      # une ambas series, categoriza y detecta eventos RONI
app/
  main.py           # dashboard Dash (single page)
data/
  raw/              # snapshots crudos descargados
  processed/        # CSVs procesados (icen.csv, roni.csv, merged.csv)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La idea fue separar completamente la &lt;strong&gt;obtención de datos&lt;/strong&gt; (ETL) de la &lt;strong&gt;presentación&lt;/strong&gt; (Dash). El pipeline ETL son tres scripts que se corren en secuencia:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;venv&lt;span class="se"&gt;\S&lt;/span&gt;cripts&lt;span class="se"&gt;\p&lt;/span&gt;ython.exe etl&lt;span class="se"&gt;\f&lt;/span&gt;etch_icen.py
venv&lt;span class="se"&gt;\S&lt;/span&gt;cripts&lt;span class="se"&gt;\p&lt;/span&gt;ython.exe etl&lt;span class="se"&gt;\f&lt;/span&gt;etch_roni.py
venv&lt;span class="se"&gt;\S&lt;/span&gt;cripts&lt;span class="se"&gt;\p&lt;/span&gt;ython.exe etl&lt;span class="se"&gt;\t&lt;/span&gt;ransform.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fetch_icen.py&lt;/code&gt; y &lt;code&gt;fetch_roni.py&lt;/code&gt; descargan los datos crudos directamente de las fuentes oficiales (IGP y NOAA CPC respectivamente) y los guardan como snapshots en &lt;code&gt;data/raw/&lt;/code&gt;. &lt;code&gt;transform.py&lt;/code&gt; toma ambas series, las une por fecha, calcula la categorización (Neutro / Cálida / Fría, según los umbrales oficiales) y detecta eventos por persistencia. El resultado es un único &lt;code&gt;merged.csv&lt;/code&gt; que alimenta todo el dashboard.&lt;/p&gt;

&lt;p&gt;Y del lado de la app, &lt;strong&gt;todo el dashboard vive en un solo archivo&lt;/strong&gt;: &lt;code&gt;app/main.py&lt;/code&gt;. Nada de blueprints, nada de carpetas de componentes — solo funciones de Python que arman piezas de HTML y un callback que conecta el botón de actualizar con el pipeline ETL.&lt;/p&gt;

&lt;h2&gt;
  
  
  El gráfico principal: serie temporal con umbrales
&lt;/h2&gt;

&lt;p&gt;El corazón visual del dashboard es un gráfico de líneas con Plotly que muestra ICEN y RONI en el tiempo, más las líneas de umbral que definen cada categoría:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_timeseries_figure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;go&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Figure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;go&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Figure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_trace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;go&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Scatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;icen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ICEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lines&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COLOR_WARM&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_trace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;go&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Scatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roni&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RONI&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lines&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COLOR_COLD&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;icen_thresholds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ICEN_FRIA_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ICEN Fría&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ICEN_NEUTRA_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ICEN Neutra/Cálida déb.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ICEN_CALIDA_DEBIL_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ICEN Cálida déb./Cálida&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;icen_thresholds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_hline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line_dash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line_color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COLOR_WARM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opacity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;annotation_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;annotation_position&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top left&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;annotation_font_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;annotation_font_color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COLOR_TEXT_MUTED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpqf0773abon6j7ug6vju.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpqf0773abon6j7ug6vju.png" alt=" " width="799" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cada línea de umbral se agrega con &lt;code&gt;fig.add_hline()&lt;/code&gt; — una sola llamada te da la línea punteada, el color, y la anotación con el texto flotando encima. Esto es lo que quiero decir con "se ve increíble sin esfuerzo": no hace falta calcular posiciones de texto ni dibujar nada a mano, Plotly lo resuelve.&lt;/p&gt;

&lt;p&gt;También agrego un selector de rango temporal (botones de "1 año", "5 años", "10 años", "Todo") y un range slider debajo del gráfico, con una sola configuración:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_xaxes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;default_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_date&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;rangeselector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;buttons&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1 año&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;year&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stepmode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backward&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5 años&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;year&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stepmode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backward&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10 años&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;year&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stepmode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backward&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Todo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;rangeslider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;visible&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bgcolor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COLOR_PANEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bordercolor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COLOR_GRID&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esos botones y el slider son 100% interactivos desde el navegador, sin ningún callback de Dash detrás — vienen incluidos en el objeto &lt;code&gt;Figure&lt;/code&gt; de Plotly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los KPIs tipo semáforo
&lt;/h2&gt;

&lt;p&gt;Además del gráfico, quería una respuesta rápida a "¿cómo estamos ahora mismo?" — para eso están las dos tarjetas KPI, una para ICEN y otra para RONI, con un color de "semáforo" según el estado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SEMAFORO_COLOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;El Niño&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;COLOR_WARM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cálida&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;COLOR_WARM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cálida débil&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;COLOR_WARM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Neutro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;COLOR_TEXT_MUTED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Neutra&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;COLOR_TEXT_MUTED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;La Niña&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;COLOR_COLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fría&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;COLOR_COLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;kpi_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;titulo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fecha_label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Div&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SEMAFORO_COLOR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COLOR_TEXT_MUTED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;_kpi_card_style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;H4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;titulo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{...}),&lt;/span&gt;
            &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;H2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{...,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;color&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
            &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;P&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Valor: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fecha_label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{...}),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffo1fgsekrhrl73lu2np6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffo1fgsekrhrl73lu2np6.png" alt=" " width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Aquí es donde se ve claramente el estilo declarativo de Dash: &lt;code&gt;html.Div&lt;/code&gt;, &lt;code&gt;html.H2&lt;/code&gt;, &lt;code&gt;html.P&lt;/code&gt; son funciones de Python que devuelven el equivalente a las etiquetas HTML del mismo nombre, y el &lt;code&gt;style&lt;/code&gt; es simplemente un diccionario de Python que mapea 1:1 a CSS. No hay plantillas, ni Jinja, ni archivos &lt;code&gt;.html&lt;/code&gt; sueltos — todo es Python puro que se compone como cualquier otra función.&lt;/p&gt;

&lt;p&gt;La tarjeta de RONI tiene un pequeño extra: no solo muestra el valor puntual más reciente, sino que detecta si hay un &lt;strong&gt;evento activo por persistencia&lt;/strong&gt; (5 temporadas consecutivas cruzando el umbral de ±0.5, que es el criterio real que usa NOAA para declarar un evento El Niño/La Niña oficial, no solo un mes suelto):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_active_roni_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;roni_df&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;roni_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;year&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ref_month&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;reset_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iloc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evento_roni&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;evento&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evento_roni&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iloc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evento_roni&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;evento&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;inicio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iloc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;evento&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inicio&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;season&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inicio&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;year&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Es un &lt;code&gt;while&lt;/code&gt; simple que retrocede en el tiempo mientras la categoría del evento se mantenga igual, para encontrar desde cuándo empezó la racha actual. Nada sofisticado, pero le da al KPI un contexto real ("La Niña desde DJF 2024") en vez de solo un número aislado — esa fue una de las mejoras que pedí explícitamente al iterar sobre el dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  La parte reactiva: un solo callback
&lt;/h2&gt;

&lt;p&gt;Todo el dinamismo del dashboard se reduce a &lt;strong&gt;un callback&lt;/strong&gt;. El botón "Actualizar datos" dispara el pipeline ETL completo (los tres scripts de arriba) sin reiniciar el servidor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dashboard-content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;children&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ultima-actualizacion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;children&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actualizar-btn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n_clicks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;prevent_initial_call&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;actualizar_datos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_clicks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;run_etl_pipeline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;build_dashboard_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;get_last_update_label&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;mensaje&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error al actualizar (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;). Mostrando últimos datos disponibles.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;build_dashboard_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;mensaje&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esta es la esencia del modelo de Dash: declaras qué &lt;code&gt;Input&lt;/code&gt; dispara el callback (el número de clics del botón) y qué &lt;code&gt;Output&lt;/code&gt; va a actualizar (el contenido del dashboard y el texto de "última actualización"). Dash se encarga de todo el cableado — escuchar el evento en el navegador, mandar la petición al servidor, correr la función de Python, y actualizar el DOM con lo que la función devuelva. Yo nunca escribí una línea de JavaScript para lograr esto.&lt;/p&gt;

&lt;p&gt;El &lt;code&gt;try/except&lt;/code&gt; es a propósito defensivo: si las fuentes externas (IGP o NOAA) fallan o cambian de formato, el dashboard no se rompe — muestra un mensaje de error y sigue mostrando los últimos datos disponibles en vez de una pantalla en blanco.&lt;/p&gt;

&lt;p&gt;Y para que el estado de "última actualización" sobreviva incluso a un reinicio del servidor, en vez de guardarlo en una variable en memoria, lo derivo directamente del &lt;code&gt;mtime&lt;/code&gt; (fecha de modificación) del archivo &lt;code&gt;merged.csv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_last_update_label&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;MERGED_PATH&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sin datos aún&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;mtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromtimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MERGED_PATH&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_mtime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Última actualización: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%d/%m/%Y %H&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Es un detalle pequeño, pero es el tipo de decisión que evita bugs raros cuando el servidor se reinicia (por ejemplo, en un deploy) y de repente el estado en memoria ya no coincide con lo que realmente hay en disco. Fue justamente uno de los puntos donde Claude Code propuso este enfoque en vez de una variable global, al pedirle explícitamente que el estado sobreviviera a un reinicio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cerrando
&lt;/h2&gt;

&lt;p&gt;Lo que más rescato de esta experiencia con Dash es lo poco que hubo que pelear contra el framework para lograr algo que se ve profesional: un solo archivo &lt;code&gt;main.py&lt;/code&gt; de menos de 500 líneas contiene layout, estilos, y toda la interactividad de un dashboard con gráficos de serie temporal, zoom interactivo, KPIs dinámicos y actualización de datos en vivo. Y trabajando con Claude Code, ese "poco esfuerzo" se sintió todavía más real: pude enfocarme en decidir qué mostrar y cómo interpretarlo (los umbrales, la lógica de eventos, la UX del semáforo), mientras la implementación del layout y los callbacks avanzaba en paralelo.&lt;/p&gt;

&lt;p&gt;Si vienes del mundo Python y quieres construir dashboards de datos sin meterte a aprender un framework de frontend completo, Dash vale mucho la pena probarlo — con o sin un agente como Claude Code de por medio — y si de paso el tema es algo que te importa como país, mejor todavía.&lt;/p&gt;

&lt;p&gt;Repo completo, con el detalle de metodología de los umbrales (por qué ICEN usa valores fijos en vez de percentiles) en el &lt;code&gt;CLAUDE.md&lt;/code&gt;: &lt;strong&gt;&lt;a href="https://github.com/ckomiya/vigia-enos" rel="noopener noreferrer"&gt;github.com/ckomiya/vigia-enos&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>data</category>
      <category>python</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>(Spanish) El navegador ya no es solo para humanos</title>
      <dc:creator>Christian Gonzales Komiya</dc:creator>
      <pubDate>Thu, 25 Jun 2026 23:36:21 +0000</pubDate>
      <link>https://dev.to/ckomiya/el-navegador-ya-no-es-solo-para-humanos-1e6g</link>
      <guid>https://dev.to/ckomiya/el-navegador-ya-no-es-solo-para-humanos-1e6g</guid>
      <description>&lt;p&gt;&lt;em&gt;Por qué las páginas web públicas son ahora accesibles de formas que antes eran impensables&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Hubo un tiempo en que "ver algo en una web" y "usar ese dato en código" eran dos mundos separados. El primero era para humanos. El segundo requería que la empresa dueña del sitio te diera una API, documentación, credenciales, y su bendición.&lt;/p&gt;

&lt;p&gt;Ese tiempo terminó.&lt;/p&gt;

&lt;h2&gt;
  
  
  
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Lo que descubrí explorando
&lt;/h2&gt;

&lt;p&gt;Estaba analizando cómo obtener datos de una página web pública — sin registro, sin login, sin nada que el usuario promedio no pudiera ver con sus propios ojos. Información completamente pública.&lt;/p&gt;

&lt;p&gt;El camino obvio parecía ser el scraping tradicional: descargar el HTML, parsear el contenido. Pero la web moderna no funciona así. La mayoría de sitios hoy son &lt;strong&gt;Single Page Applications&lt;/strong&gt; — React, Angular, Vue — donde el HTML inicial está vacío y el contenido llega después, cargado dinámicamente por JavaScript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"root"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eso es todo lo que ves si intentas descargar el HTML directamente. Un div vacío esperando que JavaScript haga su trabajo.&lt;/p&gt;




&lt;h2&gt;
  
  
  La API interna que nadie documenta
&lt;/h2&gt;

&lt;p&gt;Abrí las herramientas de desarrollo del browser, filtré por Fetch/XHR, y observé el tráfico. Lo que vi fue revelador: la app de React estaba consumiendo una &lt;strong&gt;API interna&lt;/strong&gt; con endpoints bien estructurados, respuestas en JSON limpio, y toda la información organizada perfectamente.&lt;/p&gt;

&lt;p&gt;Datos que visualmente aparecen en pantalla para cualquier usuario, pero que internamente viajan como JSON estructurado, listo para ser procesado.&lt;/p&gt;

&lt;p&gt;El problema: esa API tenía protección. Un WAF (Web Application Firewall) en Azure que analizaba cada request y bloqueaba cualquier cosa que no pareciera un browser legítimo. Tokens de sesión con expiración. Cookies generadas dinámicamente. Un ecosistema de seguridad diseñado para que solo browsers reales pudieran consumir esos datos.&lt;/p&gt;

&lt;p&gt;Y tenía razón en bloquearte — si intentabas replicar el request manualmente, para cuando copiabas todos los headers necesarios, los tokens ya habían vencido.&lt;/p&gt;




&lt;h2&gt;
  
  
  El agente como humano digital
&lt;/h2&gt;

&lt;p&gt;Aquí entra el cambio de paradigma.&lt;/p&gt;

&lt;p&gt;Un agente con control de browser — usando herramientas como Playwright — no simula un request. &lt;strong&gt;Abre un browser real&lt;/strong&gt;. Chromium de verdad. Navega como lo haría un usuario. Las cookies se generan naturalmente. Los tokens están vigentes. El WAF ve exactamente lo que espera ver.&lt;/p&gt;

&lt;p&gt;Pero a diferencia del humano, el agente puede hacer algo que el humano no puede: &lt;strong&gt;obtener y usar los datos en el mismo instante, dentro del mismo contexto de sesión&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;El flujo es así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agente abre browser
    → navega al sitio (tokens se generan automáticamente)
    → intercepta la respuesta de la API en tiempo real
    → extrae los datos estructurados
    → los guarda en base de datos
    → cierra el browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Todo en segundos. Sin que ningún token tenga tiempo de vencer. Sin que ningún WAF detecte anomalías. Porque técnicamente no hay ninguna.&lt;/p&gt;




&lt;h2&gt;
  
  
  El problema del humano vs. el agente
&lt;/h2&gt;

&lt;p&gt;Cuando un humano intenta hacer esto manualmente:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Abre DevTools&lt;/li&gt;
&lt;li&gt;Filtra el tráfico de red&lt;/li&gt;
&lt;li&gt;Encuentra el request correcto&lt;/li&gt;
&lt;li&gt;Copia los headers&lt;/li&gt;
&lt;li&gt;Los pega en un cliente HTTP&lt;/li&gt;
&lt;li&gt;Ejecuta el request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Resultado:&lt;/strong&gt; Token vencido. Request bloqueado.&lt;/p&gt;

&lt;p&gt;El tiempo que tarda el humano en completar esos pasos es mayor que el tiempo de vida de los tokens de sesión. Es una carrera que el humano pierde por diseño.&lt;/p&gt;

&lt;p&gt;Cuando el agente hace lo mismo, no hay pasos separados. La obtención y el uso son &lt;strong&gt;una sola operación continua&lt;/strong&gt;. El agente no anota el token para usarlo después — lo usa mientras aún está caliente, en el mismo contexto donde nació.&lt;/p&gt;




&lt;h2&gt;
  
  
  ¿Es esto vulnerar algo?
&lt;/h2&gt;

&lt;p&gt;No. Y esa es la parte más interesante del análisis.&lt;/p&gt;

&lt;p&gt;Cualquier información que una página muestra públicamente, sin requerir autenticación, es información que el usuario tiene derecho a ver. El agente no hace nada que el usuario no pudiera hacer manualmente — simplemente lo hace más rápido y de forma programática.&lt;/p&gt;

&lt;p&gt;La línea ética está clara:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Información pública, uso personal o académico&lt;/strong&gt; → completamente legítimo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatización masiva que sature servidores&lt;/strong&gt; → problemático&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uso comercial de datos ajenos&lt;/strong&gt; → requiere revisar términos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acceso a cuentas de terceros&lt;/strong&gt; → ilegal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;8 requests al día para leer información pública genera menos tráfico que un usuario humano visitando el sitio dos veces. No hay daño, no hay saturación, no hay violación.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lo que esto significa para el desarrollo
&lt;/h2&gt;

&lt;p&gt;Estamos en un momento donde la barrera entre "ver datos" y "usar datos en código" prácticamente desapareció para información pública.&lt;/p&gt;

&lt;p&gt;Antes necesitabas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Una API oficial con documentación&lt;/li&gt;
&lt;li&gt;Credenciales y proceso de registro&lt;/li&gt;
&lt;li&gt;Límites de uso negociados&lt;/li&gt;
&lt;li&gt;Meses de espera para aprobación&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ahora necesitas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un agente con Playwright&lt;/li&gt;
&lt;li&gt;Decirle qué quieres obtener&lt;/li&gt;
&lt;li&gt;Esperar segundos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Las implicancias para el desarrollo de software son enormes. Cualquier fuente de datos pública se convierte potencialmente en una fuente estructurada para tus aplicaciones. Sin fricción. Sin burocracia. Sin depender de que la empresa decida publicar una API.&lt;/p&gt;




&lt;h2&gt;
  
  
  La arquitectura que emerge
&lt;/h2&gt;

&lt;p&gt;Lo interesante no es solo obtener los datos una vez — es cómo esto se integra en una arquitectura completa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agente recopilador (cron programado)
    → navega fuentes públicas
    → extrae datos estructurados
    → guarda en base de datos local

MCP Server
    → lee la base de datos
    → expone herramientas al agente de consulta

Usuario
    → pregunta en lenguaje natural
    → el agente consulta las herramientas
    → respuesta instantánea desde datos frescos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El agente recopilador y el agente de consulta son independientes. Uno escribe, el otro lee. La base de datos es el punto de encuentro. Y el usuario final nunca sabe — ni necesita saber — de dónde vienen los datos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Las páginas web públicas siempre fueron accesibles para humanos. Lo nuevo es que ahora son igualmente accesibles para agentes — y en algunos aspectos, el agente las maneja mejor que el humano.&lt;/p&gt;

&lt;p&gt;No porque rompa barreras de seguridad. Sino porque opera dentro de los mismos canales legítimos que usa cualquier browser, pero sin las limitaciones de tiempo y coordinación que tiene un humano copiando y pegando headers.&lt;/p&gt;

&lt;p&gt;El browser ya no es solo para humanos. Y eso cambia bastante cómo pensamos el acceso a datos en el desarrollo moderno.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este post es parte de una exploración práctica sobre agentes de IA. Las conclusiones son aplicables a cualquier fuente de información pública en la web.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>web</category>
      <category>webdev</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>(Spanish) [App] Visualización de Denuncias en el mapa del Perú - Uso de Streamlit + Pandas + Folium</title>
      <dc:creator>Christian Gonzales Komiya</dc:creator>
      <pubDate>Tue, 09 Jun 2026 15:30:56 +0000</pubDate>
      <link>https://dev.to/ckomiya/spanish-app-visualizacion-de-denuncias-en-el-mapa-del-peru-uso-de-streamlit-pandas-folium-1f74</link>
      <guid>https://dev.to/ckomiya/spanish-app-visualizacion-de-denuncias-en-el-mapa-del-peru-uso-de-streamlit-pandas-folium-1f74</guid>
      <description>&lt;p&gt;En los últimos meses, la inseguridad ciudadana en el Perú ha ido en aumento.&lt;br&gt;
&amp;nbsp;Quise explorar una manera simple y accesible de visualizar esta problemática usando datos abiertos y herramientas de análisis de datos.&lt;br&gt;
Este es un pequeño proyecto motivado por un dataset de denuncias policiales publicado por la Policía Nacional del Perú en la plataforma oficial de datos abiertos del Estado Peruano.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flzf3rrl2d6k2adknvp5y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flzf3rrl2d6k2adknvp5y.png" alt=" " width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dataset utilizado&lt;/strong&gt;: Denuncias Policiales - Enero 2018 a Junio 2025&lt;br&gt;
&amp;nbsp;(También lo puedes encontrar buscando "Denuncia" en datosabiertos.gob.pe)&lt;/p&gt;

&lt;p&gt;El dataset incluye (al corte de agosto 2025) modalidades como:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Estafa&lt;/li&gt;
&lt;li&gt;Extorsión&lt;/li&gt;
&lt;li&gt;Homicidio&lt;/li&gt;
&lt;li&gt;Hurto&lt;/li&gt;
&lt;li&gt;Robo&lt;/li&gt;
&lt;li&gt;Violencia contra la mujer&lt;/li&gt;
&lt;li&gt;Otros&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Link de la app -&amp;gt; &lt;a href="https://denuncias-pnp.streamlit.app/" rel="noopener noreferrer"&gt;https://denuncias-pnp.streamlit.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🎯 Motivación&lt;br&gt;
La idea nació como una forma de:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Analizar cómo han evolucionado las denuncias en el tiempo.&lt;/li&gt;
&lt;li&gt;Ubicarlas en un mapa para identificar zonas con mayor incidencia.&lt;/li&gt;
&lt;li&gt;Darle un formato visual e interactivo, aprovechando herramientas de código abierto.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;🛠 Herramientas utilizadas&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Streamlit → Framework en Python que permite crear aplicaciones web interactivas para análisis de datos de forma rápida y sencilla.&lt;/li&gt;
&lt;li&gt;Pandas → Librería para manipulación y análisis de datos en Python. Ideal para trabajar con tablas, filtrar y agrupar datos.&lt;/li&gt;
&lt;li&gt;Folium → Librería para generar mapas interactivos en Python usando Leaflet.js.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🌍 Visualización en mapa&lt;br&gt;
La parte más llamativa del proyecto es la representación geográfica de las denuncias.&lt;br&gt;
&amp;nbsp;A partir de los datos filtrados por año y tipo de modalidad, se agrupan las denuncias por distrito y se dibujan círculos proporcionales a la cantidad de casos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# --- Mapa por distrito ---
&lt;/span&gt;&lt;span class="n"&gt;df_map_filt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANIO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;modalidad&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Todas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;df_map_filt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df_map_filt&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df_map_filt&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P_MODALIDADES&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;modalidad&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;df_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;df_map_filt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UBIGEO_HECHO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;as_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cantidad&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;df_ubigeo&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inei&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;distrito&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;longitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
        &lt;span class="n"&gt;left_on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UBIGEO_HECHO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;right_on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inei&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;how&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dropna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;longitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subheader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🌍 Mapa por distrito&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;folium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;9.19&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;75.02&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;zoom_start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;max_c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cantidad&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterrows&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;radius&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cantidad&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;max_c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;max_c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="n"&gt;folium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CircleMarker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;longitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
        &lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;crimson&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;fill_opacity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;popup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;distrito&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cantidad&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;add_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;st_folium&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Qué hace este código:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Filtra las denuncias por año y modalidad.&lt;/li&gt;
&lt;li&gt;Agrupa por distrito (usando el código UBIGEO) y suma la cantidad de casos.&lt;/li&gt;
&lt;li&gt;Une la información con coordenadas geográficas para cada distrito.&lt;/li&gt;
&lt;li&gt;Crea un mapa con círculos rojos cuyo tamaño es proporcional a la cantidad de denuncias.&lt;/li&gt;
&lt;li&gt;Muestra un popup con el nombre del distrito y el número de denuncias.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdef16b7qnp1xxsmj2hr9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdef16b7qnp1xxsmj2hr9.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📈 Gráfico de tendencia por tipo de denuncia&lt;br&gt;
Además del mapa, la aplicación muestra un gráfico de tendencia donde se observa la evolución de cada tipo de denuncia a lo largo de los años, desglosado por departamento.&lt;br&gt;
&amp;nbsp;Esto permite identificar patrones, por ejemplo, si ciertos delitos han ido aumentando o disminuyendo con el tiempo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ujnihtdd303abhd7paz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ujnihtdd303abhd7paz.png" alt=" " width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📦 Código fuente&lt;br&gt;
El código completo y los datos procesados están disponibles en mi repositorio de GitHub:&lt;br&gt;
&amp;nbsp;🔗 &lt;a href="https://github.com/ckomiya/data_science_denuncias_pnp" rel="noopener noreferrer"&gt;https://github.com/ckomiya/data_science_denuncias_pnp&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>(Spanish) Arquitectura Hexagonal (Ports &amp; Adapters) - Beneficios y Ejemplo en Java</title>
      <dc:creator>Christian Gonzales Komiya</dc:creator>
      <pubDate>Tue, 09 Jun 2026 15:22:23 +0000</pubDate>
      <link>https://dev.to/ckomiya/spanish-arquitectura-hexagonal-ports-adapters-beneficios-y-ejemplo-en-java-711</link>
      <guid>https://dev.to/ckomiya/spanish-arquitectura-hexagonal-ports-adapters-beneficios-y-ejemplo-en-java-711</guid>
      <description>&lt;p&gt;Mientras leía el libro &lt;em&gt;Hexagonal Architecture Explained&lt;/em&gt; de Alistair Cockburn y Juan Manuel Garrido, me encontré con una sección que destaca beneficios clave de este patrón.&lt;br&gt;&lt;br&gt;
En este artículo represento en código Java los &lt;strong&gt;tres pasos esenciales&lt;/strong&gt; para implementar la arquitectura y lograr dichos beneficios.&lt;/p&gt;


&lt;h2&gt;
  
  
  Beneficios destacados
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;El sistema queda abierto a tener &lt;strong&gt;pruebas de regresión&lt;/strong&gt; para todas sus funciones, de extremo a extremo.&lt;/li&gt;
&lt;li&gt;Es posible &lt;strong&gt;variar las tecnologías externas&lt;/strong&gt; conforme evolucionan (como siempre lo hacen) sin un esfuerzo enorme.&lt;/li&gt;
&lt;li&gt;Estos dos beneficios adicionales superan fácilmente el pequeño costo extra de implementar la solución.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Paso 1 – API del sistema (interfaz proporcionada)
&lt;/h2&gt;

&lt;p&gt;Creamos la aplicación para que sus servicios estén disponibles como llamadas a funciones, sin depender directamente de UI o base de datos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Paso 1: API del sistema (interfaz proporcionada)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaxCalculatorApp&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Servicio principal expuesto como API&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;calculateTax&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aquí la aplicación no sabe nada de dónde viene el rate ni de si hay base de datos: es solo lógica pura.&lt;/p&gt;




&lt;h2&gt;
  
  
  Paso 2 – Variables para las conexiones externas (interfaces requeridas)
&lt;/h2&gt;

&lt;p&gt;Agregamos variables para cada conexión externa (por ejemplo, un servicio que da la tasa de impuestos).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Interfaz requerida: servicio externo para obtener tasa de impuestos&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;TaxRateProvider&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;getTaxRate&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Paso 2: la aplicación ahora tiene una variable para su conexión externa&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaxCalculatorApp&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Variable para el proveedor de tasas (actor secundario)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;TaxRateProvider&lt;/span&gt; &lt;span class="n"&gt;taxRateProvider&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Inyectamos la dependencia (aún puede ser null si no se configura)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setTaxRateProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TaxRateProvider&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;taxRateProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Servicio principal usa la interfaz requerida&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;calculateTax&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;taxRateProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTaxRate&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahora la aplicación no llama directamente a un servicio concreto: usa una interfaz requerida (&lt;code&gt;TaxRateProvider&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  Paso 3 – Configuración con actores reales o adaptadores (inyección de dependencias)
&lt;/h2&gt;

&lt;p&gt;En la construcción/configuración, conectamos la app con un adaptador concreto, o con un doble de pruebas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Adaptador concreto que obtiene tasa desde una BD (ejemplo simple)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DatabaseTaxRateAdapter&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;TaxRateProvider&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;getTaxRate&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Aquí iría la lógica real de conexión a base de datos&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.21&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Ejemplo: IVA 21%&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Adaptador doble de pruebas&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FixedTaxRateStub&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;TaxRateProvider&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;fixedRate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;FixedTaxRateStub&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;fixedRate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fixedRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fixedRate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;getTaxRate&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fixedRate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Paso 3: Configuración y uso&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Main&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;TaxCalculatorApp&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TaxCalculatorApp&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Configuración para producción&lt;/span&gt;
        &lt;span class="nc"&gt;TaxRateProvider&lt;/span&gt; &lt;span class="n"&gt;dbAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DatabaseTaxRateAdapter&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTaxRateProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dbAdapter&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Impuesto: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calculateTax&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// Configuración para pruebas&lt;/span&gt;
        &lt;span class="nc"&gt;TaxRateProvider&lt;/span&gt; &lt;span class="n"&gt;testStub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FixedTaxRateStub&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTaxRateProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testStub&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Impuesto (prueba): "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calculateTax&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ✅ Qué logramos siguiendo los pasos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;La app expone su lógica a través de una API clara (&lt;code&gt;calculateTax&lt;/code&gt;), sin UI ni BD acoplada.&lt;/li&gt;
&lt;li&gt;Creamos variables para las dependencias externas (&lt;code&gt;TaxRateProvider&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Conectamos esas dependencias en tiempo de configuración, ya sea con adaptadores reales o dobles de prueba.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>java</category>
      <category>spanish</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>(Spanish) Cómo iniciar un proyecto con Claude Code</title>
      <dc:creator>Christian Gonzales Komiya</dc:creator>
      <pubDate>Tue, 09 Jun 2026 15:01:25 +0000</pubDate>
      <link>https://dev.to/ckomiya/como-iniciar-un-proyecto-con-claude-code-46he</link>
      <guid>https://dev.to/ckomiya/como-iniciar-un-proyecto-con-claude-code-46he</guid>
      <description>&lt;p&gt;Guía rápida para desarrolladores que quieren arrancar un proyecto de forma ordenada y sacarle el máximo provecho a Claude Code.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Este artículo no pretende ser una guía exhaustiva — es mi guía de bolsillo personal. La escribí para consultarla cada vez que inicio un proyecto nuevo y no querer recordar de memoria qué pasos seguir. Si te sirve a ti también, genial.&lt;/p&gt;

&lt;p&gt;Fuente: &lt;em&gt;Claude Code Advanced&lt;/em&gt; de Javier Rayon.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Paso 1 — Pre-Setup: prepara el terreno
&lt;/h2&gt;

&lt;p&gt;Antes de escribir una sola línea de código, pídele a Claude que configure la estructura del proyecto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Estoy comenzando un proyecto de [descripción]. Antes de programar, ayúdame a crear:
- Estructura de directorios
- CLAUDE.md con convenciones
- Scripts de desarrollo (dev, test, build)
- Configuración de linting y formateo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude generará algo así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;my-project/
&lt;span class="p"&gt;-&lt;/span&gt; src/
&lt;span class="p"&gt;    -&lt;/span&gt; routes/
&lt;span class="p"&gt;    -&lt;/span&gt; services/
&lt;span class="p"&gt;    -&lt;/span&gt; models/
&lt;span class="p"&gt;    -&lt;/span&gt; utils/
&lt;span class="p"&gt;-&lt;/span&gt; tests/
&lt;span class="p"&gt;-&lt;/span&gt; scripts/
&lt;span class="p"&gt;-&lt;/span&gt; .claude/
&lt;span class="p"&gt;    -&lt;/span&gt; settings.json
&lt;span class="p"&gt;    -&lt;/span&gt; CLAUDE.md
&lt;span class="p"&gt;    -&lt;/span&gt; rules/
&lt;span class="p"&gt;        -&lt;/span&gt; testing.md
&lt;span class="p"&gt;        -&lt;/span&gt; api-patterns.md
&lt;span class="p"&gt;        -&lt;/span&gt; security.md
&lt;span class="p"&gt;    -&lt;/span&gt; skills/
&lt;span class="p"&gt;        -&lt;/span&gt; deploy/skill.md
&lt;span class="p"&gt;        -&lt;/span&gt; db-migrate/skill.md
&lt;span class="p"&gt;    -&lt;/span&gt; agents/
&lt;span class="p"&gt;        -&lt;/span&gt; reviewer.md
&lt;span class="p"&gt;        -&lt;/span&gt; qa.md
&lt;span class="p"&gt;    -&lt;/span&gt; commands/
&lt;span class="p"&gt;        -&lt;/span&gt; COMMANDS.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada carpeta tiene un propósito claro:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rules/&lt;/code&gt; — lo que Claude siempre debe respetar (frameworks, patrones, seguridad)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;skills/&lt;/code&gt; — procedimientos reutilizables (deploy, migraciones)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;agents/&lt;/code&gt; — roles especializados (revisor de código, QA)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;commands/&lt;/code&gt; — comandos slash personalizados (&lt;code&gt;/deploy&lt;/code&gt;, &lt;code&gt;/migrate&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ejemplo de regla&lt;/strong&gt; (&lt;code&gt;.claude/rules/testing.md&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Reglas de Testing&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Framework: vitest (NUNCA jest)
&lt;span class="p"&gt;-&lt;/span&gt; Convención de nombres: &lt;span class="err"&gt;*&lt;/span&gt;.test.ts
&lt;span class="p"&gt;-&lt;/span&gt; Toda función pública debe tener al menos una prueba
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ejemplo de skill&lt;/strong&gt; (&lt;code&gt;.claude/skills/deploy/skill.md&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Desplegar a Producción&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Ejecutar pruebas: &lt;span class="sb"&gt;`npm test`&lt;/span&gt;
&lt;span class="p"&gt;2.&lt;/span&gt; Compilar: &lt;span class="sb"&gt;`npm run build`&lt;/span&gt;
&lt;span class="p"&gt;3.&lt;/span&gt; Verificar que no haya cambios sin commit
&lt;span class="p"&gt;4.&lt;/span&gt; Ejecutar: &lt;span class="sb"&gt;`./scripts/deploy.sh production`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Agrega &lt;code&gt;.claude/worktrees/&lt;/code&gt; a tu &lt;code&gt;.gitignore&lt;/code&gt; desde el inicio.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Paso 2 — Plan: define qué vas a construir
&lt;/h2&gt;

&lt;p&gt;Con el proyecto configurado, entra en modo planificación antes de tocar código.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude analizará el problema y presentará un plan detallado sin modificar nada todavía. Tú revisas, ajustas y le das luz verde.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt vago (evitar):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build a login system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Prompt específico (usar):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan an OAuth2 login implementation (Google and GitHub).
Requirements:
- JWT with refresh tokens
- 24h expiration
- Rate limiting of 5 attempts per minute
- E2E tests with Playwright
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Técnica avanzada — revisión cruzada:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Usa dos sesiones de Claude en paralelo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sesión A genera el plan completo.&lt;/li&gt;
&lt;li&gt;Sesión B lo revisa con este prompt:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Revisa este plan como si fueras un Staff Engineer.
Busca puntos débiles, casos límite no considerados
y dependencias faltantes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ajusta el plan con el feedback de la Sesión B antes de ejecutar.&lt;/p&gt;




&lt;h2&gt;
  
  
  Paso 3 — Worktrees: ejecuta en paralelo
&lt;/h2&gt;

&lt;p&gt;Con el plan listo, identifica 3 a 5 tareas que puedan avanzar simultáneamente y crea un worktree para cada una.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opción A — Worktrees manuales (control total):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add &lt;span class="nt"&gt;-b&lt;/span&gt; feature/auth ../wt-auth main
git worktree add &lt;span class="nt"&gt;-b&lt;/span&gt; feature/api ../wt-api main
git worktree add &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;/e2e ../wt-tests main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Luego lanza Claude en cada uno:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal 1&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../wt-auth &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; claude

&lt;span class="c"&gt;# Terminal 2&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../wt-api &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; claude

&lt;span class="c"&gt;# Terminal 3&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../wt-tests &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Opción B — Worktrees nativos de Claude (más rápido):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;--worktree&lt;/span&gt; auth-feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude crea la rama, el directorio y la sesión en un solo paso. Al terminar, limpia automáticamente si no hubo cambios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flujo de cierre:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Cada worktree hace commit y crea su PR&lt;/span&gt;
git add &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: auth OAuth2"&lt;/span&gt;
gh &lt;span class="nb"&gt;pr &lt;/span&gt;create &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"Auth OAuth2"&lt;/span&gt; &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"Descripción"&lt;/span&gt;

&lt;span class="c"&gt;# Al terminar todos, limpia&lt;/span&gt;
git worktree remove ../wt-auth
git worktree prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Si en lugar de tareas puntuales necesitas cambios masivos en todo el proyecto (por ejemplo, migrar 50 archivos), usa &lt;code&gt;/batch&lt;/code&gt; en lugar de worktrees manuales — Claude los gestiona automáticamente.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resumen
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Paso&lt;/th&gt;
&lt;th&gt;Qué haces&lt;/th&gt;
&lt;th&gt;Herramienta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pre-Setup&lt;/td&gt;
&lt;td&gt;Configuras estructura, reglas, skills y agentes&lt;/td&gt;
&lt;td&gt;Prompt inicial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan&lt;/td&gt;
&lt;td&gt;Defines qué construir y lo validas&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/plan&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worktrees&lt;/td&gt;
&lt;td&gt;Ejecutas tareas en paralelo&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git worktree&lt;/code&gt; / &lt;code&gt;claude --worktree&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>spanish</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
